mirror of
https://github.com/typst/typst
synced 2025-08-18 08:58:33 +08:00
Support linking from and into html.frame
This commit is contained in:
parent
47ac4165e1
commit
c58ef50a0a
@ -1,10 +1,13 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use comemo::{Tracked, TrackedMut};
|
use comemo::{Tracked, TrackedMut};
|
||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{bail, SourceResult};
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Content, StyleChain};
|
use typst_library::foundations::{Content, StyleChain};
|
||||||
use typst_library::introspection::{Introspector, IntrospectorBuilder, Locator};
|
use typst_library::introspection::{
|
||||||
|
Introspector, IntrospectorBuilder, Location, Locator,
|
||||||
|
};
|
||||||
use typst_library::layout::{Point, Position, Transform};
|
use typst_library::layout::{Point, Position, Transform};
|
||||||
use typst_library::model::DocumentInfo;
|
use typst_library::model::DocumentInfo;
|
||||||
use typst_library::routines::{Arenas, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, RealizationKind, Routines};
|
||||||
@ -84,43 +87,55 @@ fn html_document_impl(
|
|||||||
children.iter().copied(),
|
children.iter().copied(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut introspector = introspect_html(&output);
|
let mut link_targets = HashSet::new();
|
||||||
|
let mut introspector = introspect_html(&output, &mut link_targets);
|
||||||
let mut root = root_element(output, &info)?;
|
let mut root = root_element(output, &info)?;
|
||||||
crate::link::identify_link_targets(&mut root, &mut introspector);
|
crate::link::identify_link_targets(&mut root, &mut introspector, link_targets);
|
||||||
|
|
||||||
Ok(HtmlDocument { info, root, introspector })
|
Ok(HtmlDocument { info, root, introspector })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Introspects HTML nodes.
|
/// Introspects HTML nodes.
|
||||||
#[typst_macros::time(name = "introspect html")]
|
#[typst_macros::time(name = "introspect html")]
|
||||||
fn introspect_html(output: &[HtmlNode]) -> Introspector {
|
fn introspect_html(
|
||||||
|
output: &[HtmlNode],
|
||||||
|
link_targets: &mut HashSet<Location>,
|
||||||
|
) -> Introspector {
|
||||||
fn discover(
|
fn discover(
|
||||||
builder: &mut IntrospectorBuilder,
|
builder: &mut IntrospectorBuilder,
|
||||||
sink: &mut Vec<(Content, Position)>,
|
sink: &mut Vec<(Content, Position)>,
|
||||||
|
link_targets: &mut HashSet<Location>,
|
||||||
nodes: &[HtmlNode],
|
nodes: &[HtmlNode],
|
||||||
) {
|
) {
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
match node {
|
match node {
|
||||||
HtmlNode::Tag(tag) => builder.discover_in_tag(
|
HtmlNode::Tag(tag) => {
|
||||||
|
builder.discover_in_tag(
|
||||||
sink,
|
sink,
|
||||||
tag,
|
tag,
|
||||||
Position { page: NonZeroUsize::ONE, point: Point::zero() },
|
Position { page: NonZeroUsize::ONE, point: Point::zero() },
|
||||||
),
|
);
|
||||||
|
}
|
||||||
HtmlNode::Text(_, _) => {}
|
HtmlNode::Text(_, _) => {}
|
||||||
HtmlNode::Element(elem) => discover(builder, sink, &elem.children),
|
HtmlNode::Element(elem) => {
|
||||||
HtmlNode::Frame(frame) => builder.discover_in_frame(
|
discover(builder, sink, link_targets, &elem.children)
|
||||||
|
}
|
||||||
|
HtmlNode::Frame(frame) => {
|
||||||
|
builder.discover_in_frame(
|
||||||
sink,
|
sink,
|
||||||
&frame.inner,
|
&frame.inner,
|
||||||
NonZeroUsize::ONE,
|
NonZeroUsize::ONE,
|
||||||
Transform::identity(),
|
Transform::identity(),
|
||||||
),
|
);
|
||||||
|
crate::link::introspect_frame_links(&frame.inner, link_targets);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut elems = Vec::new();
|
let mut elems = Vec::new();
|
||||||
let mut builder = IntrospectorBuilder::new();
|
let mut builder = IntrospectorBuilder::new();
|
||||||
discover(&mut builder, &mut elems, output);
|
discover(&mut builder, &mut elems, link_targets, output);
|
||||||
builder.finalize(elems)
|
builder.finalize(elems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ use ecow::{EcoString, EcoVec};
|
|||||||
use typst_library::diag::{bail, HintedStrResult, StrResult};
|
use typst_library::diag::{bail, HintedStrResult, StrResult};
|
||||||
use typst_library::foundations::{cast, Dict, Repr, Str, StyleChain};
|
use typst_library::foundations::{cast, Dict, Repr, Str, StyleChain};
|
||||||
use typst_library::introspection::{Introspector, Tag};
|
use typst_library::introspection::{Introspector, Tag};
|
||||||
use typst_library::layout::{Abs, Frame};
|
use typst_library::layout::{Abs, Frame, Point};
|
||||||
use typst_library::model::DocumentInfo;
|
use typst_library::model::DocumentInfo;
|
||||||
use typst_library::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
@ -289,8 +289,10 @@ pub struct HtmlFrame {
|
|||||||
/// frame with em units to make text in and outside of the frame sized
|
/// frame with em units to make text in and outside of the frame sized
|
||||||
/// consistently.
|
/// consistently.
|
||||||
pub text_size: Abs,
|
pub text_size: Abs,
|
||||||
/// An ID to assign to the SVG.
|
/// An ID to assign to the SVG itself.
|
||||||
pub id: Option<EcoString>,
|
pub id: Option<EcoString>,
|
||||||
|
/// IDs to assign to destination jump points within the SVG.
|
||||||
|
pub link_points: Vec<(Point, EcoString)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HtmlFrame {
|
impl HtmlFrame {
|
||||||
@ -300,6 +302,7 @@ impl HtmlFrame {
|
|||||||
inner,
|
inner,
|
||||||
text_size: styles.resolve(TextElem::size),
|
text_size: styles.resolve(TextElem::size),
|
||||||
id: None,
|
id: None,
|
||||||
|
link_points: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ use std::fmt::Write;
|
|||||||
|
|
||||||
use typst_library::diag::{bail, At, SourceResult, StrResult};
|
use typst_library::diag::{bail, At, SourceResult, StrResult};
|
||||||
use typst_library::foundations::Repr;
|
use typst_library::foundations::Repr;
|
||||||
|
use typst_library::introspection::Introspector;
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -10,7 +11,7 @@ use crate::{
|
|||||||
|
|
||||||
/// Encodes an HTML document into a string.
|
/// Encodes an HTML document into a string.
|
||||||
pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
||||||
let mut w = Writer { pretty: true, ..Writer::default() };
|
let mut w = Writer::new(&document.introspector, true);
|
||||||
w.buf.push_str("<!DOCTYPE html>");
|
w.buf.push_str("<!DOCTYPE html>");
|
||||||
write_indent(&mut w);
|
write_indent(&mut w);
|
||||||
write_element(&mut w, &document.root)?;
|
write_element(&mut w, &document.root)?;
|
||||||
@ -20,16 +21,25 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
|||||||
Ok(w.buf)
|
Ok(w.buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
/// Encodes HTML.
|
||||||
struct Writer {
|
struct Writer<'a> {
|
||||||
/// The output buffer.
|
/// The output buffer.
|
||||||
buf: String,
|
buf: String,
|
||||||
/// The current indentation level
|
/// The current indentation level
|
||||||
level: usize,
|
level: usize,
|
||||||
|
/// The document's introspector.
|
||||||
|
introspector: &'a Introspector,
|
||||||
/// Whether pretty printing is enabled.
|
/// Whether pretty printing is enabled.
|
||||||
pretty: bool,
|
pretty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Writer<'a> {
|
||||||
|
/// Creates a new writer.
|
||||||
|
fn new(introspector: &'a Introspector, pretty: bool) -> Self {
|
||||||
|
Self { buf: String::new(), level: 0, introspector, pretty }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Writes a newline and indent, if pretty printing is enabled.
|
/// Writes a newline and indent, if pretty printing is enabled.
|
||||||
fn write_indent(w: &mut Writer) {
|
fn write_indent(w: &mut Writer) {
|
||||||
if w.pretty {
|
if w.pretty {
|
||||||
@ -306,7 +316,12 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
|
|||||||
|
|
||||||
/// Encode a laid out frame into the writer.
|
/// Encode a laid out frame into the writer.
|
||||||
fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
|
fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
|
||||||
let svg =
|
let svg = typst_svg::svg_html_frame(
|
||||||
typst_svg::svg_html_frame(&frame.inner, frame.text_size, frame.id.as_deref());
|
&frame.inner,
|
||||||
|
frame.text_size,
|
||||||
|
frame.id.as_deref(),
|
||||||
|
&frame.link_points,
|
||||||
|
w.introspector,
|
||||||
|
);
|
||||||
w.buf.push_str(&svg);
|
w.buf.push_str(&svg);
|
||||||
}
|
}
|
||||||
|
@ -4,27 +4,51 @@ use comemo::Track;
|
|||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use typst_library::foundations::{Label, NativeElement};
|
use typst_library::foundations::{Label, NativeElement};
|
||||||
use typst_library::introspection::{Introspector, Location, Tag};
|
use typst_library::introspection::{Introspector, Location, Tag};
|
||||||
|
use typst_library::layout::{Frame, FrameItem, Point};
|
||||||
use typst_library::model::{Destination, LinkElem};
|
use typst_library::model::{Destination, LinkElem};
|
||||||
use typst_utils::PicoStr;
|
use typst_utils::PicoStr;
|
||||||
|
|
||||||
use crate::{attr, tag, HtmlElement, HtmlNode};
|
use crate::{attr, tag, HtmlElement, HtmlNode};
|
||||||
|
|
||||||
|
/// Searches for links within a frame.
|
||||||
|
///
|
||||||
|
/// If all links are created via `LinkElem` in the future, this can be removed
|
||||||
|
/// in favor of the query in `identify_link_targets`. For the time being, some
|
||||||
|
/// links are created without existence of a `LinkElem`, so this is
|
||||||
|
/// unfortunately necessary.
|
||||||
|
pub fn introspect_frame_links(frame: &Frame, targets: &mut HashSet<Location>) {
|
||||||
|
for (_, item) in frame.items() {
|
||||||
|
match item {
|
||||||
|
FrameItem::Link(Destination::Location(loc), _) => {
|
||||||
|
targets.insert(*loc);
|
||||||
|
}
|
||||||
|
FrameItem::Group(group) => introspect_frame_links(&group.frame, targets),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Attaches IDs to nodes produced by link targets to make them linkable.
|
/// Attaches IDs to nodes produced by link targets to make them linkable.
|
||||||
///
|
///
|
||||||
/// May produce `<span>`s for link targets that turned into text nodes or no
|
/// May produce `<span>`s for link targets that turned into text nodes or no
|
||||||
/// nodes at all. See the [`LinkElem`] documentation for more details.
|
/// nodes at all. See the [`LinkElem`] documentation for more details.
|
||||||
pub fn identify_link_targets(root: &mut HtmlElement, introspector: &mut Introspector) {
|
pub fn identify_link_targets(
|
||||||
|
root: &mut HtmlElement,
|
||||||
|
introspector: &mut Introspector,
|
||||||
|
mut targets: HashSet<Location>,
|
||||||
|
) {
|
||||||
// Query for all links with an intra-doc (i.e. `Location`) destination to
|
// Query for all links with an intra-doc (i.e. `Location`) destination to
|
||||||
// know what needs IDs.
|
// know what needs IDs.
|
||||||
let targets = introspector
|
targets.extend(
|
||||||
|
introspector
|
||||||
.query(&LinkElem::ELEM.select())
|
.query(&LinkElem::ELEM.select())
|
||||||
.iter()
|
.iter()
|
||||||
.map(|elem| elem.to_packed::<LinkElem>().unwrap())
|
.map(|elem| elem.to_packed::<LinkElem>().unwrap())
|
||||||
.filter_map(|elem| match elem.dest.resolve(introspector.track()) {
|
.filter_map(|elem| match elem.dest.resolve(introspector.track()) {
|
||||||
Ok(Destination::Location(loc)) => Some(loc),
|
Ok(Destination::Location(loc)) => Some(loc),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
}),
|
||||||
.collect::<HashSet<_>>();
|
);
|
||||||
|
|
||||||
if targets.is_empty() {
|
if targets.is_empty() {
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
@ -103,6 +127,13 @@ fn traverse(
|
|||||||
work.drain(|label| {
|
work.drain(|label| {
|
||||||
frame.id.get_or_insert_with(|| identificator.identify(label)).clone()
|
frame.id.get_or_insert_with(|| identificator.identify(label)).clone()
|
||||||
});
|
});
|
||||||
|
traverse_frame(
|
||||||
|
work,
|
||||||
|
targets,
|
||||||
|
identificator,
|
||||||
|
&frame.inner,
|
||||||
|
&mut frame.link_points,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +141,33 @@ fn traverse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Traverses a frame embedded in HTML.
|
||||||
|
fn traverse_frame(
|
||||||
|
work: &mut Work,
|
||||||
|
targets: &HashSet<Location>,
|
||||||
|
identificator: &mut Identificator<'_>,
|
||||||
|
frame: &Frame,
|
||||||
|
link_points: &mut Vec<(Point, EcoString)>,
|
||||||
|
) {
|
||||||
|
for (_, item) in frame.items() {
|
||||||
|
match item {
|
||||||
|
FrameItem::Tag(Tag::Start(elem)) => {
|
||||||
|
let loc = elem.location().unwrap();
|
||||||
|
if targets.contains(&loc) {
|
||||||
|
let pos = identificator.introspector.position(loc).point;
|
||||||
|
let id = identificator.identify(elem.label());
|
||||||
|
work.ids.insert(loc, id.clone());
|
||||||
|
link_points.push((pos, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FrameItem::Group(group) => {
|
||||||
|
traverse_frame(work, targets, identificator, &group.frame, link_points);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Keeps track of the work to be done during ID generation.
|
/// Keeps track of the work to be done during ID generation.
|
||||||
struct Work {
|
struct Work {
|
||||||
/// The locations and labels of elements we need to assign an ID to right
|
/// The locations and labels of elements we need to assign an ID to right
|
||||||
|
@ -9,7 +9,7 @@ use typst_library::visualize::{
|
|||||||
|
|
||||||
use crate::SVGRenderer;
|
use crate::SVGRenderer;
|
||||||
|
|
||||||
impl SVGRenderer {
|
impl SVGRenderer<'_> {
|
||||||
/// Render an image element.
|
/// Render an image element.
|
||||||
pub(super) fn render_image(&mut self, image: &Image, size: &Axes<Abs>) {
|
pub(super) fn render_image(&mut self, image: &Image, size: &Axes<Abs>) {
|
||||||
let url = convert_image_to_base64_url(image);
|
let url = convert_image_to_base64_url(image);
|
||||||
|
@ -6,6 +6,8 @@ mod shape;
|
|||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
pub use image::{convert_image_scaling, convert_image_to_base64_url};
|
pub use image::{convert_image_scaling, convert_image_to_base64_url};
|
||||||
|
use typst_library::introspection::Introspector;
|
||||||
|
use typst_library::model::Destination;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Display, Formatter, Write};
|
use std::fmt::{self, Display, Formatter, Write};
|
||||||
@ -47,11 +49,20 @@ pub fn svg_frame(frame: &Frame) -> String {
|
|||||||
|
|
||||||
/// Export a frame into an SVG suitable for embedding into HTML.
|
/// Export a frame into an SVG suitable for embedding into HTML.
|
||||||
#[typst_macros::time(name = "svg html frame")]
|
#[typst_macros::time(name = "svg html frame")]
|
||||||
pub fn svg_html_frame(frame: &Frame, text_size: Abs, id: Option<&str>) -> String {
|
pub fn svg_html_frame(
|
||||||
let mut renderer = SVGRenderer::with_options(xmlwriter::Options {
|
frame: &Frame,
|
||||||
|
text_size: Abs,
|
||||||
|
id: Option<&str>,
|
||||||
|
link_points: &[(Point, EcoString)],
|
||||||
|
introspector: &Introspector,
|
||||||
|
) -> String {
|
||||||
|
let mut renderer = SVGRenderer::with_options(
|
||||||
|
xmlwriter::Options {
|
||||||
indent: xmlwriter::Indent::None,
|
indent: xmlwriter::Indent::None,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
},
|
||||||
|
Some(introspector),
|
||||||
|
);
|
||||||
renderer.write_header_with_custom_attrs(frame.size(), |xml| {
|
renderer.write_header_with_custom_attrs(frame.size(), |xml| {
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
xml.write_attribute("id", id);
|
xml.write_attribute("id", id);
|
||||||
@ -69,6 +80,11 @@ pub fn svg_html_frame(frame: &Frame, text_size: Abs, id: Option<&str>) -> String
|
|||||||
|
|
||||||
let state = State::new(frame.size(), Transform::identity());
|
let state = State::new(frame.size(), Transform::identity());
|
||||||
renderer.render_frame(state, Transform::identity(), frame);
|
renderer.render_frame(state, Transform::identity(), frame);
|
||||||
|
|
||||||
|
for (pos, id) in link_points {
|
||||||
|
renderer.render_link_point(*pos, id);
|
||||||
|
}
|
||||||
|
|
||||||
renderer.finalize()
|
renderer.finalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,9 +121,11 @@ pub fn svg_merged(document: &PagedDocument, padding: Abs) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Renders one or multiple frames to an SVG file.
|
/// Renders one or multiple frames to an SVG file.
|
||||||
struct SVGRenderer {
|
struct SVGRenderer<'a> {
|
||||||
/// The internal XML writer.
|
/// The internal XML writer.
|
||||||
xml: XmlWriter,
|
xml: XmlWriter,
|
||||||
|
/// The document's introspector, if we're writing an HTML frame.
|
||||||
|
introspector: Option<&'a Introspector>,
|
||||||
/// Prepared glyphs.
|
/// Prepared glyphs.
|
||||||
glyphs: Deduplicator<RenderedGlyph>,
|
glyphs: Deduplicator<RenderedGlyph>,
|
||||||
/// Clip paths are used to clip a group. A clip path is a path that defines
|
/// Clip paths are used to clip a group. A clip path is a path that defines
|
||||||
@ -182,16 +200,20 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SVGRenderer {
|
impl<'a> SVGRenderer<'a> {
|
||||||
/// Create a new SVG renderer with empty glyph and clip path.
|
/// Create a new SVG renderer with empty glyph and clip path.
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::with_options(Default::default())
|
Self::with_options(Default::default(), None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new SVG renderer with the given configuration.
|
/// Create a new SVG renderer with the given configuration.
|
||||||
fn with_options(options: xmlwriter::Options) -> Self {
|
fn with_options(
|
||||||
|
options: xmlwriter::Options,
|
||||||
|
introspector: Option<&'a Introspector>,
|
||||||
|
) -> Self {
|
||||||
SVGRenderer {
|
SVGRenderer {
|
||||||
xml: XmlWriter::new(options),
|
xml: XmlWriter::new(options),
|
||||||
|
introspector,
|
||||||
glyphs: Deduplicator::new('g'),
|
glyphs: Deduplicator::new('g'),
|
||||||
clip_paths: Deduplicator::new('c'),
|
clip_paths: Deduplicator::new('c'),
|
||||||
gradient_refs: Deduplicator::new('g'),
|
gradient_refs: Deduplicator::new('g'),
|
||||||
@ -251,8 +273,7 @@ impl SVGRenderer {
|
|||||||
|
|
||||||
for (pos, item) in frame.items() {
|
for (pos, item) in frame.items() {
|
||||||
// File size optimization.
|
// File size optimization.
|
||||||
// TODO: SVGs could contain links, couldn't they?
|
if matches!(item, FrameItem::Tag(_)) {
|
||||||
if matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +294,7 @@ impl SVGRenderer {
|
|||||||
self.render_shape(state.pre_translate(*pos), shape)
|
self.render_shape(state.pre_translate(*pos), shape)
|
||||||
}
|
}
|
||||||
FrameItem::Image(image, size, _) => self.render_image(image, size),
|
FrameItem::Image(image, size, _) => self.render_image(image, size),
|
||||||
FrameItem::Link(_, _) => unreachable!(),
|
FrameItem::Link(dest, size) => self.render_link(dest, *size),
|
||||||
FrameItem::Tag(_) => unreachable!(),
|
FrameItem::Tag(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -311,6 +332,53 @@ impl SVGRenderer {
|
|||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render a link element.
|
||||||
|
fn render_link(&mut self, dest: &Destination, size: Size) {
|
||||||
|
self.xml.start_element("a");
|
||||||
|
|
||||||
|
match dest {
|
||||||
|
Destination::Location(loc) => {
|
||||||
|
// TODO: Location links on the same page could also be supported
|
||||||
|
// outside of HTML.
|
||||||
|
if let Some(introspector) = self.introspector {
|
||||||
|
if let Some(id) = introspector.html_id(*loc) {
|
||||||
|
self.xml.write_attribute_fmt("href", format_args!("#{id}"));
|
||||||
|
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Destination::Position(_) => {
|
||||||
|
// TODO: Links on the same page could be supported.
|
||||||
|
}
|
||||||
|
Destination::Url(url) => {
|
||||||
|
self.xml.write_attribute("href", url.as_str());
|
||||||
|
self.xml.write_attribute("xlink:href", url.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.xml.start_element("rect");
|
||||||
|
self.xml
|
||||||
|
.write_attribute_fmt("width", format_args!("{}", size.x.to_pt()));
|
||||||
|
self.xml
|
||||||
|
.write_attribute_fmt("height", format_args!("{}", size.y.to_pt()));
|
||||||
|
self.xml.write_attribute("fill", "transparent");
|
||||||
|
self.xml.write_attribute("stroke", "none");
|
||||||
|
self.xml.end_element();
|
||||||
|
|
||||||
|
self.xml.end_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a linkable point that can be used to link into an HTML frame.
|
||||||
|
fn render_link_point(&mut self, pos: Point, id: &str) {
|
||||||
|
self.xml.start_element("g");
|
||||||
|
self.xml.write_attribute("id", id);
|
||||||
|
self.xml.write_attribute_fmt(
|
||||||
|
"transform",
|
||||||
|
format_args!("translate({} {})", pos.x.to_pt(), pos.y.to_pt()),
|
||||||
|
);
|
||||||
|
self.xml.end_element();
|
||||||
|
}
|
||||||
|
|
||||||
/// Finalize the SVG file. This must be called after all rendering is done.
|
/// Finalize the SVG file. This must be called after all rendering is done.
|
||||||
fn finalize(mut self) -> String {
|
fn finalize(mut self) -> String {
|
||||||
self.write_glyph_defs();
|
self.write_glyph_defs();
|
||||||
|
@ -15,7 +15,7 @@ use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder};
|
|||||||
/// Smaller values could be interesting for optimization.
|
/// Smaller values could be interesting for optimization.
|
||||||
const CONIC_SEGMENT: usize = 360;
|
const CONIC_SEGMENT: usize = 360;
|
||||||
|
|
||||||
impl SVGRenderer {
|
impl SVGRenderer<'_> {
|
||||||
/// Render a frame to a string.
|
/// Render a frame to a string.
|
||||||
pub(super) fn render_tiling_frame(
|
pub(super) fn render_tiling_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -8,7 +8,7 @@ use typst_library::visualize::{
|
|||||||
use crate::paint::ColorEncode;
|
use crate::paint::ColorEncode;
|
||||||
use crate::{SVGRenderer, State, SvgPathBuilder};
|
use crate::{SVGRenderer, State, SvgPathBuilder};
|
||||||
|
|
||||||
impl SVGRenderer {
|
impl SVGRenderer<'_> {
|
||||||
/// Render a shape element.
|
/// Render a shape element.
|
||||||
pub(super) fn render_shape(&mut self, state: State, shape: &Shape) {
|
pub(super) fn render_shape(&mut self, state: State, shape: &Shape) {
|
||||||
self.xml.start_element("path");
|
self.xml.start_element("path");
|
||||||
|
@ -13,7 +13,7 @@ use typst_utils::hash128;
|
|||||||
|
|
||||||
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
|
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
|
||||||
|
|
||||||
impl SVGRenderer {
|
impl SVGRenderer<'_> {
|
||||||
/// Render a text item. The text is rendered as a group of glyphs. We will
|
/// Render a text item. The text is rendered as a group of glyphs. We will
|
||||||
/// try to render the text as SVG first, then bitmap, then outline. If none
|
/// try to render the text as SVG first, then bitmap, then outline. If none
|
||||||
/// of them works, we will skip the text.
|
/// of them works, we will skip the text.
|
||||||
|
11
tests/ref/html/link-html-frame-ref.html
Normal file
11
tests/ref/html/link-html-frame-ref.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<svg class="typst-frame" style="overflow: visible; width: 4.5em; height: 3em;" viewBox="0 0 45 30" width="45pt" height="30pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 30 L 45 30 L 45 0 Z "/></g><g transform="translate(0 0)"><a href="#intro" xlink:href="#intro"><rect width="45" height="30" fill="transparent" stroke="none"/></a></g></g></svg>
|
||||||
|
<h2 id="intro">1 Introduction</h2>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
tests/ref/html/link-html-frame.html
Normal file
15
tests/ref/html/link-html-frame.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Frame 1</h2>
|
||||||
|
<svg class="typst-frame" style="overflow: visible; width: 20em; height: 50em;" viewBox="0 0 200 500" width="200pt" height="500pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"><g><g transform="translate(0 0)"><g class="typst-group"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 500 L 200 500 L 200 0 Z "/></g><g transform="translate(75 100)"><g class="typst-group"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="#39cccc" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g><g transform="translate(0 0)"><a href="#f1" xlink:href="#f1"><rect width="10" height="10" fill="transparent" stroke="none"/></a></g><g transform="translate(20 0)"><path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g><g transform="translate(20 0)"><a href="#text" xlink:href="#text"><rect width="10" height="10" fill="transparent" stroke="none"/></a></g><g transform="translate(40 0)"><path class="typst-shape" fill="#ffdc00" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g><g transform="translate(40 0)"><a href="#f2" xlink:href="#f2"><rect width="10" height="10" fill="transparent" stroke="none"/></a></g></g></g></g><g transform="translate(95 200)"><path class="typst-shape" fill="#39cccc" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g></g></g></g></g><g id="f1" transform="translate(95 200)"/></svg>
|
||||||
|
<h2 id="text">Text</h2>
|
||||||
|
<p><a href="#f1">Go to teal square</a></p>
|
||||||
|
<h2>Frame 2</h2>
|
||||||
|
<svg class="typst-frame" style="overflow: visible; width: 20em; height: 50em;" viewBox="0 0 200 500" width="200pt" height="500pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"><g><g transform="translate(0 0)"><g class="typst-group"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 500 L 200 500 L 200 0 Z "/></g><g transform="translate(95 100)"><path class="typst-shape" fill="#ffdc00" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g></g></g></g></g><g id="f2" transform="translate(95 100)"/></svg>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -134,6 +134,49 @@ See #metadata(none) <t8>
|
|||||||
#link(<b>)[B] // creates second empty span
|
#link(<b>)[B] // creates second empty span
|
||||||
#link(<c>)[C] // links to #a because the generated span is contained in it
|
#link(<c>)[C] // links to #a because the generated span is contained in it
|
||||||
|
|
||||||
|
--- link-html-frame html ---
|
||||||
|
= Frame 1
|
||||||
|
#html.frame(block(
|
||||||
|
stroke: 1pt,
|
||||||
|
width: 200pt,
|
||||||
|
height: 500pt,
|
||||||
|
)[
|
||||||
|
#place(center, dy: 100pt, stack(
|
||||||
|
dir: ltr,
|
||||||
|
spacing: 10pt,
|
||||||
|
link(<f1>, square(size: 10pt, fill: teal)),
|
||||||
|
link(<text>, square(size: 10pt, fill: black)),
|
||||||
|
link(<f2>, square(size: 10pt, fill: yellow)),
|
||||||
|
))
|
||||||
|
#place(center, dy: 200pt)[
|
||||||
|
#square(size: 10pt, fill: teal) <f1>
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
= Text <text>
|
||||||
|
#link(<f1>)[Go to teal square]
|
||||||
|
|
||||||
|
= Frame 2
|
||||||
|
#html.frame(block(
|
||||||
|
stroke: 1pt,
|
||||||
|
width: 200pt,
|
||||||
|
height: 500pt,
|
||||||
|
)[
|
||||||
|
#place(center, dy: 100pt)[
|
||||||
|
#square(size: 10pt, fill: yellow) <f2>
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
--- link-html-frame-ref html ---
|
||||||
|
// Test that reference links work in `html.frame`. Currently, references (and a
|
||||||
|
// few other elements) do not internally use `LinkElem`s, so they trigger a
|
||||||
|
// slightly different code path; see `typst-html/src/link.rs`. The text show
|
||||||
|
// rule is only there to keep the output small.
|
||||||
|
#set heading(numbering: "1")
|
||||||
|
#show "Section" + sym.space.nobreak + "1": rect()
|
||||||
|
#html.frame[@intro]
|
||||||
|
= Introduction <intro>
|
||||||
|
|
||||||
--- link-to-label-missing ---
|
--- link-to-label-missing ---
|
||||||
// Error: 2-20 label `<hey>` does not exist in the document
|
// Error: 2-20 label `<hey>` does not exist in the document
|
||||||
#link(<hey>)[Nope.]
|
#link(<hey>)[Nope.]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user