Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions crates/typst-library/src/introspection/location.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,30 @@ impl Repr for Location {
}
}

/// Can be used to have a location as a key in an ordered set or map.
///
/// [`Location`] itself does not implement [`Ord`] because comparing hashes like
/// this has no semantic meaning. The potential for misuse (e.g. checking
/// whether locations have a particular relative ordering) is relatively high.
///
/// Still, it can be useful to have orderable locations for things like sets.
/// That's where this type comes in.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct LocationKey(u128);

impl LocationKey {
/// Create a location key from a location.
pub fn new(location: Location) -> Self {
Self(location.0)
}
}

impl From<Location> for LocationKey {
fn from(location: Location) -> Self {
Self::new(location)
}
}

/// Makes this element as locatable through the introspector.
pub trait Locatable {}

Expand Down
53 changes: 48 additions & 5 deletions crates/typst-realize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::borrow::Cow;
use std::cell::LazyCell;

use arrayvec::ArrayVec;
use bumpalo::collections::{String as BumpString, Vec as BumpVec};
use bumpalo::collections::{CollectIn, String as BumpString, Vec as BumpVec};
use comemo::Track;
use ecow::EcoString;
use typst_library::diag::{At, SourceResult, bail};
Expand All @@ -18,7 +18,7 @@ use typst_library::foundations::{
RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem, Styles,
SymbolElem, Synthesize, TargetElem, Transformation,
};
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
use typst_library::introspection::{Locatable, LocationKey, SplitLocator, Tag, TagElem};
use typst_library::layout::{
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
};
Expand All @@ -30,7 +30,7 @@ use typst_library::model::{
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_syntax::Span;
use typst_utils::{SliceExt, SmallBitSet};
use typst_utils::{ListSet, SliceExt, SmallBitSet};

/// Realize content into a flat list of well-known, styled items.
#[typst_macros::time(name = "realize")]
Expand Down Expand Up @@ -797,11 +797,54 @@ where
/// Finishes the currently innermost grouping.
fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
// The grouping we are interrupting.
let Grouping { start, rule, .. } = s.groupings.pop().unwrap();
let Grouping { mut start, rule, .. } = s.groupings.pop().unwrap();

// Trim trailing non-trigger elements.
let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, s));
let end = start + trimmed.len();
let mut end = start + trimmed.len();

// Tags that are opened within the grouping should have their closing tag
// included if it is at the end boundary. Similarly, tags that are closed
// within should have their opening tag included if it is at the start
// boundary. Finally, tags that are sandwiched between an opening tag with a
// matching closing tag should also be included.
if rule.tags
&& let within = ListSet::new(
trimmed
.iter()
.filter_map(|(c, _)| c.to_packed::<TagElem>())
.map(|elem| LocationKey::new(elem.tag.location()))
.collect_in::<BumpVec<_>>(&s.arenas.bump),
)
&& !within.is_empty()
{
// Include all tags at the start that are closed within.
for (k, (c, _)) in s.sink[..start].iter().enumerate().rev() {
let Some(elem) = c.to_packed::<TagElem>() else { break };
if within.contains(&elem.tag.location().into()) {
start = k;
}
}

// The trailing part of the sink can contain a mix of inner elements and
// tags. If there is a closing tag with a matching start tag, but there
// is an inner element in between, that's in principle a situation with
// overlapping tags. However, if the inner element would immediately be
// destructed anyways, there isn't really a problem. So we try to
// anticipate that and destruct it eagerly.
if std::ptr::eq(rule, &PAR) {
for _ in s.sink.extract_if(end.., |(c, _)| c.is::<SpaceElem>()) {}
}

// Include all tags at the end that are opened within.
for (k, (c, _)) in s.sink.iter().enumerate().skip(end) {
let Some(elem) = c.to_packed::<TagElem>() else { break };
if within.contains(&elem.tag.location().into()) {
end = k;
}
}
}

let tail = s.store_slice(&s.sink[end..]);
s.sink.truncate(end);

