mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
parent
0806af4aec
commit
d546453880
@ -20,7 +20,7 @@ pub struct Template(Rc<Vec<TemplateNode>>);
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum TemplateNode {
|
enum TemplateNode {
|
||||||
/// A word space.
|
/// A word space.
|
||||||
Space,
|
Space(Vec<Decoration>),
|
||||||
/// A line break.
|
/// A line break.
|
||||||
Linebreak,
|
Linebreak,
|
||||||
/// A paragraph break.
|
/// A paragraph break.
|
||||||
@ -28,11 +28,11 @@ enum TemplateNode {
|
|||||||
/// A page break.
|
/// A page break.
|
||||||
Pagebreak(bool),
|
Pagebreak(bool),
|
||||||
/// Plain text.
|
/// Plain text.
|
||||||
Text(EcoString),
|
Text(EcoString, Vec<Decoration>),
|
||||||
/// Spacing.
|
/// Spacing.
|
||||||
Spacing(GenAxis, Linear),
|
Spacing(GenAxis, Linear),
|
||||||
/// An inline node builder.
|
/// An inline node builder.
|
||||||
Inline(Rc<dyn Fn(&State) -> LayoutNode>),
|
Inline(Rc<dyn Fn(&State) -> LayoutNode>, Vec<Decoration>),
|
||||||
/// An block node builder.
|
/// An block node builder.
|
||||||
Block(Rc<dyn Fn(&State) -> LayoutNode>),
|
Block(Rc<dyn Fn(&State) -> LayoutNode>),
|
||||||
/// Save the current state.
|
/// Save the current state.
|
||||||
@ -43,6 +43,13 @@ enum TemplateNode {
|
|||||||
Modify(Rc<dyn Fn(&mut State)>),
|
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 {
|
impl Template {
|
||||||
/// Create a new, empty template.
|
/// Create a new, empty template.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@ -55,7 +62,7 @@ impl Template {
|
|||||||
F: Fn(&State) -> T + 'static,
|
F: Fn(&State) -> T + 'static,
|
||||||
T: Into<LayoutNode>,
|
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]))
|
Self(Rc::new(vec![node]))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +78,7 @@ impl Template {
|
|||||||
|
|
||||||
/// Add a word space to the template.
|
/// Add a word space to the template.
|
||||||
pub fn space(&mut self) {
|
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.
|
/// Add a line break to the template.
|
||||||
@ -91,7 +98,7 @@ impl Template {
|
|||||||
|
|
||||||
/// Add text to the template.
|
/// Add text to the template.
|
||||||
pub fn text(&mut self, text: impl Into<EcoString>) {
|
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.
|
/// Add text, but in monospace.
|
||||||
@ -107,6 +114,19 @@ impl Template {
|
|||||||
self.make_mut().push(TemplateNode::Spacing(axis, spacing));
|
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.
|
/// Register a restorable snapshot.
|
||||||
pub fn save(&mut self) {
|
pub fn save(&mut self) {
|
||||||
self.make_mut().push(TemplateNode::Save);
|
self.make_mut().push(TemplateNode::Save);
|
||||||
@ -201,7 +221,7 @@ impl Add<Str> for Template {
|
|||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
fn add(mut self, rhs: Str) -> Self::Output {
|
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
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,7 +230,7 @@ impl Add<Template> for Str {
|
|||||||
type Output = Template;
|
type Output = Template;
|
||||||
|
|
||||||
fn add(self, mut rhs: Template) -> Self::Output {
|
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
|
rhs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -261,26 +281,26 @@ impl Builder {
|
|||||||
self.pagebreak(true, false);
|
self.pagebreak(true, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TemplateNode::Space => self.space(),
|
TemplateNode::Space(decos) => self.space(decos),
|
||||||
TemplateNode::Linebreak => self.linebreak(),
|
TemplateNode::Linebreak => self.linebreak(),
|
||||||
TemplateNode::Parbreak => self.parbreak(),
|
TemplateNode::Parbreak => self.parbreak(),
|
||||||
TemplateNode::Pagebreak(keep) => self.pagebreak(*keep, true),
|
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::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::Block(f) => self.block(f(&self.state)),
|
||||||
TemplateNode::Modify(f) => f(&mut self.state),
|
TemplateNode::Modify(f) => f(&mut self.state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a word space into the active paragraph.
|
/// Push a word space into the active paragraph.
|
||||||
fn space(&mut self) {
|
fn space(&mut self, decos: &[Decoration]) {
|
||||||
self.stack.par.push_soft(self.make_text_node(' '));
|
self.stack.par.push_soft(self.make_text_node(' ', decos.to_vec()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a forced line break.
|
/// Apply a forced line break.
|
||||||
fn linebreak(&mut self) {
|
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.
|
/// Apply a forced paragraph break.
|
||||||
@ -300,16 +320,14 @@ impl Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Push text into the active paragraph.
|
/// Push text into the active paragraph.
|
||||||
///
|
fn text(&mut self, text: impl Into<EcoString>, decos: &[Decoration]) {
|
||||||
/// The text is split into lines at newlines.
|
self.stack.par.push(self.make_text_node(text, decos.to_vec()));
|
||||||
fn text(&mut self, text: impl Into<EcoString>) {
|
|
||||||
self.stack.par.push(self.make_text_node(text));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push an inline node into the active paragraph.
|
/// 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;
|
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.
|
/// 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
|
/// Construct a text node with the given text and settings from the active
|
||||||
/// state.
|
/// 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(
|
ParChild::Text(
|
||||||
text.into(),
|
text.into(),
|
||||||
self.state.aligns.inline,
|
self.state.aligns.inline,
|
||||||
Rc::clone(&self.state.font),
|
Rc::clone(&self.state.font),
|
||||||
|
decos,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,11 +488,14 @@ impl ParBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push_inner(&mut self, child: ParChild) {
|
fn push_inner(&mut self, child: ParChild) {
|
||||||
if let ParChild::Text(curr_text, curr_align, curr_props) = &child {
|
if let ParChild::Text(curr_text, curr_align, curr_props, curr_decos) = &child {
|
||||||
if let Some(ParChild::Text(prev_text, prev_align, prev_props)) =
|
if let Some(ParChild::Text(prev_text, prev_align, prev_props, prev_decos)) =
|
||||||
self.children.last_mut()
|
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);
|
prev_text.push_str(&curr_text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,7 @@ fn walk_item(ctx: &mut EvalContext, label: EcoString, body: Template) {
|
|||||||
label.clone(),
|
label.clone(),
|
||||||
state.aligns.inline,
|
state.aligns.inline,
|
||||||
Rc::clone(&state.font),
|
Rc::clone(&state.font),
|
||||||
|
vec![],
|
||||||
)],
|
)],
|
||||||
};
|
};
|
||||||
StackNode {
|
StackNode {
|
||||||
|
@ -8,8 +8,8 @@ use std::rc::Rc;
|
|||||||
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
|
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
|
||||||
use miniz_oxide::deflate;
|
use miniz_oxide::deflate;
|
||||||
use pdf_writer::{
|
use pdf_writer::{
|
||||||
CidFontType, ColorSpace, Content, Filter, FontFlags, Name, PdfWriter, Rect, Ref, Str,
|
ActionType, AnnotationType, CidFontType, ColorSpace, Content, Filter, FontFlags,
|
||||||
SystemInfo, UnicodeCmap,
|
Name, PdfWriter, Rect, Ref, Str, SystemInfo, UnicodeCmap,
|
||||||
};
|
};
|
||||||
use ttf_parser::{name_id, GlyphId};
|
use ttf_parser::{name_id, GlyphId};
|
||||||
|
|
||||||
@ -59,6 +59,7 @@ impl<'a> PdfExporter<'a> {
|
|||||||
}
|
}
|
||||||
image_map.insert(id);
|
image_map.insert(id);
|
||||||
}
|
}
|
||||||
|
Element::Link(_, _) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,16 +117,34 @@ impl<'a> PdfExporter<'a> {
|
|||||||
for ((page_id, content_id), page) in
|
for ((page_id, content_id), page) in
|
||||||
self.refs.pages().zip(self.refs.contents()).zip(self.frames)
|
self.refs.pages().zip(self.refs.contents()).zip(self.frames)
|
||||||
{
|
{
|
||||||
self.writer
|
let w = page.size.w.to_pt() as f32;
|
||||||
.page(page_id)
|
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)
|
.parent(self.refs.page_tree)
|
||||||
.media_box(Rect::new(
|
.media_box(Rect::new(0.0, 0.0, w, h));
|
||||||
0.0,
|
|
||||||
0.0,
|
let mut annotations = page_writer.annotations();
|
||||||
page.size.w.to_pt() as f32,
|
for (pos, element) in page.elements() {
|
||||||
page.size.h.to_pt() as f32,
|
if let Element::Link(href, size) = element {
|
||||||
))
|
let x = pos.x.to_pt() as f32;
|
||||||
.contents(content_id);
|
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.x_object(Name(name.as_bytes()));
|
||||||
content.restore_state();
|
content.restore_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Element::Link(_, _) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,17 +16,17 @@ pub struct Frame {
|
|||||||
/// The baseline of the frame measured from the top.
|
/// The baseline of the frame measured from the top.
|
||||||
pub baseline: Length,
|
pub baseline: Length,
|
||||||
/// The elements composing this layout.
|
/// 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
|
/// A frame can contain two different kinds of children: a leaf element or a
|
||||||
/// nested frame.
|
/// nested frame.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
enum Child {
|
pub enum FrameChild {
|
||||||
/// A leaf node in the frame tree.
|
/// A leaf node in the frame tree.
|
||||||
Element(Element),
|
Element(Element),
|
||||||
/// An interior node.
|
/// An interior node with an optional index.
|
||||||
Frame(Rc<Frame>),
|
Frame(Option<usize>, Rc<Frame>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Frame {
|
impl Frame {
|
||||||
@ -38,17 +38,22 @@ impl Frame {
|
|||||||
|
|
||||||
/// Add an element at a position in the foreground.
|
/// Add an element at a position in the foreground.
|
||||||
pub fn push(&mut self, pos: Point, element: Element) {
|
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.
|
/// Add an element at a position in the background.
|
||||||
pub fn prepend(&mut self, pos: Point, element: Element) {
|
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.
|
/// Add a frame element.
|
||||||
pub fn push_frame(&mut self, pos: Point, subframe: Rc<Self>) {
|
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
|
/// 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> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let (cursor, offset, frame) = self.stack.last_mut()?;
|
let (cursor, offset, frame) = self.stack.last_mut()?;
|
||||||
match frame.children.get(*cursor) {
|
match frame.children.get(*cursor) {
|
||||||
Some((pos, Child::Frame(f))) => {
|
Some((pos, FrameChild::Frame(_, f))) => {
|
||||||
let new_offset = *offset + *pos;
|
let new_offset = *offset + *pos;
|
||||||
self.stack.push((0, new_offset, f.as_ref()));
|
self.stack.push((0, new_offset, f.as_ref()));
|
||||||
self.next()
|
self.next()
|
||||||
}
|
}
|
||||||
Some((pos, Child::Element(e))) => {
|
Some((pos, FrameChild::Element(e))) => {
|
||||||
*cursor += 1;
|
*cursor += 1;
|
||||||
Some((*offset + *pos, e))
|
Some((*offset + *pos, e))
|
||||||
}
|
}
|
||||||
@ -115,6 +120,8 @@ pub enum Element {
|
|||||||
Geometry(Geometry, Paint),
|
Geometry(Geometry, Paint),
|
||||||
/// A raster image.
|
/// A raster image.
|
||||||
Image(ImageId, Size),
|
Image(ImageId, Size),
|
||||||
|
/// A link to an external resource.
|
||||||
|
Link(String, Size),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A run of shaped text.
|
/// A run of shaped text.
|
||||||
|
@ -4,7 +4,7 @@ use unicode_bidi::{BidiInfo, Level};
|
|||||||
use xi_unicode::LineBreakIterator;
|
use xi_unicode::LineBreakIterator;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::eval::FontState;
|
use crate::eval::{Decoration, FontState};
|
||||||
use crate::util::{EcoString, RangeExt, SliceExt};
|
use crate::util::{EcoString, RangeExt, SliceExt};
|
||||||
|
|
||||||
type Range = std::ops::Range<usize>;
|
type Range = std::ops::Range<usize>;
|
||||||
@ -26,9 +26,9 @@ pub enum ParChild {
|
|||||||
/// Spacing between other nodes.
|
/// Spacing between other nodes.
|
||||||
Spacing(Linear),
|
Spacing(Linear),
|
||||||
/// A run of text and how to align it in its line.
|
/// 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 child node and how to align it in its line.
|
||||||
Any(LayoutNode, Align),
|
Any(LayoutNode, Align, Vec<Decoration>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout for ParNode {
|
impl Layout for ParNode {
|
||||||
@ -48,7 +48,7 @@ impl Layout for ParNode {
|
|||||||
let layouter = ParLayouter::new(self, ctx, regions, bidi);
|
let layouter = ParLayouter::new(self, ctx, regions, bidi);
|
||||||
|
|
||||||
// Find suitable linebreaks.
|
// 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> {
|
fn strings(&self) -> impl Iterator<Item = &str> {
|
||||||
self.children.iter().map(|child| match child {
|
self.children.iter().map(|child| match child {
|
||||||
ParChild::Spacing(_) => " ",
|
ParChild::Spacing(_) => " ",
|
||||||
ParChild::Text(ref piece, _, _) => piece,
|
ParChild::Text(ref piece, ..) => piece,
|
||||||
ParChild::Any(_, _) => "\u{FFFC}",
|
ParChild::Any(..) => "\u{FFFC}",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,25 +119,25 @@ impl<'a> ParLayouter<'a> {
|
|||||||
let mut ranges = vec![];
|
let mut ranges = vec![];
|
||||||
|
|
||||||
// Layout the children and collect them into items.
|
// 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 {
|
match *child {
|
||||||
ParChild::Spacing(amount) => {
|
ParChild::Spacing(amount) => {
|
||||||
let resolved = amount.resolve(regions.current.w);
|
let resolved = amount.resolve(regions.current.w);
|
||||||
items.push(ParItem::Spacing(resolved));
|
items.push(ParItem::Spacing(resolved));
|
||||||
ranges.push(range);
|
ranges.push(range);
|
||||||
}
|
}
|
||||||
ParChild::Text(_, align, ref state) => {
|
ParChild::Text(_, align, ref state, _) => {
|
||||||
// TODO: Also split by language and script.
|
// TODO: Also split by language and script.
|
||||||
for (subrange, dir) in split_runs(&bidi, range) {
|
for (subrange, dir) in split_runs(&bidi, range) {
|
||||||
let text = &bidi.text[subrange.clone()];
|
let text = &bidi.text[subrange.clone()];
|
||||||
let shaped = shape(ctx, text, dir, state);
|
let shaped = shape(ctx, text, dir, state);
|
||||||
items.push(ParItem::Text(shaped, align));
|
items.push(ParItem::Text(shaped, align, i));
|
||||||
ranges.push(subrange);
|
ranges.push(subrange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParChild::Any(ref node, align) => {
|
ParChild::Any(ref node, align, _) => {
|
||||||
let frame = node.layout(ctx, regions).remove(0);
|
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);
|
ranges.push(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,9 +156,10 @@ impl<'a> ParLayouter<'a> {
|
|||||||
fn layout(
|
fn layout(
|
||||||
self,
|
self,
|
||||||
ctx: &mut LayoutContext,
|
ctx: &mut LayoutContext,
|
||||||
|
children: &[ParChild],
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
) -> Vec<Constrained<Rc<Frame>>> {
|
) -> 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.
|
// The current line attempt.
|
||||||
// Invariant: Always fits into `stack.regions.current`.
|
// Invariant: Always fits into `stack.regions.current`.
|
||||||
@ -273,9 +274,9 @@ enum ParItem<'a> {
|
|||||||
/// Spacing between other items.
|
/// Spacing between other items.
|
||||||
Spacing(Length),
|
Spacing(Length),
|
||||||
/// A shaped text run with consistent direction.
|
/// A shaped text run with consistent direction.
|
||||||
Text(ShapedText<'a>, Align),
|
Text(ShapedText<'a>, Align, usize),
|
||||||
/// A layouted child node.
|
/// A layouted child node.
|
||||||
Frame(Rc<Frame>, Align),
|
Frame(Rc<Frame>, Align, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParItem<'_> {
|
impl ParItem<'_> {
|
||||||
@ -283,8 +284,8 @@ impl ParItem<'_> {
|
|||||||
pub fn size(&self) -> Size {
|
pub fn size(&self) -> Size {
|
||||||
match self {
|
match self {
|
||||||
Self::Spacing(amount) => Size::new(*amount, Length::zero()),
|
Self::Spacing(amount) => Size::new(*amount, Length::zero()),
|
||||||
Self::Text(shaped, _) => shaped.size,
|
Self::Text(shaped, ..) => shaped.size,
|
||||||
Self::Frame(frame, _) => frame.size,
|
Self::Frame(frame, ..) => frame.size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,8 +293,17 @@ impl ParItem<'_> {
|
|||||||
pub fn baseline(&self) -> Length {
|
pub fn baseline(&self) -> Length {
|
||||||
match self {
|
match self {
|
||||||
Self::Spacing(_) => Length::zero(),
|
Self::Spacing(_) => Length::zero(),
|
||||||
Self::Text(shaped, _) => shaped.baseline,
|
Self::Text(shaped, ..) => shaped.baseline,
|
||||||
Self::Frame(frame, _) => frame.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.
|
/// Stacks lines on top of each other.
|
||||||
struct LineStack<'a> {
|
struct LineStack<'a> {
|
||||||
line_spacing: Length,
|
line_spacing: Length,
|
||||||
|
children: &'a [ParChild],
|
||||||
full: Size,
|
full: Size,
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
size: Size,
|
size: Size,
|
||||||
@ -312,11 +323,12 @@ struct LineStack<'a> {
|
|||||||
|
|
||||||
impl<'a> LineStack<'a> {
|
impl<'a> LineStack<'a> {
|
||||||
/// Create an empty line stack.
|
/// 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 {
|
Self {
|
||||||
line_spacing,
|
line_spacing,
|
||||||
constraints: Constraints::new(regions.expand),
|
children,
|
||||||
full: regions.current,
|
full: regions.current,
|
||||||
|
constraints: Constraints::new(regions.expand),
|
||||||
regions,
|
regions,
|
||||||
size: Size::zero(),
|
size: Size::zero(),
|
||||||
lines: vec![],
|
lines: vec![],
|
||||||
@ -368,6 +380,25 @@ impl<'a> LineStack<'a> {
|
|||||||
output.merge_frame(pos, frame);
|
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.finished.push(output.constrain(self.constraints));
|
||||||
self.regions.next();
|
self.regions.next();
|
||||||
self.full = self.regions.current;
|
self.full = self.regions.current;
|
||||||
@ -426,7 +457,7 @@ impl<'a> LineLayout<'a> {
|
|||||||
|
|
||||||
// Reshape the last item if it's split in half.
|
// Reshape the last item if it's split in half.
|
||||||
let mut last = None;
|
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
|
// Compute the range we want to shape, trimming whitespace at the
|
||||||
// end of the line.
|
// end of the line.
|
||||||
let base = par.ranges[last_idx].start;
|
let base = par.ranges[last_idx].start;
|
||||||
@ -442,7 +473,7 @@ impl<'a> LineLayout<'a> {
|
|||||||
if !range.is_empty() || rest.is_empty() {
|
if !range.is_empty() || rest.is_empty() {
|
||||||
// Reshape that part.
|
// Reshape that part.
|
||||||
let reshaped = shaped.reshape(ctx, range);
|
let reshaped = shaped.reshape(ctx, range);
|
||||||
last = Some(ParItem::Text(reshaped, *align));
|
last = Some(ParItem::Text(reshaped, *align, *i));
|
||||||
}
|
}
|
||||||
|
|
||||||
items = rest;
|
items = rest;
|
||||||
@ -452,7 +483,7 @@ impl<'a> LineLayout<'a> {
|
|||||||
|
|
||||||
// Reshape the start item if it's split in half.
|
// Reshape the start item if it's split in half.
|
||||||
let mut first = None;
|
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.
|
// Compute the range we want to shape.
|
||||||
let Range { start: base, end: first_end } = par.ranges[first_idx];
|
let Range { start: base, end: first_end } = par.ranges[first_idx];
|
||||||
let start = line.start;
|
let start = line.start;
|
||||||
@ -463,7 +494,7 @@ impl<'a> LineLayout<'a> {
|
|||||||
if range.len() < shaped.text.len() {
|
if range.len() < shaped.text.len() {
|
||||||
if !range.is_empty() {
|
if !range.is_empty() {
|
||||||
let reshaped = shaped.reshape(ctx, range);
|
let reshaped = shaped.reshape(ctx, range);
|
||||||
first = Some(ParItem::Text(reshaped, *align));
|
first = Some(ParItem::Text(reshaped, *align, *i));
|
||||||
}
|
}
|
||||||
|
|
||||||
items = rest;
|
items = rest;
|
||||||
@ -511,11 +542,11 @@ impl<'a> LineLayout<'a> {
|
|||||||
offset += amount;
|
offset += amount;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ParItem::Text(ref shaped, align) => {
|
ParItem::Text(ref shaped, align, _) => {
|
||||||
ruler = ruler.max(align);
|
ruler = ruler.max(align);
|
||||||
Rc::new(shaped.build(ctx))
|
Rc::new(shaped.build(ctx))
|
||||||
}
|
}
|
||||||
ParItem::Frame(ref frame, align) => {
|
ParItem::Frame(ref frame, align, _) => {
|
||||||
ruler = ruler.max(align);
|
ruler = ruler.max(align);
|
||||||
frame.clone()
|
frame.clone()
|
||||||
}
|
}
|
||||||
@ -528,7 +559,11 @@ impl<'a> LineLayout<'a> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
offset += frame.size.w;
|
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
|
output
|
||||||
|
@ -35,6 +35,7 @@ pub fn new() -> Scope {
|
|||||||
std.def_func("strike", strike);
|
std.def_func("strike", strike);
|
||||||
std.def_func("underline", underline);
|
std.def_func("underline", underline);
|
||||||
std.def_func("overline", overline);
|
std.def_func("overline", overline);
|
||||||
|
std.def_func("link", link);
|
||||||
|
|
||||||
// Layout.
|
// Layout.
|
||||||
std.def_func("page", page);
|
std.def_func("page", page);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::eval::{FontState, LineState};
|
use crate::eval::{Decoration, FontState, LineState};
|
||||||
use crate::layout::Paint;
|
use crate::layout::Paint;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -197,3 +197,18 @@ fn line_impl(
|
|||||||
|
|
||||||
Ok(Value::Template(template))
|
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
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
12
tests/typ/text/links.typ
Normal 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.]
|
@ -9,7 +9,7 @@ use tiny_skia as sk;
|
|||||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use typst::color::Color;
|
use typst::color::{Color, RgbaColor};
|
||||||
use typst::diag::Error;
|
use typst::diag::Error;
|
||||||
use typst::eval::{State, Value};
|
use typst::eval::{State, Value};
|
||||||
use typst::geom::{self, Length, PathElement, Point, Sides, Size};
|
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) => {
|
Element::Image(id, size) => {
|
||||||
draw_image(&mut canvas, ts, ctx, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user