mirror of
https://github.com/typst/typst
synced 2025-08-13 22:57:56 +08:00
Compare commits
14 Commits
f44a44b173
...
b7b9fc7dfc
Author | SHA1 | Date | |
---|---|---|---|
|
b7b9fc7dfc | ||
|
5661c20580 | ||
|
7897e86bcc | ||
|
8e0e0f1a3b | ||
|
0a4b72f8f6 | ||
|
c58766440c | ||
|
ea5272bb2b | ||
|
cdbf60e883 | ||
|
476096c2db | ||
|
fd35268a88 | ||
|
3fba007c13 | ||
|
7dd3523044 | ||
|
0160bf1547 | ||
|
4d8a9863d7 |
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -413,7 +413,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "codex"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00"
|
||||
source = "git+https://github.com/typst/codex?rev=775d828#775d82873c3f74ce95ec2621f8541de1b48778a7"
|
||||
|
||||
[[package]]
|
||||
name = "color-print"
|
||||
@ -748,9 +748,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.0"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-rs-sys",
|
||||
@ -1469,9 +1469,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.4.2"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d"
|
||||
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
@ -3932,13 +3932,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.5.0"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88"
|
||||
checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
"indexmap 2.7.1",
|
||||
"memchr",
|
||||
@ -3947,9 +3946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.4.2"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05"
|
||||
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
|
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||
clap_complete = "4.2.1"
|
||||
clap_mangen = "0.2.10"
|
||||
codespan-reporting = "0.11"
|
||||
codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" }
|
||||
codex = { git = "https://github.com/typst/codex", rev = "775d828" }
|
||||
color-print = "0.3.6"
|
||||
comemo = "0.4"
|
||||
csv = "1"
|
||||
@ -143,7 +143,7 @@ xmlparser = "0.13.5"
|
||||
xmlwriter = "0.1.0"
|
||||
xz2 = { version = "0.1", features = ["static"] }
|
||||
yaml-front-matter = "0.1"
|
||||
zip = { version = "2.5", default-features = false, features = ["deflate"] }
|
||||
zip = { version = "4.3", default-features = false, features = ["deflate"] }
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
|
@ -123,7 +123,7 @@ impl Eval for ast::Escape<'_> {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok(Value::Symbol(Symbol::single(self.get())))
|
||||
Ok(Value::Symbol(Symbol::runtime_char(self.get())))
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ impl Eval for ast::Shorthand<'_> {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok(Value::Symbol(Symbol::single(self.get())))
|
||||
Ok(Value::Symbol(Symbol::runtime_char(self.get())))
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,7 +251,7 @@ impl Eval for ast::EnumItem<'_> {
|
||||
let body = self.body().eval(vm)?;
|
||||
let mut elem = EnumItem::new(body);
|
||||
if let Some(number) = self.number() {
|
||||
elem.number.set(Some(number));
|
||||
elem.number.set(Smart::Custom(number));
|
||||
}
|
||||
Ok(elem.pack())
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ impl Eval for ast::MathShorthand<'_> {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok(Value::Symbol(Symbol::single(self.get())))
|
||||
Ok(Value::Symbol(Symbol::runtime_char(self.get())))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
use std::collections::HashSet;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use comemo::{Tracked, TrackedMut};
|
||||
use typst_library::diag::{bail, SourceResult};
|
||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||
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::model::DocumentInfo;
|
||||
use typst_library::routines::{Arenas, RealizationKind, Routines};
|
||||
@ -83,42 +86,56 @@ fn html_document_impl(
|
||||
&mut locator,
|
||||
children.iter().copied(),
|
||||
)?;
|
||||
let introspector = introspect_html(&output);
|
||||
let root = root_element(output, &info)?;
|
||||
|
||||
let mut link_targets = HashSet::new();
|
||||
let mut introspector = introspect_html(&output, &mut link_targets);
|
||||
let mut root = root_element(output, &info)?;
|
||||
crate::link::identify_link_targets(&mut root, &mut introspector, link_targets);
|
||||
|
||||
Ok(HtmlDocument { info, root, introspector })
|
||||
}
|
||||
|
||||
/// Introspects HTML nodes.
|
||||
#[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(
|
||||
builder: &mut IntrospectorBuilder,
|
||||
sink: &mut Vec<(Content, Position)>,
|
||||
link_targets: &mut HashSet<Location>,
|
||||
nodes: &[HtmlNode],
|
||||
) {
|
||||
for node in nodes {
|
||||
match node {
|
||||
HtmlNode::Tag(tag) => builder.discover_in_tag(
|
||||
sink,
|
||||
tag,
|
||||
Position { page: NonZeroUsize::ONE, point: Point::zero() },
|
||||
),
|
||||
HtmlNode::Tag(tag) => {
|
||||
builder.discover_in_tag(
|
||||
sink,
|
||||
tag,
|
||||
Position { page: NonZeroUsize::ONE, point: Point::zero() },
|
||||
);
|
||||
}
|
||||
HtmlNode::Text(_, _) => {}
|
||||
HtmlNode::Element(elem) => discover(builder, sink, &elem.children),
|
||||
HtmlNode::Frame(frame) => builder.discover_in_frame(
|
||||
sink,
|
||||
&frame.inner,
|
||||
NonZeroUsize::ONE,
|
||||
Transform::identity(),
|
||||
),
|
||||
HtmlNode::Element(elem) => {
|
||||
discover(builder, sink, link_targets, &elem.children)
|
||||
}
|
||||
HtmlNode::Frame(frame) => {
|
||||
builder.discover_in_frame(
|
||||
sink,
|
||||
&frame.inner,
|
||||
NonZeroUsize::ONE,
|
||||
Transform::identity(),
|
||||
);
|
||||
crate::link::introspect_frame_links(&frame.inner, link_targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut elems = Vec::new();
|
||||
let mut builder = IntrospectorBuilder::new();
|
||||
discover(&mut builder, &mut elems, output);
|
||||
discover(&mut builder, &mut elems, link_targets, output);
|
||||
builder.finalize(elems)
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ use ecow::{EcoString, EcoVec};
|
||||
use typst_library::diag::{bail, HintedStrResult, StrResult};
|
||||
use typst_library::foundations::{cast, Dict, Repr, Str, StyleChain};
|
||||
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::text::TextElem;
|
||||
use typst_syntax::Span;
|
||||
@ -172,10 +172,20 @@ impl HtmlAttrs {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add an attribute.
|
||||
/// Adds an attribute.
|
||||
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
||||
self.0.push((attr, value.into()));
|
||||
}
|
||||
|
||||
/// Adds an attribute to the start of the list.
|
||||
pub fn push_front(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
||||
self.0.insert(0, (attr, value.into()));
|
||||
}
|
||||
|
||||
/// Finds an attribute value.
|
||||
pub fn get(&self, attr: HtmlAttr) -> Option<&EcoString> {
|
||||
self.0.iter().find(|&&(k, _)| k == attr).map(|(_, v)| v)
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
@ -279,11 +289,20 @@ pub struct HtmlFrame {
|
||||
/// frame with em units to make text in and outside of the frame sized
|
||||
/// consistently.
|
||||
pub text_size: Abs,
|
||||
/// An ID to assign to the SVG itself.
|
||||
pub id: Option<EcoString>,
|
||||
/// IDs to assign to destination jump points within the SVG.
|
||||
pub link_points: Vec<(Point, EcoString)>,
|
||||
}
|
||||
|
||||
impl HtmlFrame {
|
||||
/// Wraps a laid-out frame.
|
||||
pub fn new(inner: Frame, styles: StyleChain) -> Self {
|
||||
Self { inner, text_size: styles.resolve(TextElem::size) }
|
||||
Self {
|
||||
inner,
|
||||
text_size: styles.resolve(TextElem::size),
|
||||
id: None,
|
||||
link_points: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ use std::fmt::Write;
|
||||
|
||||
use typst_library::diag::{bail, At, SourceResult, StrResult};
|
||||
use typst_library::foundations::Repr;
|
||||
use typst_library::introspection::Introspector;
|
||||
use typst_syntax::Span;
|
||||
|
||||
use crate::{
|
||||
@ -10,7 +11,7 @@ use crate::{
|
||||
|
||||
/// Encodes an HTML document into a 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>");
|
||||
write_indent(&mut w);
|
||||
write_element(&mut w, &document.root)?;
|
||||
@ -20,16 +21,25 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
||||
Ok(w.buf)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Writer {
|
||||
/// Encodes HTML.
|
||||
struct Writer<'a> {
|
||||
/// The output buffer.
|
||||
buf: String,
|
||||
/// The current indentation level
|
||||
level: usize,
|
||||
/// The document's introspector.
|
||||
introspector: &'a Introspector,
|
||||
/// Whether pretty printing is enabled.
|
||||
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.
|
||||
fn write_indent(w: &mut Writer) {
|
||||
if w.pretty {
|
||||
@ -306,6 +316,12 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
|
||||
|
||||
/// Encode a laid out frame into the writer.
|
||||
fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
|
||||
let svg = typst_svg::svg_html_frame(&frame.inner, frame.text_size);
|
||||
let svg = typst_svg::svg_html_frame(
|
||||
&frame.inner,
|
||||
frame.text_size,
|
||||
frame.id.as_deref(),
|
||||
&frame.link_points,
|
||||
w.introspector,
|
||||
);
|
||||
w.buf.push_str(&svg);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ mod document;
|
||||
mod dom;
|
||||
mod encode;
|
||||
mod fragment;
|
||||
mod link;
|
||||
mod rules;
|
||||
mod tag;
|
||||
mod typed;
|
||||
@ -79,6 +80,19 @@ impl HtmlElem {
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds the attribute to the element if value is not `None`.
|
||||
pub fn with_optional_attr(
|
||||
self,
|
||||
attr: HtmlAttr,
|
||||
value: Option<impl Into<EcoString>>,
|
||||
) -> Self {
|
||||
if let Some(value) = value {
|
||||
self.with_attr(attr, value)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds CSS styles to an element.
|
||||
fn with_styles(self, properties: css::Properties) -> Self {
|
||||
if let Some(value) = properties.into_inline_styles() {
|
||||
|
290
crates/typst-html/src/link.rs
Normal file
290
crates/typst-html/src/link.rs
Normal file
@ -0,0 +1,290 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use comemo::Track;
|
||||
use ecow::{eco_format, EcoString};
|
||||
use typst_library::foundations::{Label, NativeElement};
|
||||
use typst_library::introspection::{Introspector, Location, Tag};
|
||||
use typst_library::layout::{Frame, FrameItem, Point};
|
||||
use typst_library::model::{Destination, LinkElem};
|
||||
use typst_utils::PicoStr;
|
||||
|
||||
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.
|
||||
///
|
||||
/// May produce `<span>`s for link targets that turned into text nodes or no
|
||||
/// nodes at all. See the [`LinkElem`] documentation for more details.
|
||||
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
|
||||
// know what needs IDs.
|
||||
targets.extend(
|
||||
introspector
|
||||
.query(&LinkElem::ELEM.select())
|
||||
.iter()
|
||||
.map(|elem| elem.to_packed::<LinkElem>().unwrap())
|
||||
.filter_map(|elem| match elem.dest.resolve(introspector.track()) {
|
||||
Ok(Destination::Location(loc)) => Some(loc),
|
||||
_ => None,
|
||||
}),
|
||||
);
|
||||
|
||||
if targets.is_empty() {
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign IDs to all link targets.
|
||||
let mut work = Work::new();
|
||||
traverse(
|
||||
&mut work,
|
||||
&targets,
|
||||
&mut Identificator::new(introspector),
|
||||
&mut root.children,
|
||||
);
|
||||
|
||||
// Add the mapping from locations to IDs to the introspector to make it
|
||||
// available to links in the next iteration.
|
||||
introspector.set_html_ids(work.ids);
|
||||
}
|
||||
|
||||
/// Traverses a list of nodes.
|
||||
fn traverse(
|
||||
work: &mut Work,
|
||||
targets: &HashSet<Location>,
|
||||
identificator: &mut Identificator<'_>,
|
||||
nodes: &mut Vec<HtmlNode>,
|
||||
) {
|
||||
let mut i = 0;
|
||||
while i < nodes.len() {
|
||||
let node = &mut nodes[i];
|
||||
match node {
|
||||
// When visiting a start tag, we check whether the element needs an
|
||||
// ID and if so, add it to the queue, so that its first child node
|
||||
// receives an ID.
|
||||
HtmlNode::Tag(Tag::Start(elem)) => {
|
||||
let loc = elem.location().unwrap();
|
||||
if targets.contains(&loc) {
|
||||
work.enqueue(loc, elem.label());
|
||||
}
|
||||
}
|
||||
|
||||
// When we reach an end tag, we check whether it closes an element
|
||||
// that is still in our queue. If so, that means the element
|
||||
// produced no nodes and we need to insert an empty span.
|
||||
HtmlNode::Tag(Tag::End(loc, _)) => {
|
||||
work.remove(*loc, |label| {
|
||||
let mut element = HtmlElement::new(tag::span);
|
||||
let id = identificator.assign(&mut element, label);
|
||||
nodes.insert(i + 1, HtmlNode::Element(element));
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
// When visiting an element and the queue is non-empty, we assign an
|
||||
// ID. Then, we traverse its children.
|
||||
HtmlNode::Element(element) => {
|
||||
work.drain(|label| identificator.assign(element, label));
|
||||
traverse(work, targets, identificator, &mut element.children);
|
||||
}
|
||||
|
||||
// When visiting text and the queue is non-empty, we generate a span
|
||||
// and assign an ID.
|
||||
HtmlNode::Text(..) => {
|
||||
work.drain(|label| {
|
||||
let mut element =
|
||||
HtmlElement::new(tag::span).with_children(vec![node.clone()]);
|
||||
let id = identificator.assign(&mut element, label);
|
||||
*node = HtmlNode::Element(element);
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
// When visiting a frame and the queue is non-empty, we assign an
|
||||
// ID to it (will be added to the resulting SVG element).
|
||||
HtmlNode::Frame(frame) => {
|
||||
work.drain(|label| {
|
||||
frame.id.get_or_insert_with(|| identificator.identify(label)).clone()
|
||||
});
|
||||
traverse_frame(
|
||||
work,
|
||||
targets,
|
||||
identificator,
|
||||
&frame.inner,
|
||||
&mut frame.link_points,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
struct Work {
|
||||
/// The locations and labels of elements we need to assign an ID to right
|
||||
/// now.
|
||||
queue: VecDeque<(Location, Option<Label>)>,
|
||||
/// The resulting mapping from element location's to HTML IDs.
|
||||
ids: HashMap<Location, EcoString>,
|
||||
}
|
||||
|
||||
impl Work {
|
||||
/// Sets up.
|
||||
fn new() -> Self {
|
||||
Self { queue: VecDeque::new(), ids: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Marks the element with the given location and label as in need of an
|
||||
/// ID. A subsequent call to `drain` will call `f`.
|
||||
fn enqueue(&mut self, loc: Location, label: Option<Label>) {
|
||||
self.queue.push_back((loc, label))
|
||||
}
|
||||
|
||||
/// If one or multiple elements are in need of an ID, calls `f` to generate
|
||||
/// an ID and apply it to the current node with `f`, and then establishes a
|
||||
/// mapping from the elements' locations to that ID.
|
||||
fn drain(&mut self, f: impl FnOnce(Option<Label>) -> EcoString) {
|
||||
if let Some(&(_, label)) = self.queue.front() {
|
||||
let id = f(label);
|
||||
for (loc, _) in self.queue.drain(..) {
|
||||
self.ids.insert(loc, id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to `drain`, but only for a specific given location.
|
||||
fn remove(&mut self, loc: Location, f: impl FnOnce(Option<Label>) -> EcoString) {
|
||||
if let Some(i) = self.queue.iter().position(|&(l, _)| l == loc) {
|
||||
let (_, label) = self.queue.remove(i).unwrap();
|
||||
let id = f(label);
|
||||
self.ids.insert(loc, id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates unique IDs for elements.
|
||||
struct Identificator<'a> {
|
||||
introspector: &'a Introspector,
|
||||
loc_counter: usize,
|
||||
label_counter: HashMap<Label, usize>,
|
||||
}
|
||||
|
||||
impl<'a> Identificator<'a> {
|
||||
/// Creates a new identificator.
|
||||
fn new(introspector: &'a Introspector) -> Self {
|
||||
Self {
|
||||
introspector,
|
||||
loc_counter: 0,
|
||||
label_counter: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Assigns an ID to an element or reuses an existing ID.
|
||||
fn assign(&mut self, element: &mut HtmlElement, label: Option<Label>) -> EcoString {
|
||||
element.attrs.get(attr::id).cloned().unwrap_or_else(|| {
|
||||
let id = self.identify(label);
|
||||
element.attrs.push_front(attr::id, id.clone());
|
||||
id
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates an ID, potentially based on a label.
|
||||
fn identify(&mut self, label: Option<Label>) -> EcoString {
|
||||
if let Some(label) = label {
|
||||
let resolved = label.resolve();
|
||||
let text = resolved.as_str();
|
||||
if can_use_label_as_id(text) {
|
||||
if self.introspector.label_count(label) == 1 {
|
||||
return text.into();
|
||||
}
|
||||
|
||||
let counter = self.label_counter.entry(label).or_insert(0);
|
||||
*counter += 1;
|
||||
return disambiguate(self.introspector, text, counter);
|
||||
}
|
||||
}
|
||||
|
||||
self.loc_counter += 1;
|
||||
disambiguate(self.introspector, "loc", &mut self.loc_counter)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the label is both a valid CSS identifier and a valid URL fragment
|
||||
/// for linking.
|
||||
///
|
||||
/// This is slightly more restrictive than HTML and CSS, but easier to
|
||||
/// understand and explain.
|
||||
fn can_use_label_as_id(label: &str) -> bool {
|
||||
!label.is_empty()
|
||||
&& label.chars().all(|c| c.is_alphanumeric() || matches!(c, '-' | '_'))
|
||||
&& !label.starts_with(|c: char| c.is_numeric() || c == '-')
|
||||
}
|
||||
|
||||
/// Disambiguates `text` with the suffix `-{counter}`, while ensuring that this
|
||||
/// does not result in a collision with an existing label.
|
||||
fn disambiguate(
|
||||
introspector: &Introspector,
|
||||
text: &str,
|
||||
counter: &mut usize,
|
||||
) -> EcoString {
|
||||
loop {
|
||||
let disambiguated = eco_format!("{text}-{counter}");
|
||||
if PicoStr::get(&disambiguated)
|
||||
.and_then(Label::new)
|
||||
.is_some_and(|label| introspector.label_count(label) > 0)
|
||||
{
|
||||
*counter += 1;
|
||||
} else {
|
||||
break disambiguated;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use ecow::{eco_format, EcoVec};
|
||||
use typst_library::diag::warning;
|
||||
use typst_library::diag::{warning, At};
|
||||
use typst_library::foundations::{
|
||||
Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
|
||||
};
|
||||
@ -59,19 +59,11 @@ pub fn register(rules: &mut NativeRuleMap) {
|
||||
rules.register::<FrameElem>(Paged, |elem, _, _| Ok(elem.body.clone()));
|
||||
}
|
||||
|
||||
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
|
||||
Ok(HtmlElem::new(tag::strong)
|
||||
.with_body(Some(elem.body.clone()))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
};
|
||||
const STRONG_RULE: ShowFn<StrongElem> =
|
||||
|elem, _, _| Ok(HtmlElem::new(tag::strong).with_body(Some(elem.body.clone())).pack());
|
||||
|
||||
const EMPH_RULE: ShowFn<EmphElem> = |elem, _, _| {
|
||||
Ok(HtmlElem::new(tag::em)
|
||||
.with_body(Some(elem.body.clone()))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
};
|
||||
const EMPH_RULE: ShowFn<EmphElem> =
|
||||
|elem, _, _| Ok(HtmlElem::new(tag::em).with_body(Some(elem.body.clone())).pack());
|
||||
|
||||
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
|
||||
Ok(HtmlElem::new(tag::ul)
|
||||
@ -86,8 +78,7 @@ const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
|
||||
.pack()
|
||||
.spanned(item.span())
|
||||
}))))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
|
||||
@ -103,7 +94,7 @@ const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
|
||||
|
||||
let body = Content::sequence(elem.children.iter().map(|item| {
|
||||
let mut li = HtmlElem::new(tag::li);
|
||||
if let Some(nr) = item.number.get(styles) {
|
||||
if let Smart::Custom(nr) = item.number.get(styles) {
|
||||
li = li.with_attr(attr::value, eco_format!("{nr}"));
|
||||
}
|
||||
// Text in wide enums shall always turn into paragraphs.
|
||||
@ -114,7 +105,7 @@ const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
|
||||
li.with_body(Some(body)).pack().spanned(item.span())
|
||||
}));
|
||||
|
||||
Ok(ol.with_body(Some(body)).pack().spanned(elem.span()))
|
||||
Ok(ol.with_body(Some(body)).pack())
|
||||
};
|
||||
|
||||
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
|
||||
@ -141,20 +132,32 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
|
||||
};
|
||||
|
||||
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
|
||||
let body = elem.body.clone();
|
||||
Ok(if let LinkTarget::Dest(Destination::Url(url)) = &elem.dest {
|
||||
HtmlElem::new(tag::a)
|
||||
.with_attr(attr::href, url.clone().into_inner())
|
||||
.with_body(Some(body))
|
||||
.pack()
|
||||
.spanned(elem.span())
|
||||
} else {
|
||||
engine.sink.warn(warning!(
|
||||
elem.span(),
|
||||
"non-URL links are not yet supported by HTML export"
|
||||
));
|
||||
body
|
||||
})
|
||||
let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
|
||||
|
||||
let href = match dest {
|
||||
Destination::Url(url) => Some(url.clone().into_inner()),
|
||||
Destination::Location(location) => {
|
||||
let id = engine
|
||||
.introspector
|
||||
.html_id(location)
|
||||
.cloned()
|
||||
.ok_or("failed to determine link anchor")
|
||||
.at(elem.span())?;
|
||||
Some(eco_format!("#{id}"))
|
||||
}
|
||||
Destination::Position(_) => {
|
||||
engine.sink.warn(warning!(
|
||||
elem.span(),
|
||||
"positional link was ignored during HTML export"
|
||||
));
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Ok(HtmlElem::new(tag::a)
|
||||
.with_optional_attr(attr::href, href)
|
||||
.with_body(Some(elem.body.clone()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
||||
@ -190,10 +193,9 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
||||
.with_attr(attr::role, "heading")
|
||||
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
|
||||
.pack()
|
||||
.spanned(span)
|
||||
} else {
|
||||
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
|
||||
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
|
||||
HtmlElem::new(t).with_body(Some(realized)).pack()
|
||||
})
|
||||
};
|
||||
|
||||
@ -212,17 +214,13 @@ const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
|
||||
// Ensure that the body is considered a paragraph.
|
||||
realized += ParbreakElem::shared().clone().spanned(span);
|
||||
|
||||
Ok(HtmlElem::new(tag::figure)
|
||||
.with_body(Some(realized))
|
||||
.pack()
|
||||
.spanned(span))
|
||||
Ok(HtmlElem::new(tag::figure).with_body(Some(realized)).pack())
|
||||
};
|
||||
|
||||
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
|
||||
Ok(HtmlElem::new(tag::figcaption)
|
||||
.with_body(Some(elem.realize(engine, styles)?))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
|
||||
@ -363,19 +361,11 @@ fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
|
||||
.spanned(cell.span())
|
||||
}
|
||||
|
||||
const SUB_RULE: ShowFn<SubElem> = |elem, _, _| {
|
||||
Ok(HtmlElem::new(tag::sub)
|
||||
.with_body(Some(elem.body.clone()))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
};
|
||||
const SUB_RULE: ShowFn<SubElem> =
|
||||
|elem, _, _| Ok(HtmlElem::new(tag::sub).with_body(Some(elem.body.clone())).pack());
|
||||
|
||||
const SUPER_RULE: ShowFn<SuperElem> = |elem, _, _| {
|
||||
Ok(HtmlElem::new(tag::sup)
|
||||
.with_body(Some(elem.body.clone()))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
};
|
||||
const SUPER_RULE: ShowFn<SuperElem> =
|
||||
|elem, _, _| Ok(HtmlElem::new(tag::sup).with_body(Some(elem.body.clone())).pack());
|
||||
|
||||
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| {
|
||||
// Note: In modern HTML, `<u>` is not the underline element, but
|
||||
@ -414,8 +404,7 @@ const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
|
||||
|
||||
Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code })
|
||||
.with_body(Some(Content::sequence(seq)))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
|
||||
|
@ -98,7 +98,7 @@ pub enum CompletionKind {
|
||||
/// A font family.
|
||||
Font,
|
||||
/// A symbol.
|
||||
Symbol(char),
|
||||
Symbol(EcoString),
|
||||
}
|
||||
|
||||
/// Complete in comments. Or rather, don't!
|
||||
@ -457,7 +457,7 @@ fn field_access_completions(
|
||||
for modifier in symbol.modifiers() {
|
||||
if let Ok(modified) = symbol.clone().modified((), modifier) {
|
||||
ctx.completions.push(Completion {
|
||||
kind: CompletionKind::Symbol(modified.get()),
|
||||
kind: CompletionKind::Symbol(modified.get().into()),
|
||||
label: modifier.into(),
|
||||
apply: None,
|
||||
detail: None,
|
||||
@ -1392,7 +1392,7 @@ impl<'a> CompletionContext<'a> {
|
||||
kind: kind.unwrap_or_else(|| match value {
|
||||
Value::Func(_) => CompletionKind::Func,
|
||||
Value::Type(_) => CompletionKind::Type,
|
||||
Value::Symbol(s) => CompletionKind::Symbol(s.get()),
|
||||
Value::Symbol(s) => CompletionKind::Symbol(s.get().into()),
|
||||
_ => CompletionKind::Constant,
|
||||
}),
|
||||
label,
|
||||
|
@ -719,6 +719,10 @@ fn glyphs_width(glyphs: &[ShapedGlyph]) -> Abs {
|
||||
struct ShapingContext<'a, 'v> {
|
||||
engine: &'a Engine<'v>,
|
||||
glyphs: Vec<ShapedGlyph>,
|
||||
/// Font families that have been used with unlimited coverage.
|
||||
///
|
||||
/// These font families are considered exhausted and will not be used again,
|
||||
/// even if they are declared again (e.g., during fallback after normal selection).
|
||||
used: Vec<Font>,
|
||||
styles: StyleChain<'a>,
|
||||
size: Abs,
|
||||
@ -777,7 +781,10 @@ fn shape_segment<'a>(
|
||||
return;
|
||||
};
|
||||
|
||||
ctx.used.push(font.clone());
|
||||
// This font has been exhausted and will not be used again.
|
||||
if covers.is_none() {
|
||||
ctx.used.push(font.clone());
|
||||
}
|
||||
|
||||
// Fill the buffer with our text.
|
||||
let mut buffer = UnicodeBuffer::new();
|
||||
|
@ -129,12 +129,22 @@ pub fn layout_symbol(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
assert!(
|
||||
elem.text.len() <= 4 && elem.text.chars().count() == 1,
|
||||
"TODO: layout multi-char symbol"
|
||||
);
|
||||
let elem_char = elem
|
||||
.text
|
||||
.chars()
|
||||
.next()
|
||||
.expect("TODO: should an empty symbol value forbidden?");
|
||||
|
||||
// Switch dotless char to normal when we have the dtls OpenType feature.
|
||||
// This should happen before the main styling pass.
|
||||
let dtls = style_dtls();
|
||||
let (unstyled_c, symbol_styles) = match try_dotless(elem.text) {
|
||||
let (unstyled_c, symbol_styles) = match try_dotless(elem_char) {
|
||||
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
|
||||
_ => (elem.text, styles),
|
||||
_ => (elem_char, styles),
|
||||
};
|
||||
|
||||
let variant = styles.get(EquationElem::variant);
|
||||
|
@ -20,8 +20,8 @@ use typst_library::math::EquationElem;
|
||||
use typst_library::model::{
|
||||
Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem,
|
||||
EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem,
|
||||
LinkElem, LinkTarget, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem,
|
||||
ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
|
||||
LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem,
|
||||
QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
|
||||
};
|
||||
use typst_library::pdf::EmbedElem;
|
||||
use typst_library::text::{
|
||||
@ -216,14 +216,8 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
|
||||
|
||||
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
|
||||
let body = elem.body.clone();
|
||||
Ok(match &elem.dest {
|
||||
LinkTarget::Dest(dest) => body.linked(dest.clone()),
|
||||
LinkTarget::Label(label) => {
|
||||
let elem = engine.introspector.query_label(*label).at(elem.span())?;
|
||||
let dest = Destination::Location(elem.location().unwrap());
|
||||
body.linked(dest)
|
||||
}
|
||||
})
|
||||
let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
|
||||
Ok(body.linked(dest))
|
||||
};
|
||||
|
||||
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
||||
@ -278,7 +272,7 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
||||
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
|
||||
};
|
||||
|
||||
Ok(block.pack().spanned(span))
|
||||
Ok(block.pack())
|
||||
};
|
||||
|
||||
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
|
||||
@ -332,8 +326,7 @@ const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
|
||||
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
|
||||
Ok(BlockElem::new()
|
||||
.with_body(Some(BlockBody::Content(elem.realize(engine, styles)?)))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
|
||||
@ -556,9 +549,7 @@ const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
|
||||
};
|
||||
|
||||
const TABLE_RULE: ShowFn<TableElem> = |elem, _, _| {
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table).pack())
|
||||
};
|
||||
|
||||
const TABLE_CELL_RULE: ShowFn<TableCell> = |elem, _, styles| {
|
||||
@ -709,27 +700,19 @@ const ALIGN_RULE: ShowFn<AlignElem> =
|
||||
|elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles)));
|
||||
|
||||
const PAD_RULE: ShowFn<PadElem> = |elem, _, _| {
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad).pack())
|
||||
};
|
||||
|
||||
const COLUMNS_RULE: ShowFn<ColumnsElem> = |elem, _, _| {
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns).pack())
|
||||
};
|
||||
|
||||
const STACK_RULE: ShowFn<StackElem> = |elem, _, _| {
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack).pack())
|
||||
};
|
||||
|
||||
const GRID_RULE: ShowFn<GridElem> = |elem, _, _| {
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid).pack())
|
||||
};
|
||||
|
||||
const GRID_CELL_RULE: ShowFn<GridCell> = |elem, _, styles| {
|
||||
@ -759,33 +742,23 @@ fn show_cell(
|
||||
}
|
||||
|
||||
const MOVE_RULE: ShowFn<MoveElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move).pack())
|
||||
};
|
||||
|
||||
const SCALE_RULE: ShowFn<ScaleElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale).pack())
|
||||
};
|
||||
|
||||
const ROTATE_RULE: ShowFn<RotateElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate).pack())
|
||||
};
|
||||
|
||||
const SKEW_RULE: ShowFn<SkewElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew).pack())
|
||||
};
|
||||
|
||||
const REPEAT_RULE: ShowFn<RepeatElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat).pack())
|
||||
};
|
||||
|
||||
const HIDE_RULE: ShowFn<HideElem> =
|
||||
@ -807,83 +780,66 @@ const LAYOUT_RULE: ShowFn<LayoutElem> = |elem, _, _| {
|
||||
crate::flow::layout_fragment(engine, &result, locator, styles, regions)
|
||||
},
|
||||
)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const IMAGE_RULE: ShowFn<ImageElem> = |elem, _, styles| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image)
|
||||
.with_width(elem.width.get(styles))
|
||||
.with_height(elem.height.get(styles))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const LINE_RULE: ShowFn<LineElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line).pack())
|
||||
};
|
||||
|
||||
const RECT_RULE: ShowFn<RectElem> = |elem, _, styles| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect)
|
||||
.with_width(elem.width.get(styles))
|
||||
.with_height(elem.height.get(styles))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const SQUARE_RULE: ShowFn<SquareElem> = |elem, _, styles| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square)
|
||||
.with_width(elem.width.get(styles))
|
||||
.with_height(elem.height.get(styles))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const ELLIPSE_RULE: ShowFn<EllipseElem> = |elem, _, styles| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse)
|
||||
.with_width(elem.width.get(styles))
|
||||
.with_height(elem.height.get(styles))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const CIRCLE_RULE: ShowFn<CircleElem> = |elem, _, styles| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle)
|
||||
.with_width(elem.width.get(styles))
|
||||
.with_height(elem.height.get(styles))
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
};
|
||||
|
||||
const POLYGON_RULE: ShowFn<PolygonElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon).pack())
|
||||
};
|
||||
|
||||
const CURVE_RULE: ShowFn<CurveElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve).pack())
|
||||
};
|
||||
|
||||
const PATH_RULE: ShowFn<PathElem> = |elem, _, _| {
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path).pack())
|
||||
};
|
||||
|
||||
const EQUATION_RULE: ShowFn<EquationElem> = |elem, _, styles| {
|
||||
if elem.block.get(styles) {
|
||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
.pack())
|
||||
} else {
|
||||
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline)
|
||||
.pack()
|
||||
.spanned(elem.span()))
|
||||
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline).pack())
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -37,13 +37,12 @@ pub use crate::__select_where as select_where;
|
||||
|
||||
/// A filter for selecting elements within the document.
|
||||
///
|
||||
/// You can construct a selector in the following ways:
|
||||
/// - you can use an element [function]
|
||||
/// - you can filter for an element function with
|
||||
/// [specific fields]($function.where)
|
||||
/// - you can use a [string]($str) or [regular expression]($regex)
|
||||
/// - you can use a [`{<label>}`]($label)
|
||||
/// - you can use a [`location`]
|
||||
/// To construct a selector you can:
|
||||
/// - use an element [function]
|
||||
/// - filter for an element function with [specific fields]($function.where)
|
||||
/// - use a [string]($str) or [regular expression]($regex)
|
||||
/// - use a [`{<label>}`]($label)
|
||||
/// - use a [`location`]
|
||||
/// - call the [`selector`] constructor to convert any of the above types into a
|
||||
/// selector value and use the methods below to refine it
|
||||
///
|
||||
@ -148,7 +147,9 @@ impl Selector {
|
||||
impl Selector {
|
||||
/// Turns a value into a selector. The following values are accepted:
|
||||
/// - An element function like a `heading` or `figure`.
|
||||
/// - A [string]($str) or [regular expression]($regex).
|
||||
/// - A `{<label>}`.
|
||||
/// - A [`location`].
|
||||
/// - A more complex selector like `{heading.where(level: 1)}`.
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::fmt::{self, Debug, Display, Formatter, Write};
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex::ModifierSet;
|
||||
@ -52,7 +52,7 @@ pub struct Symbol(Repr);
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
enum Repr {
|
||||
/// A native symbol that has no named variant.
|
||||
Single(char),
|
||||
Single(&'static str),
|
||||
/// A native symbol with multiple named variants.
|
||||
Complex(&'static [Variant<&'static str>]),
|
||||
/// A symbol with multiple named variants, where some modifiers may have
|
||||
@ -61,9 +61,9 @@ enum Repr {
|
||||
Modified(Arc<(List, ModifierSet<EcoString>)>),
|
||||
}
|
||||
|
||||
/// A symbol variant, consisting of a set of modifiers, a character, and an
|
||||
/// A symbol variant, consisting of a set of modifiers, the variant's value, and an
|
||||
/// optional deprecation message.
|
||||
type Variant<S> = (ModifierSet<S>, char, Option<S>);
|
||||
type Variant<S> = (ModifierSet<S>, S, Option<S>);
|
||||
|
||||
/// A collection of symbols.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
@ -73,9 +73,9 @@ enum List {
|
||||
}
|
||||
|
||||
impl Symbol {
|
||||
/// Create a new symbol from a single character.
|
||||
pub const fn single(c: char) -> Self {
|
||||
Self(Repr::Single(c))
|
||||
/// Create a new symbol from a single value.
|
||||
pub const fn single(value: &'static str) -> Self {
|
||||
Self(Repr::Single(value))
|
||||
}
|
||||
|
||||
/// Create a symbol with a static variant list.
|
||||
@ -85,6 +85,11 @@ impl Symbol {
|
||||
Self(Repr::Complex(list))
|
||||
}
|
||||
|
||||
/// Create a symbol from a runtime char.
|
||||
pub fn runtime_char(c: char) -> Self {
|
||||
Self::runtime(Box::new([(ModifierSet::default(), c.into(), None)]))
|
||||
}
|
||||
|
||||
/// Create a symbol with a runtime variant list.
|
||||
#[track_caller]
|
||||
pub fn runtime(list: Box<[Variant<EcoString>]>) -> Self {
|
||||
@ -92,15 +97,15 @@ impl Symbol {
|
||||
Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default()))))
|
||||
}
|
||||
|
||||
/// Get the symbol's character.
|
||||
pub fn get(&self) -> char {
|
||||
/// Get the symbol's value.
|
||||
pub fn get(&self) -> &str {
|
||||
match &self.0 {
|
||||
Repr::Single(c) => *c,
|
||||
Repr::Single(value) => value,
|
||||
Repr::Complex(_) => ModifierSet::<&'static str>::default()
|
||||
.best_match_in(self.variants().map(|(m, c, _)| (m, c)))
|
||||
.best_match_in(self.variants().map(|(m, v, _)| (m, v)))
|
||||
.unwrap(),
|
||||
Repr::Modified(arc) => {
|
||||
arc.1.best_match_in(self.variants().map(|(m, c, _)| (m, c))).unwrap()
|
||||
arc.1.best_match_in(self.variants().map(|(m, v, _)| (m, v))).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -108,27 +113,27 @@ impl Symbol {
|
||||
/// Try to get the function associated with the symbol, if any.
|
||||
pub fn func(&self) -> StrResult<Func> {
|
||||
match self.get() {
|
||||
'⌈' => Ok(crate::math::ceil::func()),
|
||||
'⌊' => Ok(crate::math::floor::func()),
|
||||
'–' => Ok(crate::math::accent::dash::func()),
|
||||
'⋅' | '\u{0307}' => Ok(crate::math::accent::dot::func()),
|
||||
'¨' => Ok(crate::math::accent::dot_double::func()),
|
||||
'\u{20db}' => Ok(crate::math::accent::dot_triple::func()),
|
||||
'\u{20dc}' => Ok(crate::math::accent::dot_quad::func()),
|
||||
'∼' => Ok(crate::math::accent::tilde::func()),
|
||||
'´' => Ok(crate::math::accent::acute::func()),
|
||||
'˝' => Ok(crate::math::accent::acute_double::func()),
|
||||
'˘' => Ok(crate::math::accent::breve::func()),
|
||||
'ˇ' => Ok(crate::math::accent::caron::func()),
|
||||
'^' => Ok(crate::math::accent::hat::func()),
|
||||
'`' => Ok(crate::math::accent::grave::func()),
|
||||
'¯' => Ok(crate::math::accent::macron::func()),
|
||||
'○' => Ok(crate::math::accent::circle::func()),
|
||||
'→' => Ok(crate::math::accent::arrow::func()),
|
||||
'←' => Ok(crate::math::accent::arrow_l::func()),
|
||||
'↔' => Ok(crate::math::accent::arrow_l_r::func()),
|
||||
'⇀' => Ok(crate::math::accent::harpoon::func()),
|
||||
'↼' => Ok(crate::math::accent::harpoon_lt::func()),
|
||||
"⌈" => Ok(crate::math::ceil::func()),
|
||||
"⌊" => Ok(crate::math::floor::func()),
|
||||
"–" => Ok(crate::math::accent::dash::func()),
|
||||
"⋅" | "\u{0307}" => Ok(crate::math::accent::dot::func()),
|
||||
"¨" => Ok(crate::math::accent::dot_double::func()),
|
||||
"\u{20db}" => Ok(crate::math::accent::dot_triple::func()),
|
||||
"\u{20dc}" => Ok(crate::math::accent::dot_quad::func()),
|
||||
"∼" => Ok(crate::math::accent::tilde::func()),
|
||||
"´" => Ok(crate::math::accent::acute::func()),
|
||||
"˝" => Ok(crate::math::accent::acute_double::func()),
|
||||
"˘" => Ok(crate::math::accent::breve::func()),
|
||||
"ˇ" => Ok(crate::math::accent::caron::func()),
|
||||
"^" => Ok(crate::math::accent::hat::func()),
|
||||
"`" => Ok(crate::math::accent::grave::func()),
|
||||
"¯" => Ok(crate::math::accent::macron::func()),
|
||||
"○" => Ok(crate::math::accent::circle::func()),
|
||||
"→" => Ok(crate::math::accent::arrow::func()),
|
||||
"←" => Ok(crate::math::accent::arrow_l::func()),
|
||||
"↔" => Ok(crate::math::accent::arrow_l_r::func()),
|
||||
"⇀" => Ok(crate::math::accent::harpoon::func()),
|
||||
"↼" => Ok(crate::math::accent::harpoon_lt::func()),
|
||||
_ => bail!("symbol {self} is not callable"),
|
||||
}
|
||||
}
|
||||
@ -163,7 +168,7 @@ impl Symbol {
|
||||
/// The characters that are covered by this symbol.
|
||||
pub fn variants(&self) -> impl Iterator<Item = Variant<&str>> {
|
||||
match &self.0 {
|
||||
Repr::Single(c) => Variants::Single(Some(*c).into_iter()),
|
||||
Repr::Single(value) => Variants::Single(std::iter::once(*value)),
|
||||
Repr::Complex(list) => Variants::Static(list.iter()),
|
||||
Repr::Modified(arc) => arc.0.variants(),
|
||||
}
|
||||
@ -279,14 +284,14 @@ impl Symbol {
|
||||
|
||||
impl Display for Symbol {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_char(self.get())
|
||||
f.write_str(self.get())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Repr {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Single(c) => Debug::fmt(c, f),
|
||||
Self::Single(value) => Debug::fmt(value, f),
|
||||
Self::Complex(list) => list.fmt(f),
|
||||
Self::Modified(lists) => lists.fmt(f),
|
||||
}
|
||||
@ -305,7 +310,7 @@ impl Debug for List {
|
||||
impl crate::foundations::Repr for Symbol {
|
||||
fn repr(&self) -> EcoString {
|
||||
match &self.0 {
|
||||
Repr::Single(c) => eco_format!("symbol(\"{}\")", *c),
|
||||
Repr::Single(value) => eco_format!("symbol({})", value.repr()),
|
||||
Repr::Complex(variants) => {
|
||||
eco_format!(
|
||||
"symbol{}",
|
||||
@ -341,15 +346,15 @@ fn repr_variants<'a>(
|
||||
// that contain all applied modifiers.
|
||||
applied_modifiers.iter().all(|am| modifiers.contains(am))
|
||||
})
|
||||
.map(|(modifiers, c, _)| {
|
||||
.map(|(modifiers, value, _)| {
|
||||
let trimmed_modifiers =
|
||||
modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m));
|
||||
if trimmed_modifiers.clone().all(|m| m.is_empty()) {
|
||||
eco_format!("\"{c}\"")
|
||||
value.repr()
|
||||
} else {
|
||||
let trimmed_modifiers =
|
||||
trimmed_modifiers.collect::<Vec<_>>().join(".");
|
||||
eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c)
|
||||
eco_format!("({}, {})", trimmed_modifiers.repr(), value.repr())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@ -362,7 +367,7 @@ impl Serialize for Symbol {
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_char(self.get())
|
||||
serializer.serialize_str(self.get())
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,11 +382,12 @@ impl List {
|
||||
}
|
||||
|
||||
/// A value that can be cast to a symbol.
|
||||
pub struct SymbolVariant(EcoString, char);
|
||||
pub struct SymbolVariant(EcoString, EcoString);
|
||||
|
||||
cast! {
|
||||
SymbolVariant,
|
||||
c: char => Self(EcoString::new(), c),
|
||||
c: char => Self(EcoString::new(), c.into()),
|
||||
s: EcoString => Self(EcoString::new(), s),
|
||||
array: Array => {
|
||||
let mut iter = array.into_iter();
|
||||
match (iter.next(), iter.next(), iter.next()) {
|
||||
@ -393,7 +399,7 @@ cast! {
|
||||
|
||||
/// Iterator over variants.
|
||||
enum Variants<'a> {
|
||||
Single(std::option::IntoIter<char>),
|
||||
Single(std::iter::Once<&'static str>),
|
||||
Static(std::slice::Iter<'static, Variant<&'static str>>),
|
||||
Runtime(std::slice::Iter<'a, Variant<EcoString>>),
|
||||
}
|
||||
@ -406,7 +412,7 @@ impl<'a> Iterator for Variants<'a> {
|
||||
Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)),
|
||||
Self::Static(list) => list.next().copied(),
|
||||
Self::Runtime(list) => {
|
||||
list.next().map(|(m, c, d)| (m.as_deref(), *c, d.as_deref()))
|
||||
list.next().map(|(m, s, d)| (m.as_deref(), s.as_str(), d.as_deref()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -415,21 +421,21 @@ impl<'a> Iterator for Variants<'a> {
|
||||
/// A single character.
|
||||
#[elem(Repr, PlainText)]
|
||||
pub struct SymbolElem {
|
||||
/// The symbol's character.
|
||||
/// The symbol's value.
|
||||
#[required]
|
||||
pub text: char, // This is called `text` for consistency with `TextElem`.
|
||||
pub text: EcoString, // This is called `text` for consistency with `TextElem`.
|
||||
}
|
||||
|
||||
impl SymbolElem {
|
||||
/// Create a new packed symbol element.
|
||||
pub fn packed(text: impl Into<char>) -> Content {
|
||||
pub fn packed(text: impl Into<EcoString>) -> Content {
|
||||
Self::new(text.into()).pack()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlainText for Packed<SymbolElem> {
|
||||
fn plain_text(&self, text: &mut EcoString) {
|
||||
text.push(self.text);
|
||||
text.push_str(&self.text);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ use std::hash::Hash;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use ecow::EcoVec;
|
||||
use ecow::{EcoString, EcoVec};
|
||||
use smallvec::SmallVec;
|
||||
use typst_utils::NonZeroExt;
|
||||
|
||||
@ -35,6 +35,11 @@ pub struct Introspector {
|
||||
/// Accelerates lookup of elements by label.
|
||||
labels: MultiMap<Label, usize>,
|
||||
|
||||
/// Maps from element locations to assigned HTML IDs. This used to support
|
||||
/// intra-doc links in HTML export. In paged export, is is simply left
|
||||
/// empty and [`Self::html_id`] is not used.
|
||||
html_ids: HashMap<Location, EcoString>,
|
||||
|
||||
/// Caches queries done on the introspector. This is important because
|
||||
/// even if all top-level queries are distinct, they often have shared
|
||||
/// subqueries. Example: Individual counter queries with `before` that
|
||||
@ -51,6 +56,17 @@ impl Introspector {
|
||||
self.elems.iter().map(|(c, _)| c)
|
||||
}
|
||||
|
||||
/// Checks how many times a label exists.
|
||||
pub fn label_count(&self, label: Label) -> usize {
|
||||
self.labels.get(&label).len()
|
||||
}
|
||||
|
||||
/// Enriches an existing introspector with HTML IDs, which were assigned
|
||||
/// to the DOM in a post-processing step.
|
||||
pub fn set_html_ids(&mut self, html_ids: HashMap<Location, EcoString>) {
|
||||
self.html_ids = html_ids;
|
||||
}
|
||||
|
||||
/// Retrieves the element with the given index.
|
||||
#[track_caller]
|
||||
fn get_by_idx(&self, idx: usize) -> &Content {
|
||||
@ -268,6 +284,11 @@ impl Introspector {
|
||||
self.page_supplements.get(page.get() - 1).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Retrieves the ID to link to for this location in HTML export.
|
||||
pub fn html_id(&self, location: Location) -> Option<&EcoString> {
|
||||
self.html_ids.get(&location)
|
||||
}
|
||||
|
||||
/// Try to find a location for an element with the given `key` hash
|
||||
/// that is closest after the `anchor`.
|
||||
///
|
||||
@ -343,6 +364,7 @@ pub struct IntrospectorBuilder {
|
||||
pub pages: usize,
|
||||
pub page_numberings: Vec<Option<Numbering>>,
|
||||
pub page_supplements: Vec<Content>,
|
||||
pub html_ids: HashMap<Location, EcoString>,
|
||||
seen: HashSet<Location>,
|
||||
insertions: MultiMap<Location, Vec<Pair>>,
|
||||
keys: MultiMap<u128, Location>,
|
||||
@ -426,6 +448,7 @@ impl IntrospectorBuilder {
|
||||
pages: self.pages,
|
||||
page_numberings: self.page_numberings,
|
||||
page_supplements: self.page_supplements,
|
||||
html_ids: self.html_ids,
|
||||
elems,
|
||||
keys: self.keys,
|
||||
locations: self.locations,
|
||||
|
@ -188,7 +188,7 @@ cast! {
|
||||
self => self.0.into_value(),
|
||||
v: char => Self::new(v),
|
||||
v: Content => match v.to_packed::<SymbolElem>() {
|
||||
Some(elem) => Self::new(elem.text),
|
||||
None => bail!("expected a symbol"),
|
||||
Some(elem) if elem.text.chars().count() == 1 => Self::new(elem.text.chars().next().unwrap()),
|
||||
_ => bail!("expected a single-codepoint symbol"),
|
||||
},
|
||||
}
|
||||
|
@ -274,7 +274,7 @@ cast! {
|
||||
Delimiter,
|
||||
self => self.0.into_value(),
|
||||
_: NoneValue => Self::none(),
|
||||
v: Symbol => Self::char(v.get())?,
|
||||
v: Symbol => Self::char(v.get().parse::<char>().map_err(|_| "expected a single-codepoint symbol")?)?,
|
||||
v: char => Self::char(v)?,
|
||||
}
|
||||
|
||||
|
@ -220,7 +220,7 @@ impl EnumElem {
|
||||
pub struct EnumItem {
|
||||
/// The item's number.
|
||||
#[positional]
|
||||
pub number: Option<u64>,
|
||||
pub number: Smart<u64>,
|
||||
|
||||
/// The item's body.
|
||||
#[required]
|
||||
|
@ -1,12 +1,13 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use comemo::Tracked;
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::foundations::{
|
||||
cast, elem, Content, Label, Packed, Repr, ShowSet, Smart, StyleChain, Styles,
|
||||
};
|
||||
use crate::introspection::Location;
|
||||
use crate::introspection::{Introspector, Locatable, Location};
|
||||
use crate::layout::Position;
|
||||
use crate::text::TextElem;
|
||||
|
||||
@ -27,15 +28,62 @@ use crate::text::TextElem;
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: Text that starts with `http://` or
|
||||
/// `https://` is automatically turned into a link.
|
||||
///
|
||||
/// # Hyphenation
|
||||
/// If you enable hyphenation or justification, by default, it will not apply to
|
||||
/// links to prevent unwanted hyphenation in URLs. You can opt out of this
|
||||
/// default via `{show link: set text(hyphenate: true)}`.
|
||||
///
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: Text that starts with `http://` or
|
||||
/// `https://` is automatically turned into a link.
|
||||
#[elem]
|
||||
/// # Links in HTML export
|
||||
/// In HTML export, a link to a [label] or [location] will be turned into a
|
||||
/// fragment link to a named anchor point. To support this, targets without an
|
||||
/// existing ID will automatically receive an ID in the DOM. How this works
|
||||
/// varies by which kind of HTML node(s) the link target turned into:
|
||||
///
|
||||
/// - If the link target turned into a single HTML element, that element will
|
||||
/// receive the ID. This is, for instance, typically the case when linking to
|
||||
/// a top-level heading (which turns into a single `<h2>` element).
|
||||
///
|
||||
/// - If the link target turned into a single text node, the node will be
|
||||
/// wrapped in a `<span>`, which will then receive the ID.
|
||||
///
|
||||
/// - If the link target turned into multiple nodes, the first node will receive
|
||||
/// the ID.
|
||||
///
|
||||
/// - If the link target turned into no nodes at all, an empty span will be
|
||||
/// generated to serve as a link target.
|
||||
///
|
||||
/// If you rely on a specific DOM structure, you should ensure that the link
|
||||
/// target turns into one or multiple elements, as the compiler makes no
|
||||
/// guarantees on the precise segmentation of text into text nodes.
|
||||
///
|
||||
/// If present, the automatic ID generation tries to reuse the link target's
|
||||
/// label to create a human-readable ID. A label can be reused if:
|
||||
///
|
||||
/// - All characters are alphabetic or numeric according to Unicode, or a
|
||||
/// hyphen, or an underscore.
|
||||
///
|
||||
/// - The label does not start with a digit or hyphen.
|
||||
///
|
||||
/// These rules ensure that the label is both a valid CSS identifier and a valid
|
||||
/// URL fragment for linking.
|
||||
///
|
||||
/// As IDs must be unique in the DOM, duplicate labels might need disambiguation
|
||||
/// when reusing them as IDs. The precise rules for this are as follows:
|
||||
///
|
||||
/// - If a label can be reused and is unique in the document, it will directly
|
||||
/// be used as the ID.
|
||||
///
|
||||
/// - If it's reusable, but not unique, a suffix consisting of a hyphen and an
|
||||
/// integer will be added. For instance, if the label `<mylabel>` exists
|
||||
/// twice, it would turn into `mylabel-1` and `mylabel-2`.
|
||||
///
|
||||
/// - Otherwise, a unique ID of the form `loc-` followed by an integer will be
|
||||
/// generated.
|
||||
#[elem(Locatable)]
|
||||
pub struct LinkElem {
|
||||
/// The destination the link points to.
|
||||
///
|
||||
@ -124,6 +172,19 @@ pub enum LinkTarget {
|
||||
Label(Label),
|
||||
}
|
||||
|
||||
impl LinkTarget {
|
||||
/// Resolves the destination.
|
||||
pub fn resolve(&self, introspector: Tracked<Introspector>) -> StrResult<Destination> {
|
||||
Ok(match self {
|
||||
LinkTarget::Dest(dest) => dest.clone(),
|
||||
LinkTarget::Label(label) => {
|
||||
let elem = introspector.query_label(*label)?;
|
||||
Destination::Location(elem.location().unwrap())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
LinkTarget,
|
||||
self => match self {
|
||||
|
@ -5,12 +5,13 @@ use crate::diag::{bail, At, Hint, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed,
|
||||
Repr, Smart, StyleChain, Synthesize,
|
||||
Repr, Smart, StyleChain, Synthesize, TargetElem,
|
||||
};
|
||||
use crate::introspection::{Counter, CounterKey, Locatable};
|
||||
use crate::math::EquationElem;
|
||||
use crate::model::{
|
||||
BibliographyElem, CiteElem, Destination, Figurable, FootnoteElem, Numbering,
|
||||
BibliographyElem, CiteElem, Destination, Figurable, FootnoteElem, LinkElem,
|
||||
LinkTarget, Numbering,
|
||||
};
|
||||
use crate::text::TextElem;
|
||||
|
||||
@ -346,7 +347,14 @@ fn realize_reference(
|
||||
content = supplement + TextElem::packed("\u{a0}") + content;
|
||||
}
|
||||
|
||||
Ok(content.linked(Destination::Location(loc)))
|
||||
Ok(if styles.get(TargetElem::target).is_html() {
|
||||
LinkElem::new(LinkTarget::Dest(Destination::Location(loc)), content).pack()
|
||||
} else {
|
||||
// TODO: We should probably also use `LinkElem` in the paged target, but
|
||||
// it's a bit breaking and it becomes hard to style links without
|
||||
// affecting references, so this change should be well-considered.
|
||||
content.linked(Destination::Location(loc))
|
||||
})
|
||||
}
|
||||
|
||||
/// Turn a reference into a citation.
|
||||
|
@ -39,7 +39,7 @@ impl From<codex::Module> for Scope {
|
||||
impl From<codex::Symbol> for Symbol {
|
||||
fn from(symbol: codex::Symbol) -> Self {
|
||||
match symbol {
|
||||
codex::Symbol::Single(c) => Symbol::single(c),
|
||||
codex::Symbol::Single(value) => Symbol::single(value),
|
||||
codex::Symbol::Multi(list) => Symbol::list(list),
|
||||
}
|
||||
}
|
||||
|
@ -301,9 +301,7 @@ fn visit_kind_rules<'a>(
|
||||
// textual elements via `TEXTUAL` grouping. However, in math, this is
|
||||
// not desirable, so we just do it on a per-element basis.
|
||||
if let Some(elem) = content.to_packed::<SymbolElem>() {
|
||||
if let Some(m) =
|
||||
find_regex_match_in_str(elem.text.encode_utf8(&mut [0; 4]), styles)
|
||||
{
|
||||
if let Some(m) = find_regex_match_in_str(elem.text.as_str(), styles) {
|
||||
visit_regex_match(s, &[(content, styles)], m)?;
|
||||
return Ok(true);
|
||||
}
|
||||
@ -324,7 +322,7 @@ fn visit_kind_rules<'a>(
|
||||
// Symbols in non-math content transparently convert to `TextElem` so we
|
||||
// don't have to handle them in non-math layout.
|
||||
if let Some(elem) = content.to_packed::<SymbolElem>() {
|
||||
let mut text = TextElem::packed(elem.text).spanned(elem.span());
|
||||
let mut text = TextElem::packed(elem.text.clone()).spanned(elem.span());
|
||||
if let Some(label) = elem.label() {
|
||||
text.set_label(label);
|
||||
}
|
||||
@ -374,7 +372,11 @@ fn visit_show_rules<'a>(
|
||||
}
|
||||
|
||||
// Apply a built-in show rule.
|
||||
ShowStep::Builtin(rule) => rule.apply(&output, s.engine, chained),
|
||||
ShowStep::Builtin(rule) => {
|
||||
let _scope = typst_timing::TimingScope::new(output.elem().name());
|
||||
rule.apply(&output, s.engine, chained)
|
||||
.map(|content| content.spanned(output.span()))
|
||||
}
|
||||
};
|
||||
|
||||
// Errors in show rules don't terminate compilation immediately. We just
|
||||
@ -1236,7 +1238,7 @@ fn visit_regex_match<'a>(
|
||||
let len = if let Some(elem) = content.to_packed::<TextElem>() {
|
||||
elem.text.len()
|
||||
} else if let Some(elem) = content.to_packed::<SymbolElem>() {
|
||||
elem.text.len_utf8()
|
||||
elem.text.len()
|
||||
} else {
|
||||
1 // The rest are Ascii, so just one byte.
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ use typst_library::visualize::{
|
||||
|
||||
use crate::SVGRenderer;
|
||||
|
||||
impl SVGRenderer {
|
||||
impl SVGRenderer<'_> {
|
||||
/// Render an image element.
|
||||
pub(super) fn render_image(&mut self, image: &Image, size: &Axes<Abs>) {
|
||||
let url = convert_image_to_base64_url(image);
|
||||
|
@ -6,6 +6,8 @@ mod shape;
|
||||
mod text;
|
||||
|
||||
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::fmt::{self, Display, Formatter, Write};
|
||||
@ -47,12 +49,24 @@ pub fn svg_frame(frame: &Frame) -> String {
|
||||
|
||||
/// Export a frame into an SVG suitable for embedding into HTML.
|
||||
#[typst_macros::time(name = "svg html frame")]
|
||||
pub fn svg_html_frame(frame: &Frame, text_size: Abs) -> String {
|
||||
let mut renderer = SVGRenderer::with_options(xmlwriter::Options {
|
||||
indent: xmlwriter::Indent::None,
|
||||
..Default::default()
|
||||
});
|
||||
pub fn svg_html_frame(
|
||||
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,
|
||||
..Default::default()
|
||||
},
|
||||
Some(introspector),
|
||||
);
|
||||
renderer.write_header_with_custom_attrs(frame.size(), |xml| {
|
||||
if let Some(id) = id {
|
||||
xml.write_attribute("id", id);
|
||||
}
|
||||
xml.write_attribute("class", "typst-frame");
|
||||
xml.write_attribute_fmt(
|
||||
"style",
|
||||
@ -66,6 +80,11 @@ pub fn svg_html_frame(frame: &Frame, text_size: Abs) -> String {
|
||||
|
||||
let state = State::new(frame.size(), Transform::identity());
|
||||
renderer.render_frame(state, Transform::identity(), frame);
|
||||
|
||||
for (pos, id) in link_points {
|
||||
renderer.render_link_point(*pos, id);
|
||||
}
|
||||
|
||||
renderer.finalize()
|
||||
}
|
||||
|
||||
@ -102,9 +121,11 @@ pub fn svg_merged(document: &PagedDocument, padding: Abs) -> String {
|
||||
}
|
||||
|
||||
/// Renders one or multiple frames to an SVG file.
|
||||
struct SVGRenderer {
|
||||
struct SVGRenderer<'a> {
|
||||
/// The internal XML writer.
|
||||
xml: XmlWriter,
|
||||
/// The document's introspector, if we're writing an HTML frame.
|
||||
introspector: Option<&'a Introspector>,
|
||||
/// Prepared glyphs.
|
||||
glyphs: Deduplicator<RenderedGlyph>,
|
||||
/// Clip paths are used to clip a group. A clip path is a path that defines
|
||||
@ -179,16 +200,20 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
impl SVGRenderer {
|
||||
impl<'a> SVGRenderer<'a> {
|
||||
/// Create a new SVG renderer with empty glyph and clip path.
|
||||
fn new() -> Self {
|
||||
Self::with_options(Default::default())
|
||||
Self::with_options(Default::default(), None)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
xml: XmlWriter::new(options),
|
||||
introspector,
|
||||
glyphs: Deduplicator::new('g'),
|
||||
clip_paths: Deduplicator::new('c'),
|
||||
gradient_refs: Deduplicator::new('g'),
|
||||
@ -248,8 +273,7 @@ impl SVGRenderer {
|
||||
|
||||
for (pos, item) in frame.items() {
|
||||
// File size optimization.
|
||||
// TODO: SVGs could contain links, couldn't they?
|
||||
if matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) {
|
||||
if matches!(item, FrameItem::Tag(_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -270,7 +294,7 @@ impl SVGRenderer {
|
||||
self.render_shape(state.pre_translate(*pos), shape)
|
||||
}
|
||||
FrameItem::Image(image, size, _) => self.render_image(image, size),
|
||||
FrameItem::Link(_, _) => unreachable!(),
|
||||
FrameItem::Link(dest, size) => self.render_link(dest, *size),
|
||||
FrameItem::Tag(_) => unreachable!(),
|
||||
};
|
||||
|
||||
@ -308,6 +332,53 @@ impl SVGRenderer {
|
||||
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.
|
||||
fn finalize(mut self) -> String {
|
||||
self.write_glyph_defs();
|
||||
|
@ -15,7 +15,7 @@ use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder};
|
||||
/// Smaller values could be interesting for optimization.
|
||||
const CONIC_SEGMENT: usize = 360;
|
||||
|
||||
impl SVGRenderer {
|
||||
impl SVGRenderer<'_> {
|
||||
/// Render a frame to a string.
|
||||
pub(super) fn render_tiling_frame(
|
||||
&mut self,
|
||||
|
@ -8,7 +8,7 @@ use typst_library::visualize::{
|
||||
use crate::paint::ColorEncode;
|
||||
use crate::{SVGRenderer, State, SvgPathBuilder};
|
||||
|
||||
impl SVGRenderer {
|
||||
impl SVGRenderer<'_> {
|
||||
/// Render a shape element.
|
||||
pub(super) fn render_shape(&mut self, state: State, shape: &Shape) {
|
||||
self.xml.start_element("path");
|
||||
|
@ -13,7 +13,7 @@ use typst_utils::hash128;
|
||||
|
||||
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
|
||||
/// try to render the text as SVG first, then bitmap, then outline. If none
|
||||
/// of them works, we will skip the text.
|
||||
|
@ -65,6 +65,23 @@ impl PicoStr {
|
||||
id
|
||||
}
|
||||
|
||||
/// Try to create a `PicoStr`, but don't intern it if it does not exist yet.
|
||||
///
|
||||
/// This is useful to try to compare against one or multiple `PicoStr`
|
||||
/// without interning needlessly.
|
||||
///
|
||||
/// Will always return `Some(_)` if the string can be represented inline.
|
||||
pub fn get(string: &str) -> Option<PicoStr> {
|
||||
// Try to use bitcode or exception representations.
|
||||
if let Ok(value) = PicoStr::try_constant(string) {
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
// Try to find an existing entry that we can reuse.
|
||||
let interner = INTERNER.read().unwrap();
|
||||
interner.seen.get(string).copied()
|
||||
}
|
||||
|
||||
/// Creates a compile-time constant `PicoStr`.
|
||||
///
|
||||
/// Should only be used in const contexts because it can panic.
|
||||
|
@ -718,9 +718,13 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
|
||||
}
|
||||
};
|
||||
|
||||
for (variant, c, deprecation) in symbol.variants() {
|
||||
for (variant, value, deprecation) in symbol.variants() {
|
||||
let value_char = value.parse::<char>().ok();
|
||||
|
||||
let shorthand = |list: &[(&'static str, char)]| {
|
||||
list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s)
|
||||
value_char.and_then(|c| {
|
||||
list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s)
|
||||
})
|
||||
};
|
||||
|
||||
let name = complete(variant);
|
||||
@ -729,9 +733,12 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
|
||||
name,
|
||||
markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST),
|
||||
math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST),
|
||||
math_class: typst_utils::default_math_class(c).map(math_class_name),
|
||||
codepoint: c as _,
|
||||
accent: typst::math::Accent::combine(c).is_some(),
|
||||
math_class: value_char.and_then(|c| {
|
||||
typst_utils::default_math_class(c).map(math_class_name)
|
||||
}),
|
||||
value: value.into(),
|
||||
accent: value_char
|
||||
.is_some_and(|c| typst::math::Accent::combine(c).is_some()),
|
||||
alternates: symbol
|
||||
.variants()
|
||||
.filter(|(other, _, _)| other != &variant)
|
||||
|
@ -159,7 +159,7 @@ pub struct SymbolsModel {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SymbolModel {
|
||||
pub name: EcoString,
|
||||
pub codepoint: u32,
|
||||
pub value: EcoString,
|
||||
pub accent: bool,
|
||||
pub alternates: Vec<EcoString>,
|
||||
pub markup_shorthand: Option<&'static str>,
|
||||
|
BIN
tests/ref/enum-item-number-optional.png
Normal file
BIN
tests/ref/enum-item-number-optional.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 474 B |
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>
|
10
tests/ref/html/link-html-here.html
Normal file
10
tests/ref/html/link-html-here.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<p id="loc-1"><a href="#loc-1">Go</a></p>
|
||||
</body>
|
||||
</html>
|
29
tests/ref/html/link-html-id-attach.html
Normal file
29
tests/ref/html/link-html-id-attach.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li><a href="#t1">Go</a></li>
|
||||
<li><a href="#t2">Go</a></li>
|
||||
<li><a href="#t3">Go</a></li>
|
||||
<li><a href="#t4">Go</a></li>
|
||||
<li><a href="#t5">Go</a></li>
|
||||
<li><a href="#t6">Go</a></li>
|
||||
<li><a href="#t7">Go</a></li>
|
||||
<li><a href="#t8">Go</a></li>
|
||||
</ul>
|
||||
<p id="t1">Hi</p>
|
||||
<p id="t2">Hi there</p>
|
||||
<p>See <span id="t4">it</span></p>
|
||||
<p>See <span id="t5">it</span> here</p>
|
||||
<p>See <span id="t6">a</span> <strong>b</strong></p>
|
||||
<p>See <strong id="t3">a <em>b</em></strong></p>
|
||||
<p>See</p>
|
||||
<span id="t7"></span>
|
||||
<p>See</p>
|
||||
<span id="t8"></span>
|
||||
</body>
|
||||
</html>
|
11
tests/ref/html/link-html-id-existing.html
Normal file
11
tests/ref/html/link-html-id-existing.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>
|
||||
<div><span id="this">This</span></div>
|
||||
<p><a href="#this">Go</a></p>
|
||||
</body>
|
||||
</html>
|
31
tests/ref/html/link-html-label-disambiguation.html
Normal file
31
tests/ref/html/link-html-label-disambiguation.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<h2 id="loc-1">A</h2>
|
||||
<h2 id="loc-3">B</h2>
|
||||
<h2 id="loc">C</h2>
|
||||
<h2 id="loc-2">D</h2>
|
||||
<h2 id="lib-1">E</h2>
|
||||
<h2 id="lib-3">F</h2>
|
||||
<h2 id="lib-2">G</h2>
|
||||
<h2 id="hi">H</h2>
|
||||
<h2 id="hi-2-1">I</h2>
|
||||
<h2 id="hi-2-2">J</h2>
|
||||
<ul>
|
||||
<li><a href="#loc-1">A</a></li>
|
||||
<li><a href="#loc-3">B</a></li>
|
||||
<li><a href="#loc">C</a></li>
|
||||
<li><a href="#loc-2">D</a></li>
|
||||
<li><a href="#lib-1">E</a></li>
|
||||
<li><a href="#lib-3">F</a></li>
|
||||
<li><a href="#lib-2">G</a></li>
|
||||
<li><a href="#hi">H</a></li>
|
||||
<li><a href="#hi-2-1">I</a></li>
|
||||
<li><a href="#hi-2-2">J</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
12
tests/ref/html/link-html-nested-empty.html
Normal file
12
tests/ref/html/link-html-nested-empty.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<span id="a"></span><span id="b"></span>
|
||||
<p>Hi</p>
|
||||
<p><a href="#a">A</a> <a href="#b">B</a> <a href="#a">C</a></p>
|
||||
</body>
|
||||
</html>
|
13
tests/ref/html/ref-basic.html
Normal file
13
tests/ref/html/ref-basic.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<h2 id="intro">1. Introduction</h2>
|
||||
<p>See <a href="#setup">Section 1.1</a>.</p>
|
||||
<h3 id="setup">1.1. Setup</h3>
|
||||
<p>As seen in <a href="#intro">Section 1</a>, we proceed.</p>
|
||||
</body>
|
||||
</html>
|
BIN
tests/ref/text-font-covers-repeat.png
Normal file
BIN
tests/ref/text-font-covers-repeat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 467 B |
BIN
tests/ref/text-font-covers-riffle.png
Normal file
BIN
tests/ref/text-font-covers-riffle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
@ -56,6 +56,10 @@ a + 0.
|
||||
enum.item(5)[Fifth]
|
||||
)
|
||||
|
||||
--- enum-item-number-optional ---
|
||||
#enum.item[First]
|
||||
#enum.item[Second]
|
||||
|
||||
--- enum-numbering-pattern ---
|
||||
// Test numbering pattern.
|
||||
#set enum(numbering: "(1.a.*)")
|
||||
@ -217,7 +221,7 @@ a + 0.
|
||||
|
||||
--- issue-2530-enum-item-panic ---
|
||||
// Enum item (pre-emptive)
|
||||
#enum.item(none)[Hello]
|
||||
#enum.item(auto)[Hello]
|
||||
#enum.item(17)[Hello]
|
||||
|
||||
--- issue-5503-enum-in-align ---
|
||||
|
@ -66,6 +66,117 @@ My cool #box(move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))))
|
||||
Text <hey>
|
||||
#link(<hey>)[Go to text.]
|
||||
|
||||
--- link-html-id-attach html ---
|
||||
// Tests how IDs and, if necessary, spans, are added to the DOM to support
|
||||
// links.
|
||||
|
||||
#for i in range(1, 9) {
|
||||
list.item(link(label("t" + str(i)), [Go]))
|
||||
}
|
||||
|
||||
// Text at start of paragraph
|
||||
Hi <t1>
|
||||
|
||||
// Text at start of paragraph + more text
|
||||
Hi <t2> there
|
||||
|
||||
// Text in the middle of paragraph
|
||||
See #[it <t4>]
|
||||
|
||||
// Text in the middle of paragraph + more text
|
||||
See #[it <t5>] here
|
||||
|
||||
// Text + more elements
|
||||
See #[a *b*] <t6>
|
||||
|
||||
// Element
|
||||
See *a _b_* <t3>
|
||||
|
||||
// Nothing
|
||||
See #[] <t7>
|
||||
|
||||
// Nothing 2
|
||||
See #metadata(none) <t8>
|
||||
|
||||
--- link-html-label-disambiguation html ---
|
||||
// Tests automatic ID generation for labelled elements.
|
||||
|
||||
#[= A] #label("%") // not reusable => loc-1
|
||||
= B <1> // not reusable => loc-3 (loc-2 exists)
|
||||
= C <loc> // reusable, unique => loc
|
||||
= D <loc-2> // reusable, unique => loc-2
|
||||
= E <lib> // reusable, not unique => lib-1
|
||||
= F <lib> // reusable, not unique => lib-3 (lib-2 exists)
|
||||
= G <lib-2> // reusable, unique => lib-2
|
||||
= H <hi> // reusable, unique => hi
|
||||
= I <hi-2> // reusable, not unique => hi-2-1
|
||||
= J <hi-2> // reusable, not unique => hi-2-2
|
||||
|
||||
#context for it in query(heading) {
|
||||
list.item(link(it.location(), it.body))
|
||||
}
|
||||
|
||||
--- link-html-id-existing html ---
|
||||
// Test that linking reuses the existing ID, if any.
|
||||
#html.div[
|
||||
#html.span(id: "this")[This] <other>
|
||||
]
|
||||
|
||||
#link(<other>)[Go]
|
||||
|
||||
--- link-html-here html ---
|
||||
#context link(here())[Go]
|
||||
|
||||
--- link-html-nested-empty html ---
|
||||
#[#metadata(none) <a> #metadata(none) <b> Hi] <c>
|
||||
|
||||
#link(<a>)[A] // creates empty span
|
||||
#link(<b>)[B] // creates second empty span
|
||||
#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 ---
|
||||
// Error: 2-20 label `<hey>` does not exist in the document
|
||||
#link(<hey>)[Nope.]
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Test references.
|
||||
|
||||
--- ref-basic ---
|
||||
--- ref-basic render html ---
|
||||
#set heading(numbering: "1.")
|
||||
|
||||
= Introduction <intro>
|
||||
|
@ -135,6 +135,34 @@ I
|
||||
|
||||
The number 123.
|
||||
|
||||
--- text-font-covers-repeat ---
|
||||
// Repeatedly use the same font.
|
||||
#set text(font: (
|
||||
(name: "Libertinus Serif", covers: regex("[0-9]")),
|
||||
"Libertinus Serif"
|
||||
))
|
||||
|
||||
The number 123.
|
||||
|
||||
--- text-font-covers-riffle ---
|
||||
// Repeatedly use two fonts alternately.
|
||||
#set text(font: (
|
||||
(name: "Noto Color Emoji", covers: regex("[🔗⛓💥]")),
|
||||
(name: "Twitter Color Emoji", covers: regex("[^🖥️]")),
|
||||
"Noto Color Emoji",
|
||||
))
|
||||
|
||||
🔗⛓💥🖥️🔑
|
||||
|
||||
// The above should be the same as:
|
||||
#{
|
||||
text(font: "Noto Color Emoji", "🔗⛓💥🖥️")
|
||||
text(font: "Twitter Color Emoji", "🔑")
|
||||
}
|
||||
|
||||
// but not:
|
||||
#text(font: "Twitter Color Emoji", "🔗⛓💥🖥️🔑")
|
||||
|
||||
--- text-font-covers-bad-1 ---
|
||||
// Error: 17-59 coverage regex may only use dot, letters, and character classes
|
||||
// Hint: 17-59 the regex is applied to each letter individually
|
||||
|
Loading…
x
Reference in New Issue
Block a user