Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Martin 2021-08-23 23:56:33 +02:00 committed by GitHub
parent 0806af4aec
commit d546453880
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 196 additions and 73 deletions

View File

@ -20,7 +20,7 @@ pub struct Template(Rc<Vec<TemplateNode>>);
#[derive(Clone)]
enum TemplateNode {
/// A word space.
Space,
Space(Vec<Decoration>),
/// A line break.
Linebreak,
/// A paragraph break.
@ -28,11 +28,11 @@ enum TemplateNode {
/// A page break.
Pagebreak(bool),
/// Plain text.
Text(EcoString),
Text(EcoString, Vec<Decoration>),
/// Spacing.
Spacing(GenAxis, Linear),
/// An inline node builder.
Inline(Rc<dyn Fn(&State) -> LayoutNode>),
Inline(Rc<dyn Fn(&State) -> LayoutNode>, Vec<Decoration>),
/// An block node builder.
Block(Rc<dyn Fn(&State) -> LayoutNode>),
/// Save the current state.
@ -43,6 +43,13 @@ enum TemplateNode {
Modify(Rc<dyn Fn(&mut State)>),
}
/// A template node decoration.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Decoration {
/// A link.
Link(EcoString),
}
impl Template {
/// Create a new, empty template.
pub fn new() -> Self {
@ -55,7 +62,7 @@ impl Template {
F: Fn(&State) -> T + 'static,
T: Into<LayoutNode>,
{
let node = TemplateNode::Inline(Rc::new(move |s| f(s).into()));
let node = TemplateNode::Inline(Rc::new(move |s| f(s).into()), vec![]);
Self(Rc::new(vec![node]))
}
@ -71,7 +78,7 @@ impl Template {
/// Add a word space to the template.
pub fn space(&mut self) {
self.make_mut().push(TemplateNode::Space);
self.make_mut().push(TemplateNode::Space(vec![]));
}
/// Add a line break to the template.
@ -91,7 +98,7 @@ impl Template {
/// Add text to the template.
pub fn text(&mut self, text: impl Into<EcoString>) {
self.make_mut().push(TemplateNode::Text(text.into()));
self.make_mut().push(TemplateNode::Text(text.into(), vec![]));
}
/// Add text, but in monospace.
@ -107,6 +114,19 @@ impl Template {
self.make_mut().push(TemplateNode::Spacing(axis, spacing));
}
/// Add a decoration to the last template node.
pub fn decorate(&mut self, deco: Decoration) {
for node in self.make_mut() {
let decos = match node {
TemplateNode::Space(decos) => decos,
TemplateNode::Text(_, decos) => decos,
TemplateNode::Inline(_, decos) => decos,
_ => continue,
};
decos.push(deco.clone());
}
}
/// Register a restorable snapshot.
pub fn save(&mut self) {
self.make_mut().push(TemplateNode::Save);
@ -201,7 +221,7 @@ impl Add<Str> for Template {
type Output = Self;
fn add(mut self, rhs: Str) -> Self::Output {
Rc::make_mut(&mut self.0).push(TemplateNode::Text(rhs.into()));
Rc::make_mut(&mut self.0).push(TemplateNode::Text(rhs.into(), vec![]));
self
}
}
@ -210,7 +230,7 @@ impl Add<Template> for Str {
type Output = Template;
fn add(self, mut rhs: Template) -> Self::Output {
Rc::make_mut(&mut rhs.0).insert(0, TemplateNode::Text(self.into()));
Rc::make_mut(&mut rhs.0).insert(0, TemplateNode::Text(self.into(), vec![]));
rhs
}
}
@ -261,26 +281,26 @@ impl Builder {
self.pagebreak(true, false);
}
}
TemplateNode::Space => self.space(),
TemplateNode::Space(decos) => self.space(decos),
TemplateNode::Linebreak => self.linebreak(),
TemplateNode::Parbreak => self.parbreak(),
TemplateNode::Pagebreak(keep) => self.pagebreak(*keep, true),
TemplateNode::Text(text) => self.text(text),
TemplateNode::Text(text, decos) => self.text(text, decos),
TemplateNode::Spacing(axis, amount) => self.spacing(*axis, *amount),
TemplateNode::Inline(f) => self.inline(f(&self.state)),
TemplateNode::Inline(f, decos) => self.inline(f(&self.state), decos),
TemplateNode::Block(f) => self.block(f(&self.state)),
TemplateNode::Modify(f) => f(&mut self.state),
}
}
/// Push a word space into the active paragraph.
fn space(&mut self) {
self.stack.par.push_soft(self.make_text_node(' '));
fn space(&mut self, decos: &[Decoration]) {
self.stack.par.push_soft(self.make_text_node(' ', decos.to_vec()));
}
/// Apply a forced line break.
fn linebreak(&mut self) {
self.stack.par.push_hard(self.make_text_node('\n'));
self.stack.par.push_hard(self.make_text_node('\n', vec![]));
}
/// Apply a forced paragraph break.
@ -300,16 +320,14 @@ impl Builder {
}
/// Push text into the active paragraph.
///
/// The text is split into lines at newlines.
fn text(&mut self, text: impl Into<EcoString>) {
self.stack.par.push(self.make_text_node(text));
fn text(&mut self, text: impl Into<EcoString>, decos: &[Decoration]) {
self.stack.par.push(self.make_text_node(text, decos.to_vec()));
}
/// Push an inline node into the active paragraph.
fn inline(&mut self, node: impl Into<LayoutNode>) {
fn inline(&mut self, node: impl Into<LayoutNode>, decos: &[Decoration]) {
let align = self.state.aligns.inline;
self.stack.par.push(ParChild::Any(node.into(), align));
self.stack.par.push(ParChild::Any(node.into(), align, decos.to_vec()));
}
/// Push a block node into the active stack, finishing the active paragraph.
@ -348,11 +366,16 @@ impl Builder {
/// Construct a text node with the given text and settings from the active
/// state.
fn make_text_node(&self, text: impl Into<EcoString>) -> ParChild {
fn make_text_node(
&self,
text: impl Into<EcoString>,
decos: Vec<Decoration>,
) -> ParChild {
ParChild::Text(
text.into(),
self.state.aligns.inline,
Rc::clone(&self.state.font),
decos,
)
}
}
@ -465,11 +488,14 @@ impl ParBuilder {
}
fn push_inner(&mut self, child: ParChild) {
if let ParChild::Text(curr_text, curr_align, curr_props) = &child {
if let Some(ParChild::Text(prev_text, prev_align, prev_props)) =
if let ParChild::Text(curr_text, curr_align, curr_props, curr_decos) = &child {
if let Some(ParChild::Text(prev_text, prev_align, prev_props, prev_decos)) =
self.children.last_mut()
{
if prev_align == curr_align && Rc::ptr_eq(prev_props, curr_props) {
if prev_align == curr_align
&& Rc::ptr_eq(prev_props, curr_props)
&& curr_decos == prev_decos
{
prev_text.push_str(&curr_text);
return;
}

View File

@ -115,6 +115,7 @@ fn walk_item(ctx: &mut EvalContext, label: EcoString, body: Template) {
label.clone(),
state.aligns.inline,
Rc::clone(&state.font),
vec![],
)],
};
StackNode {

View File

@ -8,8 +8,8 @@ use std::rc::Rc;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
use miniz_oxide::deflate;
use pdf_writer::{
CidFontType, ColorSpace, Content, Filter, FontFlags, Name, PdfWriter, Rect, Ref, Str,
SystemInfo, UnicodeCmap,
ActionType, AnnotationType, CidFontType, ColorSpace, Content, Filter, FontFlags,
Name, PdfWriter, Rect, Ref, Str, SystemInfo, UnicodeCmap,
};
use ttf_parser::{name_id, GlyphId};
@ -59,6 +59,7 @@ impl<'a> PdfExporter<'a> {
}
image_map.insert(id);
}
Element::Link(_, _) => {}
}
}
}
@ -116,16 +117,34 @@ impl<'a> PdfExporter<'a> {
for ((page_id, content_id), page) in
self.refs.pages().zip(self.refs.contents()).zip(self.frames)
{
self.writer
.page(page_id)
let w = page.size.w.to_pt() as f32;
let h = page.size.h.to_pt() as f32;
let mut page_writer = self.writer.page(page_id);
page_writer
.parent(self.refs.page_tree)
.media_box(Rect::new(
0.0,
0.0,
page.size.w.to_pt() as f32,
page.size.h.to_pt() as f32,
))
.contents(content_id);
.media_box(Rect::new(0.0, 0.0, w, h));
let mut annotations = page_writer.annotations();
for (pos, element) in page.elements() {
if let Element::Link(href, size) = element {
let x = pos.x.to_pt() as f32;
let y = (page.size.h - pos.y).to_pt() as f32;
let w = size.w.to_pt() as f32;
let h = size.h.to_pt() as f32;
annotations
.push()
.subtype(AnnotationType::Link)
.rect(Rect::new(x, y - h, x + w, y))
.action()
.action_type(ActionType::Uri)
.uri(Str(href.as_bytes()));
}
}
drop(annotations);
page_writer.contents(content_id);
}
}
@ -248,6 +267,8 @@ impl<'a> PdfExporter<'a> {
content.x_object(Name(name.as_bytes()));
content.restore_state();
}
Element::Link(_, _) => {}
}
}

View File

@ -16,17 +16,17 @@ pub struct Frame {
/// The baseline of the frame measured from the top.
pub baseline: Length,
/// The elements composing this layout.
children: Vec<(Point, Child)>,
pub children: Vec<(Point, FrameChild)>,
}
/// A frame can contain two different kinds of children: a leaf element or a
/// nested frame.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
enum Child {
pub enum FrameChild {
/// A leaf node in the frame tree.
Element(Element),
/// An interior node.
Frame(Rc<Frame>),
/// An interior node with an optional index.
Frame(Option<usize>, Rc<Frame>),
}
impl Frame {
@ -38,17 +38,22 @@ impl Frame {
/// Add an element at a position in the foreground.
pub fn push(&mut self, pos: Point, element: Element) {
self.children.push((pos, Child::Element(element)));
self.children.push((pos, FrameChild::Element(element)));
}
/// Add an element at a position in the background.
pub fn prepend(&mut self, pos: Point, element: Element) {
self.children.insert(0, (pos, Child::Element(element)))
self.children.insert(0, (pos, FrameChild::Element(element)));
}
/// Add a frame element.
pub fn push_frame(&mut self, pos: Point, subframe: Rc<Self>) {
self.children.push((pos, Child::Frame(subframe)))
self.children.push((pos, FrameChild::Frame(None, subframe)))
}
/// Add a frame element with an index of arbitrary use.
pub fn push_indexed_frame(&mut self, pos: Point, index: usize, subframe: Rc<Self>) {
self.children.push((pos, FrameChild::Frame(Some(index), subframe)));
}
/// Add all elements of another frame, placing them relative to the given
@ -85,12 +90,12 @@ impl<'a> Iterator for Elements<'a> {
fn next(&mut self) -> Option<Self::Item> {
let (cursor, offset, frame) = self.stack.last_mut()?;
match frame.children.get(*cursor) {
Some((pos, Child::Frame(f))) => {
Some((pos, FrameChild::Frame(_, f))) => {
let new_offset = *offset + *pos;
self.stack.push((0, new_offset, f.as_ref()));
self.next()
}
Some((pos, Child::Element(e))) => {
Some((pos, FrameChild::Element(e))) => {
*cursor += 1;
Some((*offset + *pos, e))
}
@ -115,6 +120,8 @@ pub enum Element {
Geometry(Geometry, Paint),
/// A raster image.
Image(ImageId, Size),
/// A link to an external resource.
Link(String, Size),
}
/// A run of shaped text.

View File

@ -4,7 +4,7 @@ use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
use super::*;
use crate::eval::FontState;
use crate::eval::{Decoration, FontState};
use crate::util::{EcoString, RangeExt, SliceExt};
type Range = std::ops::Range<usize>;
@ -26,9 +26,9 @@ pub enum ParChild {
/// Spacing between other nodes.
Spacing(Linear),
/// A run of text and how to align it in its line.
Text(EcoString, Align, Rc<FontState>),
Text(EcoString, Align, Rc<FontState>, Vec<Decoration>),
/// Any child node and how to align it in its line.
Any(LayoutNode, Align),
Any(LayoutNode, Align, Vec<Decoration>),
}
impl Layout for ParNode {
@ -48,7 +48,7 @@ impl Layout for ParNode {
let layouter = ParLayouter::new(self, ctx, regions, bidi);
// Find suitable linebreaks.
layouter.layout(ctx, regions.clone())
layouter.layout(ctx, &self.children, regions.clone())
}
}
@ -79,8 +79,8 @@ impl ParNode {
fn strings(&self) -> impl Iterator<Item = &str> {
self.children.iter().map(|child| match child {
ParChild::Spacing(_) => " ",
ParChild::Text(ref piece, _, _) => piece,
ParChild::Any(_, _) => "\u{FFFC}",
ParChild::Text(ref piece, ..) => piece,
ParChild::Any(..) => "\u{FFFC}",
})
}
}
@ -119,25 +119,25 @@ impl<'a> ParLayouter<'a> {
let mut ranges = vec![];
// Layout the children and collect them into items.
for (range, child) in par.ranges().zip(&par.children) {
for (i, (range, child)) in par.ranges().zip(&par.children).enumerate() {
match *child {
ParChild::Spacing(amount) => {
let resolved = amount.resolve(regions.current.w);
items.push(ParItem::Spacing(resolved));
ranges.push(range);
}
ParChild::Text(_, align, ref state) => {
ParChild::Text(_, align, ref state, _) => {
// TODO: Also split by language and script.
for (subrange, dir) in split_runs(&bidi, range) {
let text = &bidi.text[subrange.clone()];
let shaped = shape(ctx, text, dir, state);
items.push(ParItem::Text(shaped, align));
items.push(ParItem::Text(shaped, align, i));
ranges.push(subrange);
}
}
ParChild::Any(ref node, align) => {
ParChild::Any(ref node, align, _) => {
let frame = node.layout(ctx, regions).remove(0);
items.push(ParItem::Frame(frame.item, align));
items.push(ParItem::Frame(frame.item, align, i));
ranges.push(range);
}
}
@ -156,9 +156,10 @@ impl<'a> ParLayouter<'a> {
fn layout(
self,
ctx: &mut LayoutContext,
children: &[ParChild],
regions: Regions,
) -> Vec<Constrained<Rc<Frame>>> {
let mut stack = LineStack::new(self.line_spacing, regions);
let mut stack = LineStack::new(self.line_spacing, children, regions);
// The current line attempt.
// Invariant: Always fits into `stack.regions.current`.
@ -273,9 +274,9 @@ enum ParItem<'a> {
/// Spacing between other items.
Spacing(Length),
/// A shaped text run with consistent direction.
Text(ShapedText<'a>, Align),
Text(ShapedText<'a>, Align, usize),
/// A layouted child node.
Frame(Rc<Frame>, Align),
Frame(Rc<Frame>, Align, usize),
}
impl ParItem<'_> {
@ -283,8 +284,8 @@ impl ParItem<'_> {
pub fn size(&self) -> Size {
match self {
Self::Spacing(amount) => Size::new(*amount, Length::zero()),
Self::Text(shaped, _) => shaped.size,
Self::Frame(frame, _) => frame.size,
Self::Text(shaped, ..) => shaped.size,
Self::Frame(frame, ..) => frame.size,
}
}
@ -292,8 +293,17 @@ impl ParItem<'_> {
pub fn baseline(&self) -> Length {
match self {
Self::Spacing(_) => Length::zero(),
Self::Text(shaped, _) => shaped.baseline,
Self::Frame(frame, _) => frame.baseline,
Self::Text(shaped, ..) => shaped.baseline,
Self::Frame(frame, ..) => frame.baseline,
}
}
/// The index of the `ParChild` that this item belongs to.
pub fn index(&self) -> Option<usize> {
match *self {
Self::Spacing(_) => None,
Self::Text(.., index) => Some(index),
Self::Frame(.., index) => Some(index),
}
}
}
@ -301,6 +311,7 @@ impl ParItem<'_> {
/// Stacks lines on top of each other.
struct LineStack<'a> {
line_spacing: Length,
children: &'a [ParChild],
full: Size,
regions: Regions,
size: Size,
@ -312,11 +323,12 @@ struct LineStack<'a> {
impl<'a> LineStack<'a> {
/// Create an empty line stack.
fn new(line_spacing: Length, regions: Regions) -> Self {
fn new(line_spacing: Length, children: &'a [ParChild], regions: Regions) -> Self {
Self {
line_spacing,
constraints: Constraints::new(regions.expand),
children,
full: regions.current,
constraints: Constraints::new(regions.expand),
regions,
size: Size::zero(),
lines: vec![],
@ -368,6 +380,25 @@ impl<'a> LineStack<'a> {
output.merge_frame(pos, frame);
}
// For each frame, we look if any decorations apply.
for i in 0 .. output.children.len() {
let &(point, ref child) = &output.children[i];
if let &FrameChild::Frame(Some(frame_idx), ref frame) = child {
let size = frame.size;
for deco in match &self.children[frame_idx] {
ParChild::Spacing(_) => continue,
ParChild::Text(.., decos) => decos,
ParChild::Any(.., decos) => decos,
} {
match deco {
Decoration::Link(href) => {
output.push(point, Element::Link(href.to_string(), size));
}
}
}
}
}
self.finished.push(output.constrain(self.constraints));
self.regions.next();
self.full = self.regions.current;
@ -426,7 +457,7 @@ impl<'a> LineLayout<'a> {
// Reshape the last item if it's split in half.
let mut last = None;
if let Some((ParItem::Text(shaped, align), rest)) = items.split_last() {
if let Some((ParItem::Text(shaped, align, i), rest)) = items.split_last() {
// Compute the range we want to shape, trimming whitespace at the
// end of the line.
let base = par.ranges[last_idx].start;
@ -442,7 +473,7 @@ impl<'a> LineLayout<'a> {
if !range.is_empty() || rest.is_empty() {
// Reshape that part.
let reshaped = shaped.reshape(ctx, range);
last = Some(ParItem::Text(reshaped, *align));
last = Some(ParItem::Text(reshaped, *align, *i));
}
items = rest;
@ -452,7 +483,7 @@ impl<'a> LineLayout<'a> {
// Reshape the start item if it's split in half.
let mut first = None;
if let Some((ParItem::Text(shaped, align), rest)) = items.split_first() {
if let Some((ParItem::Text(shaped, align, i), rest)) = items.split_first() {
// Compute the range we want to shape.
let Range { start: base, end: first_end } = par.ranges[first_idx];
let start = line.start;
@ -463,7 +494,7 @@ impl<'a> LineLayout<'a> {
if range.len() < shaped.text.len() {
if !range.is_empty() {
let reshaped = shaped.reshape(ctx, range);
first = Some(ParItem::Text(reshaped, *align));
first = Some(ParItem::Text(reshaped, *align, *i));
}
items = rest;
@ -511,11 +542,11 @@ impl<'a> LineLayout<'a> {
offset += amount;
return;
}
ParItem::Text(ref shaped, align) => {
ParItem::Text(ref shaped, align, _) => {
ruler = ruler.max(align);
Rc::new(shaped.build(ctx))
}
ParItem::Frame(ref frame, align) => {
ParItem::Frame(ref frame, align, _) => {
ruler = ruler.max(align);
frame.clone()
}
@ -528,7 +559,11 @@ impl<'a> LineLayout<'a> {
);
offset += frame.size.w;
output.push_frame(pos, frame);
match item.index() {
Some(idx) => output.push_indexed_frame(pos, idx, frame),
None => output.push_frame(pos, frame),
}
});
output

View File

@ -35,6 +35,7 @@ pub fn new() -> Scope {
std.def_func("strike", strike);
std.def_func("underline", underline);
std.def_func("overline", overline);
std.def_func("link", link);
// Layout.
std.def_func("page", page);

View File

@ -1,4 +1,4 @@
use crate::eval::{FontState, LineState};
use crate::eval::{Decoration, FontState, LineState};
use crate::layout::Paint;
use super::*;
@ -197,3 +197,18 @@ fn line_impl(
Ok(Value::Template(template))
}
/// `link`: Set a link.
pub fn link(_: &mut EvalContext, args: &mut Arguments) -> TypResult<Value> {
let url = args.expect::<Str>("url")?;
let mut body = args.eat().unwrap_or_else(|| {
let mut template = Template::new();
template.text(&url);
template
});
body.decorate(Decoration::Link(url.into()));
Ok(Value::Template(body))
}

BIN
tests/ref/text/links.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

12
tests/typ/text/links.typ Normal file
View File

@ -0,0 +1,12 @@
// Link without body.
#link("https://example.com/")
// Link with body.
#link("https://typst.app/")[Some text text text]
// With line break.
This link appears #link("https://google.com/")[in the middle of] a paragraph.
// Styled with underline and color.
#let link(url, body) = link(url, [#font(fill: rgb("283663")) #underline(body)])
You could also make the #link("https://html5zombo.com/")[link look way more typical.]

View File

@ -9,7 +9,7 @@ use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
use walkdir::WalkDir;
use typst::color::Color;
use typst::color::{Color, RgbaColor};
use typst::diag::Error;
use typst::eval::{State, Value};
use typst::geom::{self, Length, PathElement, Point, Sides, Size};
@ -428,6 +428,11 @@ fn draw(ctx: &Context, frames: &[Rc<Frame>], dpi: f32) -> sk::Pixmap {
Element::Image(id, size) => {
draw_image(&mut canvas, ts, ctx, id, size);
}
Element::Link(_, s) => {
let outline = Geometry::Rect(s);
let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40)));
draw_geometry(&mut canvas, ts, &outline, paint);
}
}
}