mirror of
https://github.com/typst/typst
synced 2025-06-28 00:03:17 +08:00
Intra-document links
This commit is contained in:
parent
0170913d54
commit
99cb655832
@ -634,9 +634,13 @@ where
|
|||||||
|
|
||||||
Ok(sides)
|
Ok(sides)
|
||||||
}
|
}
|
||||||
v => T::cast(v)
|
v => T::cast(v).map(Sides::splat).map_err(|msg| {
|
||||||
.map(Sides::splat)
|
with_alternative(
|
||||||
.map_err(|msg| with_alternative(msg, "dictionary")),
|
msg,
|
||||||
|
"dictionary with any of `left`, `top`, `right`, `bottom`,\
|
||||||
|
`x`, `y`, or `rest` as keys",
|
||||||
|
)
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,10 @@ use ttf_parser::{name_id, GlyphId, Tag};
|
|||||||
|
|
||||||
use super::subset::subset;
|
use super::subset::subset;
|
||||||
use crate::font::{find_name, FaceId, FontStore};
|
use crate::font::{find_name, FaceId, FontStore};
|
||||||
use crate::frame::{Element, Frame, Group, Text};
|
use crate::frame::{Destination, Element, Frame, Group, Text};
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Shape, Size, Stroke,
|
self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size,
|
||||||
Transform,
|
Stroke, Transform,
|
||||||
};
|
};
|
||||||
use crate::image::{Image, ImageId, ImageStore, RasterImage};
|
use crate::image::{Image, ImageId, ImageStore, RasterImage};
|
||||||
use crate::library::text::Lang;
|
use crate::library::text::Lang;
|
||||||
@ -304,13 +304,18 @@ impl<'a> PdfExporter<'a> {
|
|||||||
|
|
||||||
// The page objects (non-root nodes in the page tree).
|
// The page objects (non-root nodes in the page tree).
|
||||||
let mut page_refs = vec![];
|
let mut page_refs = vec![];
|
||||||
let mut languages = HashMap::new();
|
let mut page_heights = vec![];
|
||||||
for page in self.pages {
|
for page in &self.pages {
|
||||||
let page_id = self.alloc.bump();
|
let page_id = self.alloc.bump();
|
||||||
let content_id = self.alloc.bump();
|
|
||||||
page_refs.push(page_id);
|
page_refs.push(page_id);
|
||||||
|
page_heights.push(page.size.y.to_f32());
|
||||||
|
}
|
||||||
|
|
||||||
let mut page_writer = self.writer.page(page_id);
|
let mut languages = HashMap::new();
|
||||||
|
for (page, page_id) in self.pages.into_iter().zip(page_refs.iter()) {
|
||||||
|
let content_id = self.alloc.bump();
|
||||||
|
|
||||||
|
let mut page_writer = self.writer.page(*page_id);
|
||||||
page_writer.parent(page_tree_ref);
|
page_writer.parent(page_tree_ref);
|
||||||
|
|
||||||
let w = page.size.x.to_f32();
|
let w = page.size.x.to_f32();
|
||||||
@ -319,14 +324,25 @@ impl<'a> PdfExporter<'a> {
|
|||||||
page_writer.contents(content_id);
|
page_writer.contents(content_id);
|
||||||
|
|
||||||
let mut annotations = page_writer.annotations();
|
let mut annotations = page_writer.annotations();
|
||||||
for (url, rect) in page.links {
|
for (dest, rect) in page.links {
|
||||||
annotations
|
let mut link = annotations.push();
|
||||||
.push()
|
link.subtype(AnnotationType::Link).rect(rect);
|
||||||
.subtype(AnnotationType::Link)
|
match dest {
|
||||||
.rect(rect)
|
Destination::Url(uri) => {
|
||||||
.action()
|
link.action()
|
||||||
.action_type(ActionType::Uri)
|
.action_type(ActionType::Uri)
|
||||||
.uri(Str(url.as_bytes()));
|
.uri(Str(uri.as_str().as_bytes()));
|
||||||
|
}
|
||||||
|
Destination::Internal(page, point) => {
|
||||||
|
let page = page - 1;
|
||||||
|
let height = page_heights[page];
|
||||||
|
link.action()
|
||||||
|
.action_type(ActionType::GoTo)
|
||||||
|
.destination_direct()
|
||||||
|
.page(page_refs[page])
|
||||||
|
.xyz(point.x.to_f32(), height - point.y.to_f32(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
annotations.finish();
|
annotations.finish();
|
||||||
@ -403,7 +419,7 @@ struct PageExporter<'a> {
|
|||||||
languages: HashMap<Lang, usize>,
|
languages: HashMap<Lang, usize>,
|
||||||
bottom: f32,
|
bottom: f32,
|
||||||
content: Content,
|
content: Content,
|
||||||
links: Vec<(String, Rect)>,
|
links: Vec<(Destination, Rect)>,
|
||||||
state: State,
|
state: State,
|
||||||
saves: Vec<State>,
|
saves: Vec<State>,
|
||||||
}
|
}
|
||||||
@ -412,7 +428,7 @@ struct PageExporter<'a> {
|
|||||||
struct Page {
|
struct Page {
|
||||||
size: Size,
|
size: Size,
|
||||||
content: Content,
|
content: Content,
|
||||||
links: Vec<(String, Rect)>,
|
links: Vec<(Destination, Rect)>,
|
||||||
languages: HashMap<Lang, usize>,
|
languages: HashMap<Lang, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,7 +461,14 @@ impl<'a> PageExporter<'a> {
|
|||||||
fn export(mut self, frame: &Frame) -> Page {
|
fn export(mut self, frame: &Frame) -> Page {
|
||||||
// Make the coordinate system start at the top-left.
|
// Make the coordinate system start at the top-left.
|
||||||
self.bottom = frame.size.y.to_f32();
|
self.bottom = frame.size.y.to_f32();
|
||||||
self.content.transform([1.0, 0.0, 0.0, -1.0, 0.0, self.bottom]);
|
self.transform(Transform {
|
||||||
|
sx: Ratio::one(),
|
||||||
|
ky: Ratio::zero(),
|
||||||
|
kx: Ratio::zero(),
|
||||||
|
sy: Ratio::new(-1.0),
|
||||||
|
tx: Length::zero(),
|
||||||
|
ty: frame.size.y,
|
||||||
|
});
|
||||||
self.content.set_fill_color_space(ColorSpaceOperand::Named(SRGB));
|
self.content.set_fill_color_space(ColorSpaceOperand::Named(SRGB));
|
||||||
self.content.set_stroke_color_space(ColorSpaceOperand::Named(SRGB));
|
self.content.set_stroke_color_space(ColorSpaceOperand::Named(SRGB));
|
||||||
self.write_frame(frame);
|
self.write_frame(frame);
|
||||||
@ -466,7 +489,7 @@ impl<'a> PageExporter<'a> {
|
|||||||
Element::Text(ref text) => self.write_text(x, y, text),
|
Element::Text(ref text) => self.write_text(x, y, text),
|
||||||
Element::Shape(ref shape) => self.write_shape(x, y, shape),
|
Element::Shape(ref shape) => self.write_shape(x, y, shape),
|
||||||
Element::Image(id, size) => self.write_image(x, y, id, size),
|
Element::Image(id, size) => self.write_image(x, y, id, size),
|
||||||
Element::Link(ref url, size) => self.write_link(pos, url, size),
|
Element::Link(ref dest, size) => self.write_link(pos, dest, size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -627,7 +650,7 @@ impl<'a> PageExporter<'a> {
|
|||||||
self.content.restore_state();
|
self.content.restore_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_link(&mut self, pos: Point, url: &str, size: Size) {
|
fn write_link(&mut self, pos: Point, dest: &Destination, size: Size) {
|
||||||
let mut min_x = Length::inf();
|
let mut min_x = Length::inf();
|
||||||
let mut min_y = Length::inf();
|
let mut min_y = Length::inf();
|
||||||
let mut max_x = -Length::inf();
|
let mut max_x = -Length::inf();
|
||||||
@ -649,10 +672,11 @@ impl<'a> PageExporter<'a> {
|
|||||||
|
|
||||||
let x1 = min_x.to_f32();
|
let x1 = min_x.to_f32();
|
||||||
let x2 = max_x.to_f32();
|
let x2 = max_x.to_f32();
|
||||||
let y1 = self.bottom - max_y.to_f32();
|
let y1 = max_y.to_f32();
|
||||||
let y2 = self.bottom - min_y.to_f32();
|
let y2 = min_y.to_f32();
|
||||||
let rect = Rect::new(x1, y1, x2, y2);
|
let rect = Rect::new(x1, y1, x2, y2);
|
||||||
self.links.push((url.to_string(), rect));
|
|
||||||
|
self.links.push((dest.clone(), rect));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_state(&mut self) {
|
fn save_state(&mut self) {
|
||||||
|
28
src/frame.rs
28
src/frame.rs
@ -127,8 +127,8 @@ impl Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Link the whole frame to a resource.
|
/// Link the whole frame to a resource.
|
||||||
pub fn link(&mut self, url: EcoString) {
|
pub fn link(&mut self, dest: Destination) {
|
||||||
self.push(Point::zero(), Element::Link(url, self.size));
|
self.push(Point::zero(), Element::Link(dest, self.size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ pub enum Element {
|
|||||||
/// An image and its size.
|
/// An image and its size.
|
||||||
Image(ImageId, Size),
|
Image(ImageId, Size),
|
||||||
/// A link to an external resource and its trigger region.
|
/// A link to an external resource and its trigger region.
|
||||||
Link(EcoString, Size),
|
Link(Destination, Size),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Element {
|
impl Debug for Element {
|
||||||
@ -227,7 +227,7 @@ impl Debug for Element {
|
|||||||
Self::Text(text) => write!(f, "{text:?}"),
|
Self::Text(text) => write!(f, "{text:?}"),
|
||||||
Self::Shape(shape) => write!(f, "{shape:?}"),
|
Self::Shape(shape) => write!(f, "{shape:?}"),
|
||||||
Self::Image(image, _) => write!(f, "{image:?}"),
|
Self::Image(image, _) => write!(f, "{image:?}"),
|
||||||
Self::Link(url, _) => write!(f, "Link({url:?})"),
|
Self::Link(target, _) => write!(f, "Link({target:?})"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -308,3 +308,23 @@ pub struct Glyph {
|
|||||||
/// The first character of the glyph's cluster.
|
/// The first character of the glyph's cluster.
|
||||||
pub c: char,
|
pub c: char,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A link destination.
|
||||||
|
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum Destination {
|
||||||
|
/// A link to a point on a page.
|
||||||
|
Internal(usize, Point),
|
||||||
|
/// A link to a URL.
|
||||||
|
Url(EcoString),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Destination {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Internal(page, point) => {
|
||||||
|
write!(f, "Internal(Page {}, {:?})", page, point)
|
||||||
|
}
|
||||||
|
Self::Url(url) => write!(f, "Url({})", url),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,8 +5,8 @@ use crate::util::EcoString;
|
|||||||
/// Link text and other elements to an URL.
|
/// Link text and other elements to an URL.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
pub struct LinkNode {
|
pub struct LinkNode {
|
||||||
/// The url the link points to.
|
/// The destination the link points to.
|
||||||
pub url: EcoString,
|
pub dest: Destination,
|
||||||
/// How the link is represented.
|
/// How the link is represented.
|
||||||
pub body: Option<Content>,
|
pub body: Option<Content>,
|
||||||
}
|
}
|
||||||
@ -17,20 +17,36 @@ impl LinkNode {
|
|||||||
/// if `auto`.
|
/// if `auto`.
|
||||||
pub const FILL: Smart<Paint> = Smart::Auto;
|
pub const FILL: Smart<Paint> = Smart::Auto;
|
||||||
/// Whether to underline link.
|
/// Whether to underline link.
|
||||||
pub const UNDERLINE: bool = true;
|
pub const UNDERLINE: Smart<bool> = Smart::Auto;
|
||||||
|
|
||||||
fn construct(_: &mut Machine, args: &mut Args) -> TypResult<Content> {
|
fn construct(_: &mut Machine, args: &mut Args) -> TypResult<Content> {
|
||||||
Ok(Content::show(Self {
|
Ok(Content::show({
|
||||||
url: args.expect::<EcoString>("url")?,
|
let dest = args.expect::<Destination>("destination")?;
|
||||||
body: args.eat()?,
|
let body = match dest {
|
||||||
|
Destination::Url(_) => args.eat()?,
|
||||||
|
Destination::Internal(_, _) => Some(args.expect("body")?),
|
||||||
|
};
|
||||||
|
Self { dest, body }
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
castable! {
|
||||||
|
Destination,
|
||||||
|
Expected: "string or dictionary with `page`, `x`, and `y` keys",
|
||||||
|
Value::Str(string) => Self::Url(string),
|
||||||
|
Value::Dict(dict) => {
|
||||||
|
let page: i64 = dict.get(&EcoString::from_str("page"))?.clone().cast()?;
|
||||||
|
let x: RawLength = dict.get(&EcoString::from_str("x"))?.clone().cast()?;
|
||||||
|
let y: RawLength = dict.get(&EcoString::from_str("y"))?.clone().cast()?;
|
||||||
|
Self::Internal(page as usize, Point::new(x.length, y.length))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
impl Show for LinkNode {
|
impl Show for LinkNode {
|
||||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||||
Self {
|
Self {
|
||||||
url: self.url.clone(),
|
dest: self.dest.clone(),
|
||||||
body: self.body.as_ref().map(|body| body.unguard(sel)),
|
body: self.body.as_ref().map(|body| body.unguard(sel)),
|
||||||
}
|
}
|
||||||
.pack()
|
.pack()
|
||||||
@ -38,7 +54,14 @@ impl Show for LinkNode {
|
|||||||
|
|
||||||
fn encode(&self, _: StyleChain) -> Dict {
|
fn encode(&self, _: StyleChain) -> Dict {
|
||||||
dict! {
|
dict! {
|
||||||
"url" => Value::Str(self.url.clone()),
|
"url" => match &self.dest {
|
||||||
|
Destination::Url(url) => Value::Str(url.clone()),
|
||||||
|
Destination::Internal(page, point) => Value::Dict(dict!{
|
||||||
|
"page" => Value::Int(*page as i64),
|
||||||
|
"x" => Value::Length(point.x.into()),
|
||||||
|
"y" => Value::Length(point.y.into()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
"body" => match &self.body {
|
"body" => match &self.body {
|
||||||
Some(body) => Value::Content(body.clone()),
|
Some(body) => Value::Content(body.clone()),
|
||||||
None => Value::None,
|
None => Value::None,
|
||||||
@ -47,14 +70,16 @@ impl Show for LinkNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> {
|
fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> {
|
||||||
Ok(self.body.clone().unwrap_or_else(|| {
|
Ok(self.body.clone().unwrap_or_else(|| match &self.dest {
|
||||||
let url = &self.url;
|
Destination::Url(url) => {
|
||||||
let mut text = url.as_str();
|
let mut text = url.as_str();
|
||||||
for prefix in ["mailto:", "tel:"] {
|
for prefix in ["mailto:", "tel:"] {
|
||||||
text = text.trim_start_matches(prefix);
|
text = text.trim_start_matches(prefix);
|
||||||
|
}
|
||||||
|
let shorter = text.len() < url.len();
|
||||||
|
Content::Text(if shorter { text.into() } else { url.clone() })
|
||||||
}
|
}
|
||||||
let shorter = text.len() < url.len();
|
Destination::Internal(_, _) => panic!("missing body"),
|
||||||
Content::Text(if shorter { text.into() } else { url.clone() })
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,13 +90,19 @@ impl Show for LinkNode {
|
|||||||
mut realized: Content,
|
mut realized: Content,
|
||||||
) -> TypResult<Content> {
|
) -> TypResult<Content> {
|
||||||
let mut map = StyleMap::new();
|
let mut map = StyleMap::new();
|
||||||
map.set(TextNode::LINK, Some(self.url.clone()));
|
map.set(TextNode::LINK, Some(self.dest.clone()));
|
||||||
|
|
||||||
if let Smart::Custom(fill) = styles.get(Self::FILL) {
|
if let Smart::Custom(fill) = styles.get(Self::FILL) {
|
||||||
map.set(TextNode::FILL, fill);
|
map.set(TextNode::FILL, fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
if styles.get(Self::UNDERLINE) {
|
if match styles.get(Self::UNDERLINE) {
|
||||||
|
Smart::Auto => match &self.dest {
|
||||||
|
Destination::Url(_) => true,
|
||||||
|
Destination::Internal(_, _) => false,
|
||||||
|
},
|
||||||
|
Smart::Custom(underline) => underline,
|
||||||
|
} {
|
||||||
realized = realized.underlined();
|
realized = realized.underlined();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ impl TextNode {
|
|||||||
pub const SMALLCAPS: bool = false;
|
pub const SMALLCAPS: bool = false;
|
||||||
/// An URL the text should link to.
|
/// An URL the text should link to.
|
||||||
#[property(skip, referenced)]
|
#[property(skip, referenced)]
|
||||||
pub const LINK: Option<EcoString> = None;
|
pub const LINK: Option<Destination> = None;
|
||||||
/// Decorative lines.
|
/// Decorative lines.
|
||||||
#[property(skip, fold)]
|
#[property(skip, fold)]
|
||||||
pub const DECO: Decoration = vec![];
|
pub const DECO: Decoration = vec![];
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 40 KiB |
@ -33,3 +33,7 @@ My cool #move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink)))
|
|||||||
My cool rhino
|
My cool rhino
|
||||||
#move(dx: 10pt, image("/res/rhino.png", width: 1cm))
|
#move(dx: 10pt, image("/res/rhino.png", width: 1cm))
|
||||||
])
|
])
|
||||||
|
|
||||||
|
---
|
||||||
|
// Link to page one.
|
||||||
|
#link((page: 1, x: 10pt, y: 20pt))[Back to the start]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user