summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-realize/src/lib.rs53
-rw-r--r--tests/ref/cases-content-symbol.pngbin0 -> 191 bytes
-rw-r--r--tests/ref/cases-content-text.pngbin0 -> 184 bytes
-rw-r--r--tests/suite/text/case.typ8
4 files changed, 48 insertions, 13 deletions
diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs
index 99db2ef1..ff42c3e9 100644
--- a/crates/typst-realize/src/lib.rs
+++ b/crates/typst-realize/src/lib.rs
@@ -221,7 +221,7 @@ impl<'a, 'x, 'y, 'z, 's> Grouped<'a, 'x, 'y, 'z, 's> {
/// Handles an arbitrary piece of content during realization.
fn visit<'a>(
s: &mut State<'a, '_, '_, '_>,
- mut content: &'a Content,
+ content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<()> {
// Tags can always simply be pushed.
@@ -230,12 +230,6 @@ fn visit<'a>(
return Ok(());
}
- if let Some(elem) = content.to_packed::<SymbolElem>() {
- // This is a hack to avoid affecting layout that will be replaced in a
- // later commit.
- content = Box::leak(Box::new(TextElem::packed(elem.text.to_string())));
- }
-
// Transformations for math content based on the realization kind. Needs
// to happen before show rules.
if visit_math_rules(s, content, styles)? {
@@ -247,7 +241,7 @@ fn visit<'a>(
return Ok(());
}
- // Recurse into sequences. Styled elements and sequences can currently also
+ // Recurse into sequences. Styled elements and sequences can currently also
// have labels, so this needs to happen before they are handled.
if let Some(sequence) = content.to_packed::<SequenceElem>() {
for elem in &sequence.children {
@@ -301,7 +295,14 @@ fn visit_math_rules<'a>(
// In normal realization, we apply regex show rules to consecutive
// 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::<TextElem>() {
+ 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)
+ {
+ visit_regex_match(s, &[(content, styles)], m)?;
+ return Ok(true);
+ }
+ } else if let Some(elem) = content.to_packed::<TextElem>() {
if let Some(m) = find_regex_match_in_str(&elem.text, styles) {
visit_regex_match(s, &[(content, styles)], m)?;
return Ok(true);
@@ -314,6 +315,14 @@ fn visit_math_rules<'a>(
visit(s, s.store(eq), styles)?;
return Ok(true);
}
+
+ // 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 text = TextElem::packed(elem.text).spanned(elem.span());
+ visit(s, s.store(text), styles)?;
+ return Ok(true);
+ }
}
Ok(false)
@@ -792,7 +801,7 @@ static HTML_DOCUMENT_RULES: &[&GroupingRule] =
/// Grouping rules used in HTML fragment realization.
static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
-/// Grouping rules used in math realizatio.
+/// Grouping rules used in math realization.
static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
/// Groups adjacent textual elements for text show rule application.
@@ -801,6 +810,9 @@ static TEXTUAL: GroupingRule = GroupingRule {
tags: true,
trigger: |content, _| {
let elem = content.elem();
+ // Note that `SymbolElem` converts into `TextElem` before textual show
+ // rules run, and we apply textual rules to elements manually during
+ // math realization, so we don't check for it here.
elem == TextElem::elem()
|| elem == LinebreakElem::elem()
|| elem == SmartQuoteElem::elem()
@@ -1124,7 +1136,16 @@ fn visit_regex_match<'a>(
m: RegexMatch<'a>,
) -> SourceResult<()> {
let match_range = m.offset..m.offset + m.text.len();
- let piece = TextElem::packed(m.text);
+
+ // Replace with the correct intuitive element kind: if matching against a
+ // lone symbol, return a `SymbolElem`, otherwise return a newly composed
+ // `TextElem`. We should only match against a `SymbolElem` during math
+ // realization (`RealizationKind::Math`).
+ let piece = match elems {
+ &[(lone, _)] if lone.is::<SymbolElem>() => lone.clone(),
+ _ => TextElem::packed(m.text),
+ };
+
let context = Context::new(None, Some(m.styles));
let output = m.recipe.apply(s.engine, context.track(), piece)?;
@@ -1147,10 +1168,16 @@ fn visit_regex_match<'a>(
continue;
}
- // At this point, we can have a `TextElem`, `SpaceElem`,
+ // At this point, we can have a `TextElem`, `SymbolElem`, `SpaceElem`,
// `LinebreakElem`, or `SmartQuoteElem`. We now determine the range of
// the element.
- let len = content.to_packed::<TextElem>().map_or(1, |elem| elem.text.len());
+ 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()
+ } else {
+ 1 // The rest are Ascii, so just one byte.
+ };
let elem_range = cursor..cursor + len;
// If the element starts before the start of match, visit it fully or
diff --git a/tests/ref/cases-content-symbol.png b/tests/ref/cases-content-symbol.png
new file mode 100644
index 00000000..b0b8a65e
--- /dev/null
+++ b/tests/ref/cases-content-symbol.png
Binary files differ
diff --git a/tests/ref/cases-content-text.png b/tests/ref/cases-content-text.png
new file mode 100644
index 00000000..d9767582
--- /dev/null
+++ b/tests/ref/cases-content-text.png
Binary files differ
diff --git a/tests/suite/text/case.typ b/tests/suite/text/case.typ
index 2bf68bc3..964ff28b 100644
--- a/tests/suite/text/case.typ
+++ b/tests/suite/text/case.typ
@@ -6,6 +6,14 @@
#test(upper(memes), "ARE MEMES GREAT?")
#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ")
+--- cases-content-text ---
+// Check that cases are applied to text nested in content
+#lower(box("HI!"))
+
+--- cases-content-symbol ---
+// Check that cases are applied to symbols nested in content
+#lower($H I !$.body)
+
--- upper-bad-type ---
// Error: 8-9 expected string or content, found integer
#upper(1)