Expand Down
2 changes: 2 additions & 0 deletions crates/typst-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod bitset;
mod deferred;
mod duration;
mod hash;
mod listset;
mod pico;
mod round;
mod scalar;
Expand All @@ -16,6 +17,7 @@ pub use self::bitset::{BitSet, SmallBitSet};
pub use self::deferred::Deferred;
pub use self::duration::format_duration;
pub use self::hash::{HashLock, LazyHash, ManuallyHash};
pub use self::listset::ListSet;
pub use self::pico::{PicoStr, ResolvedPicoStr};
pub use self::round::{round_int_with_precision, round_with_precision};
pub use self::scalar::Scalar;
Expand Down
48 changes: 48 additions & 0 deletions crates/typst-utils/src/listset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::ops::DerefMut;

/// Picked by gut feeling. Could probably even be a bit larger.
const CUT_OFF: usize = 15;

/// A set backed by a mutable slice-like data structure.
///
/// This data structure uses two different strategies depending on size:
///
/// - When the list is small, it is just kept as is and searched linearly in
/// [`contains`](Self::contains).
///
/// - When the list is a bit bigger, it's sorted in [`new`](Self::new) and then
/// binary-searched for containment checks.
pub struct ListSet<S>(S);

impl<T, S> ListSet<S>
where
S: DerefMut<Target = [T]>,
T: Ord,
{
/// Creates a new list set.
///
/// If the list is longer than the cutoff point, it is sorted.
pub fn new(mut list: S) -> Self {
if list.len() > CUT_OFF {
list.sort_unstable();
}
Self(list)
}

/// Whether the set contains no elements.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}

/// Checks whether the set contains the given value.
///
/// If the list is shorter than the cutoff point, performs a linear search.
/// If it is longer, performs a binary search.
pub fn contains(&self, value: &T) -> bool {
if self.0.len() > CUT_OFF {
self.0.binary_search(value).is_ok()
} else {
self.0.contains(value)
}
}
}
2 changes: 1 addition & 1 deletion tests/ref/html/link-html-id-attach.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<li><a href="#t8">Go</a></li>
</ul>
<p id="t1">Hi</p>
<p id="t2">Hi there</p>
<p><span id="t2">Hi</span> 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>
Expand Down
Binary file added tests/ref/tags-textual.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions tests/suite/introspection/tags.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
--- tags-grouping ---
// Test how grouping rules handle tags at their edges. To observe this scenario,
// we can in principle have a link at the start or end of a paragraph and see
// whether the two nest correctly.
//
// Unfortunately, there isn't really a way to test end tags from Typst code.
// Hence, we're simulating it with metadata here. Note that this tests for
// slightly different, but even a bit more complex behavior since each metadata
// has its own start and end tags. Effectively, we are enforcing that not just
// the trailing end tag is kept in the paragraph grouping, but also that
// start/end combos before it are kept, too.

// Hide everything ... we don't need a reference image.
#set text(size: 0pt)
#show: hide
#show ref: [Ref]

#let case(body, output) = context {
// Get a unique key for the case.
let key = here()
let tagged(s, it) = {
metadata((key, "<" + s + ">"))
it
metadata((key, "</" + s + ">"))
}

// Note: This only works for locatable elements because otherwise the
// metadata tags won't be sandwiched by other tags and not forced into the
// paragraph grouping.
show par: tagged.with("p")
show link: tagged.with("a")
show ref: tagged.with("ref")
body

context test(
// Finds only metadata keyed by `key`, which is unique for this case.
query(metadata)
.filter(e => e.value.first() == key)
.map(e => e.value.last())
.join(),
output
)
}

// Both link and ref are contained in the paragraph.
#case(
[@ref #link("A")[A]],
"<p><ref></ref><a></a></p>"
)

// When there's a trailing space, that's okay.
#case(
[@ref #link("A")[A ]],
"<p><ref></ref><a></a></p>"
)

// Both link and ref are contained in the paragraph.
#case(
[#link("A")[A] @ref],
"<p><a></a><ref></ref></p>"
)

// When there's a leading space, that's okay.
#case(
[#link("A")[ A] @ref],
"<p><a></a><ref></ref></p>"
)

// When there's only a link, it will surround the paragraph.
#case(
link("A")[A],
"<a><p></p></a>"
)

--- tags-textual ---
// Ensure that tags and spaces aren't reordered in textual grouping.
A#metadata(none)<a> #metadata(none)<b>#box[B]

#context assert(
locate(<a>).position().x + 1pt < locate(<b>).position().x
)
Loading