mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
parent
0806af4aec
commit
d546453880
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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(_, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user