Allow multi-character symbols (#6489)
Co-authored-by: Max <max@mkor.je> Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
c340c27924
commit
0a168b687e
|
|
@ -413,7 +413,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codex"
|
name = "codex"
|
||||||
version = "0.1.1"
|
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]]
|
[[package]]
|
||||||
name = "color-print"
|
name = "color-print"
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||||
clap_complete = "4.2.1"
|
clap_complete = "4.2.1"
|
||||||
clap_mangen = "0.2.10"
|
clap_mangen = "0.2.10"
|
||||||
codespan-reporting = "0.11"
|
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"
|
color-print = "0.3.6"
|
||||||
comemo = "0.5.0"
|
comemo = "0.5.0"
|
||||||
csv = "1"
|
csv = "1"
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ impl Eval for ast::Escape<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
||||||
|
|
||||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
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;
|
type Output = Value;
|
||||||
|
|
||||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||||
Ok(Value::Symbol(Symbol::single(self.get())))
|
Ok(Value::Symbol(Symbol::runtime_char(self.get())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ impl Eval for ast::MathShorthand<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
||||||
|
|
||||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||||
Ok(Value::Symbol(Symbol::single(self.get())))
|
Ok(Value::Symbol(Symbol::runtime_char(self.get())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ pub enum CompletionKind {
|
||||||
/// A font family.
|
/// A font family.
|
||||||
Font,
|
Font,
|
||||||
/// A symbol.
|
/// A symbol.
|
||||||
Symbol(char),
|
Symbol(EcoString),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Complete in comments. Or rather, don't!
|
/// Complete in comments. Or rather, don't!
|
||||||
|
|
@ -450,7 +450,7 @@ fn field_access_completions(
|
||||||
for modifier in symbol.modifiers() {
|
for modifier in symbol.modifiers() {
|
||||||
if let Ok(modified) = symbol.clone().modified((), modifier) {
|
if let Ok(modified) = symbol.clone().modified((), modifier) {
|
||||||
ctx.completions.push(Completion {
|
ctx.completions.push(Completion {
|
||||||
kind: CompletionKind::Symbol(modified.get()),
|
kind: CompletionKind::Symbol(modified.get().into()),
|
||||||
label: modifier.into(),
|
label: modifier.into(),
|
||||||
apply: None,
|
apply: None,
|
||||||
detail: None,
|
detail: None,
|
||||||
|
|
@ -1381,7 +1381,7 @@ impl<'a> CompletionContext<'a> {
|
||||||
kind: kind.unwrap_or_else(|| match value {
|
kind: kind.unwrap_or_else(|| match value {
|
||||||
Value::Func(_) => CompletionKind::Func,
|
Value::Func(_) => CompletionKind::Func,
|
||||||
Value::Type(_) => CompletionKind::Type,
|
Value::Type(_) => CompletionKind::Type,
|
||||||
Value::Symbol(s) => CompletionKind::Symbol(s.get()),
|
Value::Symbol(s) => CompletionKind::Symbol(s.get().into()),
|
||||||
_ => CompletionKind::Constant,
|
_ => CompletionKind::Constant,
|
||||||
}),
|
}),
|
||||||
label,
|
label,
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use rustybuzz::{BufferFlags, UnicodeBuffer};
|
||||||
use ttf_parser::GlyphId;
|
use ttf_parser::GlyphId;
|
||||||
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
use typst_library::diag::warning;
|
use typst_library::diag::{At, HintedStrResult, SourceResult, bail, warning};
|
||||||
use typst_library::foundations::StyleChain;
|
use typst_library::foundations::{Repr, StyleChain};
|
||||||
use typst_library::introspection::Tag;
|
use typst_library::introspection::Tag;
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
|
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
|
||||||
|
|
@ -307,7 +307,7 @@ impl GlyphFragment {
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
c: char,
|
c: char,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> Option<Self> {
|
) -> SourceResult<Option<Self>> {
|
||||||
Self::new(ctx.engine.world, styles, c.encode_utf8(&mut [0; 4]), span)
|
Self::new(ctx.engine.world, styles, c.encode_utf8(&mut [0; 4]), span)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,10 +318,10 @@ impl GlyphFragment {
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
text: &str,
|
text: &str,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> Option<GlyphFragment> {
|
) -> SourceResult<Option<GlyphFragment>> {
|
||||||
assert!(text.graphemes(true).count() == 1);
|
assert!(text.graphemes(true).count() == 1);
|
||||||
|
|
||||||
let (c, font, mut glyph) = shape(
|
let Some((c, font, mut glyph)) = shape(
|
||||||
world,
|
world,
|
||||||
variant(styles),
|
variant(styles),
|
||||||
features(styles),
|
features(styles),
|
||||||
|
|
@ -329,7 +329,11 @@ impl GlyphFragment {
|
||||||
styles.get(TextElem::fallback),
|
styles.get(TextElem::fallback),
|
||||||
text,
|
text,
|
||||||
families(styles).collect(),
|
families(styles).collect(),
|
||||||
)?;
|
)
|
||||||
|
.at(span)?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
glyph.span.0 = span;
|
glyph.span.0 = span;
|
||||||
|
|
||||||
let limits = Limits::for_char(c);
|
let limits = Limits::for_char(c);
|
||||||
|
|
@ -369,7 +373,7 @@ impl GlyphFragment {
|
||||||
modifiers: FrameModifiers::get_in(styles),
|
modifiers: FrameModifiers::get_in(styles),
|
||||||
};
|
};
|
||||||
fragment.update_glyph();
|
fragment.update_glyph();
|
||||||
Some(fragment)
|
Ok(Some(fragment))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets element id and boxes in appropriate way without changing other
|
/// Sets element id and boxes in appropriate way without changing other
|
||||||
|
|
@ -847,7 +851,7 @@ fn shape(
|
||||||
fallback: bool,
|
fallback: bool,
|
||||||
text: &str,
|
text: &str,
|
||||||
families: Vec<&FontFamily>,
|
families: Vec<&FontFamily>,
|
||||||
) -> Option<(char, Font, Glyph)> {
|
) -> HintedStrResult<Option<(char, Font, Glyph)>> {
|
||||||
let mut used = vec![];
|
let mut used = vec![];
|
||||||
let buffer = UnicodeBuffer::new();
|
let buffer = UnicodeBuffer::new();
|
||||||
shape_glyph(
|
shape_glyph(
|
||||||
|
|
@ -874,7 +878,7 @@ fn shape_glyph<'a>(
|
||||||
fallback: bool,
|
fallback: bool,
|
||||||
text: &str,
|
text: &str,
|
||||||
mut families: impl Iterator<Item = &'a FontFamily> + Clone,
|
mut families: impl Iterator<Item = &'a FontFamily> + Clone,
|
||||||
) -> Option<(char, Font, Glyph)> {
|
) -> HintedStrResult<Option<(char, Font, Glyph)>> {
|
||||||
// Find the next available family.
|
// Find the next available family.
|
||||||
let book = world.book();
|
let book = world.book();
|
||||||
let mut selection = None;
|
let mut selection = None;
|
||||||
|
|
@ -913,9 +917,9 @@ fn shape_glyph<'a>(
|
||||||
span: (Span::detached(), 0),
|
span: (Span::detached(), 0),
|
||||||
};
|
};
|
||||||
let c = text.chars().next().unwrap();
|
let c = text.chars().next().unwrap();
|
||||||
return Some((c, font, glyph));
|
return Ok(Some((c, font, glyph)));
|
||||||
}
|
}
|
||||||
return None;
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
// This font has been exhausted and will not be used again.
|
// This font has been exhausted and will not be used again.
|
||||||
|
|
@ -944,9 +948,13 @@ fn shape_glyph<'a>(
|
||||||
|
|
||||||
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
|
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
|
||||||
match buffer.len() {
|
match buffer.len() {
|
||||||
0 => return None,
|
0 => return Ok(None),
|
||||||
1 => {}
|
1 => {}
|
||||||
_ => unreachable!(),
|
// TODO: Deal with multiple glyphs.
|
||||||
|
_ => bail!(
|
||||||
|
"shaping the text `{}` yielded more than one glyph", text.repr();
|
||||||
|
hint: "please report this as a bug",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = buffer.glyph_infos()[0];
|
let info = buffer.glyph_infos()[0];
|
||||||
|
|
@ -969,7 +977,7 @@ fn shape_glyph<'a>(
|
||||||
span: (Span::detached(), 0),
|
span: (Span::detached(), 0),
|
||||||
};
|
};
|
||||||
let c = text[cluster..].chars().next().unwrap();
|
let c = text[cluster..].chars().next().unwrap();
|
||||||
Some((c, font, glyph))
|
Ok(Some((c, font, glyph)))
|
||||||
} else {
|
} else {
|
||||||
shape_glyph(
|
shape_glyph(
|
||||||
world,
|
world,
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ fn layout_body(
|
||||||
// way too big.
|
// way too big.
|
||||||
// This will never panic as a paren will never shape into nothing.
|
// This will never panic as a paren will never shape into nothing.
|
||||||
let paren =
|
let paren =
|
||||||
GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())
|
GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
for (column, col) in columns.iter().zip(&mut cols) {
|
for (column, col) in columns.iter().zip(&mut cols) {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ fn layout_inline_text(
|
||||||
|
|
||||||
// This won't panic as ASCII digits and '.' will never end up as
|
// This won't panic as ASCII digits and '.' will never end up as
|
||||||
// nothing after shaping.
|
// nothing after shaping.
|
||||||
let glyph = GlyphFragment::new_char(ctx, styles, c, span).unwrap();
|
let glyph = GlyphFragment::new_char(ctx, styles, c, span)?.unwrap();
|
||||||
fragments.push(glyph.into());
|
fragments.push(glyph.into());
|
||||||
}
|
}
|
||||||
let frame = MathRun::new(fragments).into_frame(styles);
|
let frame = MathRun::new(fragments).into_frame(styles);
|
||||||
|
|
@ -129,41 +129,47 @@ pub fn layout_symbol(
|
||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
// 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), ctx.font().clone()) {
|
|
||||||
(Some(c), font) if has_dtls_feat(&font) => (c, styles.chain(&dtls)),
|
|
||||||
_ => (elem.text, styles),
|
|
||||||
};
|
|
||||||
|
|
||||||
let variant = styles.get(EquationElem::variant);
|
let variant = styles.get(EquationElem::variant);
|
||||||
let bold = styles.get(EquationElem::bold);
|
let bold = styles.get(EquationElem::bold);
|
||||||
let italic = styles.get(EquationElem::italic);
|
let italic = styles.get(EquationElem::italic);
|
||||||
|
let dtls = style_dtls();
|
||||||
|
let has_dtls_feat = has_dtls_feat(ctx.font());
|
||||||
|
for cluster in elem.text.graphemes(true) {
|
||||||
|
// Switch dotless char to normal when we have the dtls OpenType feature.
|
||||||
|
// This should happen before the main styling pass.
|
||||||
|
let mut enable_dtls = false;
|
||||||
|
let text: EcoString = cluster
|
||||||
|
.chars()
|
||||||
|
.flat_map(|mut c| {
|
||||||
|
if has_dtls_feat && let Some(d) = try_dotless(c) {
|
||||||
|
enable_dtls = true;
|
||||||
|
c = d;
|
||||||
|
}
|
||||||
|
to_style(c, MathStyle::select(c, variant, bold, italic))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let styles = if enable_dtls { styles.chain(&dtls) } else { styles };
|
||||||
|
|
||||||
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
if let Some(mut glyph) =
|
||||||
let text: EcoString = to_style(unstyled_c, style).collect();
|
GlyphFragment::new(ctx.engine.world, styles, &text, elem.span())?
|
||||||
|
{
|
||||||
if let Some(mut glyph) =
|
if glyph.class == MathClass::Large {
|
||||||
GlyphFragment::new(ctx.engine.world, symbol_styles, &text, elem.span())
|
if styles.get(EquationElem::size) == MathSize::Display {
|
||||||
{
|
let height = glyph
|
||||||
if glyph.class == MathClass::Large {
|
.item
|
||||||
if styles.get(EquationElem::size) == MathSize::Display {
|
.font
|
||||||
let height = glyph
|
.math()
|
||||||
.item
|
.display_operator_min_height
|
||||||
.font
|
.at(glyph.item.size);
|
||||||
.math()
|
glyph.stretch_vertical(ctx, height);
|
||||||
.display_operator_min_height
|
};
|
||||||
.at(glyph.item.size);
|
// TeXbook p 155. Large operators are always vertically centered on
|
||||||
glyph.stretch_vertical(ctx, height);
|
// the axis.
|
||||||
};
|
glyph.center_on_axis();
|
||||||
// TeXbook p 155. Large operators are always vertically centered on
|
}
|
||||||
// the axis.
|
ctx.push(glyph);
|
||||||
glyph.center_on_axis();
|
|
||||||
}
|
}
|
||||||
ctx.push(glyph);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::fmt::{self, Debug, Display, Formatter, Write};
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use codex::ModifierSet;
|
use codex::ModifierSet;
|
||||||
|
|
@ -8,8 +8,9 @@ use rustc_hash::FxHashMap;
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use typst_syntax::{Span, Spanned, is_ident};
|
use typst_syntax::{Span, Spanned, is_ident};
|
||||||
use typst_utils::hash128;
|
use typst_utils::hash128;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::diag::{DeprecationSink, SourceResult, StrResult, bail};
|
use crate::diag::{DeprecationSink, SourceResult, StrResult, bail, error};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
Array, Content, Func, NativeElement, NativeFunc, Packed, PlainText, Repr as _, cast,
|
Array, Content, Func, NativeElement, NativeFunc, Packed, PlainText, Repr as _, cast,
|
||||||
elem, func, scope, ty,
|
elem, func, scope, ty,
|
||||||
|
|
@ -53,7 +54,7 @@ pub struct Symbol(Repr);
|
||||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||||
enum Repr {
|
enum Repr {
|
||||||
/// A native symbol that has no named variant.
|
/// A native symbol that has no named variant.
|
||||||
Single(char),
|
Single(&'static str),
|
||||||
/// A native symbol with multiple named variants.
|
/// A native symbol with multiple named variants.
|
||||||
Complex(&'static [Variant<&'static str>]),
|
Complex(&'static [Variant<&'static str>]),
|
||||||
/// A symbol with multiple named variants, where some modifiers may have
|
/// A symbol with multiple named variants, where some modifiers may have
|
||||||
|
|
@ -62,9 +63,9 @@ enum Repr {
|
||||||
Modified(Arc<(List, ModifierSet<EcoString>)>),
|
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.
|
/// optional deprecation message.
|
||||||
type Variant<S> = (ModifierSet<S>, char, Option<S>);
|
type Variant<S> = (ModifierSet<S>, S, Option<S>);
|
||||||
|
|
||||||
/// A collection of symbols.
|
/// A collection of symbols.
|
||||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||||
|
|
@ -74,9 +75,9 @@ enum List {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Symbol {
|
impl Symbol {
|
||||||
/// Create a new symbol from a single character.
|
/// Create a new symbol from a single value.
|
||||||
pub const fn single(c: char) -> Self {
|
pub const fn single(value: &'static str) -> Self {
|
||||||
Self(Repr::Single(c))
|
Self(Repr::Single(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a symbol with a static variant list.
|
/// Create a symbol with a static variant list.
|
||||||
|
|
@ -86,6 +87,11 @@ impl Symbol {
|
||||||
Self(Repr::Complex(list))
|
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.
|
/// Create a symbol with a runtime variant list.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn runtime(list: Box<[Variant<EcoString>]>) -> Self {
|
pub fn runtime(list: Box<[Variant<EcoString>]>) -> Self {
|
||||||
|
|
@ -93,15 +99,15 @@ impl Symbol {
|
||||||
Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default()))))
|
Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default()))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the symbol's character.
|
/// Get the symbol's value.
|
||||||
pub fn get(&self) -> char {
|
pub fn get(&self) -> &str {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
Repr::Single(c) => *c,
|
Repr::Single(value) => value,
|
||||||
Repr::Complex(_) => ModifierSet::<&'static str>::default()
|
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(),
|
.unwrap(),
|
||||||
Repr::Modified(arc) => {
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,27 +115,27 @@ impl Symbol {
|
||||||
/// Try to get the function associated with the symbol, if any.
|
/// Try to get the function associated with the symbol, if any.
|
||||||
pub fn func(&self) -> StrResult<Func> {
|
pub fn func(&self) -> StrResult<Func> {
|
||||||
match self.get() {
|
match self.get() {
|
||||||
'⌈' => Ok(crate::math::ceil::func()),
|
"⌈" => Ok(crate::math::ceil::func()),
|
||||||
'⌊' => Ok(crate::math::floor::func()),
|
"⌊" => Ok(crate::math::floor::func()),
|
||||||
'–' => Ok(crate::math::accent::dash::func()),
|
"–" => Ok(crate::math::accent::dash::func()),
|
||||||
'⋅' | '\u{0307}' => Ok(crate::math::accent::dot::func()),
|
"⋅" | "\u{0307}" => Ok(crate::math::accent::dot::func()),
|
||||||
'¨' => Ok(crate::math::accent::dot_double::func()),
|
"¨" => Ok(crate::math::accent::dot_double::func()),
|
||||||
'\u{20db}' => Ok(crate::math::accent::dot_triple::func()),
|
"\u{20db}" => Ok(crate::math::accent::dot_triple::func()),
|
||||||
'\u{20dc}' => Ok(crate::math::accent::dot_quad::func()),
|
"\u{20dc}" => Ok(crate::math::accent::dot_quad::func()),
|
||||||
'∼' => Ok(crate::math::accent::tilde::func()),
|
"∼" => Ok(crate::math::accent::tilde::func()),
|
||||||
'´' => Ok(crate::math::accent::acute::func()),
|
"´" => Ok(crate::math::accent::acute::func()),
|
||||||
'˝' => Ok(crate::math::accent::acute_double::func()),
|
"˝" => Ok(crate::math::accent::acute_double::func()),
|
||||||
'˘' => Ok(crate::math::accent::breve::func()),
|
"˘" => Ok(crate::math::accent::breve::func()),
|
||||||
'ˇ' => Ok(crate::math::accent::caron::func()),
|
"ˇ" => Ok(crate::math::accent::caron::func()),
|
||||||
'^' => Ok(crate::math::accent::hat::func()),
|
"^" => Ok(crate::math::accent::hat::func()),
|
||||||
'`' => Ok(crate::math::accent::grave::func()),
|
"`" => Ok(crate::math::accent::grave::func()),
|
||||||
'¯' => Ok(crate::math::accent::macron::func()),
|
"¯" => Ok(crate::math::accent::macron::func()),
|
||||||
'○' => Ok(crate::math::accent::circle::func()),
|
"○" => Ok(crate::math::accent::circle::func()),
|
||||||
'→' => Ok(crate::math::accent::arrow::func()),
|
"→" => Ok(crate::math::accent::arrow::func()),
|
||||||
'←' => Ok(crate::math::accent::arrow_l::func()),
|
"←" => Ok(crate::math::accent::arrow_l::func()),
|
||||||
'↔' => Ok(crate::math::accent::arrow_l_r::func()),
|
"↔" => Ok(crate::math::accent::arrow_l_r::func()),
|
||||||
'⇀' => Ok(crate::math::accent::harpoon::func()),
|
"⇀" => Ok(crate::math::accent::harpoon::func()),
|
||||||
'↼' => Ok(crate::math::accent::harpoon_lt::func()),
|
"↼" => Ok(crate::math::accent::harpoon_lt::func()),
|
||||||
_ => bail!("symbol {self} is not callable"),
|
_ => bail!("symbol {self} is not callable"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +170,7 @@ impl Symbol {
|
||||||
/// The characters that are covered by this symbol.
|
/// The characters that are covered by this symbol.
|
||||||
pub fn variants(&self) -> impl Iterator<Item = Variant<&str>> {
|
pub fn variants(&self) -> impl Iterator<Item = Variant<&str>> {
|
||||||
match &self.0 {
|
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::Complex(list) => Variants::Static(list.iter()),
|
||||||
Repr::Modified(arc) => arc.0.variants(),
|
Repr::Modified(arc) => arc.0.variants(),
|
||||||
}
|
}
|
||||||
|
|
@ -227,15 +233,29 @@ impl Symbol {
|
||||||
// A list of modifiers, cleared & reused in each iteration.
|
// A list of modifiers, cleared & reused in each iteration.
|
||||||
let mut modifiers = Vec::new();
|
let mut modifiers = Vec::new();
|
||||||
|
|
||||||
|
let mut errors = ecow::eco_vec![];
|
||||||
|
|
||||||
// Validate the variants.
|
// Validate the variants.
|
||||||
for (i, &Spanned { ref v, span }) in variants.iter().enumerate() {
|
'variants: for (i, &Spanned { ref v, span }) in variants.iter().enumerate() {
|
||||||
modifiers.clear();
|
modifiers.clear();
|
||||||
|
|
||||||
|
if v.1.is_empty() || v.1.graphemes(true).nth(1).is_some() {
|
||||||
|
errors.push(error!(
|
||||||
|
span, "invalid variant value: {}", v.1.repr();
|
||||||
|
hint: "variant value must be exactly one grapheme cluster"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if !v.0.is_empty() {
|
if !v.0.is_empty() {
|
||||||
// Collect all modifiers.
|
// Collect all modifiers.
|
||||||
for modifier in v.0.split('.') {
|
for modifier in v.0.split('.') {
|
||||||
if !is_ident(modifier) {
|
if !is_ident(modifier) {
|
||||||
bail!(span, "invalid symbol modifier: {}", modifier.repr());
|
errors.push(error!(
|
||||||
|
span,
|
||||||
|
"invalid symbol modifier: {}",
|
||||||
|
modifier.repr()
|
||||||
|
));
|
||||||
|
continue 'variants;
|
||||||
}
|
}
|
||||||
modifiers.push(modifier);
|
modifiers.push(modifier);
|
||||||
}
|
}
|
||||||
|
|
@ -246,29 +266,34 @@ impl Symbol {
|
||||||
|
|
||||||
// Ensure that there are no duplicate modifiers.
|
// Ensure that there are no duplicate modifiers.
|
||||||
if let Some(ms) = modifiers.windows(2).find(|ms| ms[0] == ms[1]) {
|
if let Some(ms) = modifiers.windows(2).find(|ms| ms[0] == ms[1]) {
|
||||||
bail!(
|
errors.push(error!(
|
||||||
span, "duplicate modifier within variant: {}", ms[0].repr();
|
span, "duplicate modifier within variant: {}", ms[0].repr();
|
||||||
hint: "modifiers are not ordered, so each one may appear only once"
|
hint: "modifiers are not ordered, so each one may appear only once"
|
||||||
)
|
));
|
||||||
|
continue 'variants;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether we had this set of modifiers before.
|
// Check whether we had this set of modifiers before.
|
||||||
let hash = hash128(&modifiers);
|
let hash = hash128(&modifiers);
|
||||||
if let Some(&i) = seen.get(&hash) {
|
if let Some(&i) = seen.get(&hash) {
|
||||||
if v.0.is_empty() {
|
errors.push(if v.0.is_empty() {
|
||||||
bail!(span, "duplicate default variant");
|
error!(span, "duplicate default variant")
|
||||||
} else if v.0 == variants[i].v.0 {
|
} else if v.0 == variants[i].v.0 {
|
||||||
bail!(span, "duplicate variant: {}", v.0.repr());
|
error!(span, "duplicate variant: {}", v.0.repr())
|
||||||
} else {
|
} else {
|
||||||
bail!(
|
error!(
|
||||||
span, "duplicate variant: {}", v.0.repr();
|
span, "duplicate variant: {}", v.0.repr();
|
||||||
hint: "variants with the same modifiers are identical, regardless of their order"
|
hint: "variants with the same modifiers are identical, regardless of their order"
|
||||||
)
|
)
|
||||||
}
|
});
|
||||||
|
continue 'variants;
|
||||||
}
|
}
|
||||||
|
|
||||||
seen.insert(hash, i);
|
seen.insert(hash, i);
|
||||||
}
|
}
|
||||||
|
if !errors.is_empty() {
|
||||||
|
return Err(errors);
|
||||||
|
}
|
||||||
|
|
||||||
let list = variants
|
let list = variants
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -280,14 +305,14 @@ impl Symbol {
|
||||||
|
|
||||||
impl Display for Symbol {
|
impl Display for Symbol {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_char(self.get())
|
f.write_str(self.get())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Repr {
|
impl Debug for Repr {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Single(c) => Debug::fmt(c, f),
|
Self::Single(value) => Debug::fmt(value, f),
|
||||||
Self::Complex(list) => list.fmt(f),
|
Self::Complex(list) => list.fmt(f),
|
||||||
Self::Modified(lists) => lists.fmt(f),
|
Self::Modified(lists) => lists.fmt(f),
|
||||||
}
|
}
|
||||||
|
|
@ -306,7 +331,7 @@ impl Debug for List {
|
||||||
impl crate::foundations::Repr for Symbol {
|
impl crate::foundations::Repr for Symbol {
|
||||||
fn repr(&self) -> EcoString {
|
fn repr(&self) -> EcoString {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
Repr::Single(c) => eco_format!("symbol(\"{}\")", *c),
|
Repr::Single(value) => eco_format!("symbol({})", value.repr()),
|
||||||
Repr::Complex(variants) => {
|
Repr::Complex(variants) => {
|
||||||
eco_format!(
|
eco_format!(
|
||||||
"symbol{}",
|
"symbol{}",
|
||||||
|
|
@ -342,15 +367,15 @@ fn repr_variants<'a>(
|
||||||
// that contain all applied modifiers.
|
// that contain all applied modifiers.
|
||||||
applied_modifiers.iter().all(|am| modifiers.contains(am))
|
applied_modifiers.iter().all(|am| modifiers.contains(am))
|
||||||
})
|
})
|
||||||
.map(|(modifiers, c, _)| {
|
.map(|(modifiers, value, _)| {
|
||||||
let trimmed_modifiers =
|
let trimmed_modifiers =
|
||||||
modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m));
|
modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m));
|
||||||
if trimmed_modifiers.clone().all(|m| m.is_empty()) {
|
if trimmed_modifiers.clone().all(|m| m.is_empty()) {
|
||||||
eco_format!("\"{c}\"")
|
value.repr()
|
||||||
} else {
|
} else {
|
||||||
let trimmed_modifiers =
|
let trimmed_modifiers =
|
||||||
trimmed_modifiers.collect::<Vec<_>>().join(".");
|
trimmed_modifiers.collect::<Vec<_>>().join(".");
|
||||||
eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c)
|
eco_format!("({}, {})", trimmed_modifiers.repr(), value.repr())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
|
|
@ -363,7 +388,7 @@ impl Serialize for Symbol {
|
||||||
where
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
{
|
{
|
||||||
serializer.serialize_char(self.get())
|
serializer.serialize_str(self.get())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,11 +403,11 @@ impl List {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A value that can be cast to a symbol.
|
/// A value that can be cast to a symbol.
|
||||||
pub struct SymbolVariant(EcoString, char);
|
pub struct SymbolVariant(EcoString, EcoString);
|
||||||
|
|
||||||
cast! {
|
cast! {
|
||||||
SymbolVariant,
|
SymbolVariant,
|
||||||
c: char => Self(EcoString::new(), c),
|
s: EcoString => Self(EcoString::new(), s),
|
||||||
array: Array => {
|
array: Array => {
|
||||||
let mut iter = array.into_iter();
|
let mut iter = array.into_iter();
|
||||||
match (iter.next(), iter.next(), iter.next()) {
|
match (iter.next(), iter.next(), iter.next()) {
|
||||||
|
|
@ -394,7 +419,7 @@ cast! {
|
||||||
|
|
||||||
/// Iterator over variants.
|
/// Iterator over variants.
|
||||||
enum Variants<'a> {
|
enum Variants<'a> {
|
||||||
Single(std::option::IntoIter<char>),
|
Single(std::iter::Once<&'static str>),
|
||||||
Static(std::slice::Iter<'static, Variant<&'static str>>),
|
Static(std::slice::Iter<'static, Variant<&'static str>>),
|
||||||
Runtime(std::slice::Iter<'a, Variant<EcoString>>),
|
Runtime(std::slice::Iter<'a, Variant<EcoString>>),
|
||||||
}
|
}
|
||||||
|
|
@ -407,7 +432,7 @@ impl<'a> Iterator for Variants<'a> {
|
||||||
Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)),
|
Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)),
|
||||||
Self::Static(list) => list.next().copied(),
|
Self::Static(list) => list.next().copied(),
|
||||||
Self::Runtime(list) => {
|
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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -416,21 +441,21 @@ impl<'a> Iterator for Variants<'a> {
|
||||||
/// A single character.
|
/// A single character.
|
||||||
#[elem(Repr, PlainText)]
|
#[elem(Repr, PlainText)]
|
||||||
pub struct SymbolElem {
|
pub struct SymbolElem {
|
||||||
/// The symbol's character.
|
/// The symbol's value.
|
||||||
#[required]
|
#[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 {
|
impl SymbolElem {
|
||||||
/// Create a new packed symbol element.
|
/// 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()
|
Self::new(text.into()).pack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlainText for Packed<SymbolElem> {
|
impl PlainText for Packed<SymbolElem> {
|
||||||
fn plain_text(&self, text: &mut EcoString) {
|
fn plain_text(&self, text: &mut EcoString) {
|
||||||
text.push(self.text);
|
text.push_str(&self.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,8 +187,8 @@ cast! {
|
||||||
Accent,
|
Accent,
|
||||||
self => self.0.into_value(),
|
self => self.0.into_value(),
|
||||||
v: char => Self::new(v),
|
v: char => Self::new(v),
|
||||||
v: Content => match v.to_packed::<SymbolElem>() {
|
v: Content => match v.to_packed::<SymbolElem>().and_then(|elem| elem.text.parse::<char>().ok()) {
|
||||||
Some(elem) => Self::new(elem.text),
|
Some(c) => Self::new(c),
|
||||||
None => bail!("expected a symbol"),
|
_ => bail!("expected a single-codepoint symbol"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ cast! {
|
||||||
Delimiter,
|
Delimiter,
|
||||||
self => self.0.into_value(),
|
self => self.0.into_value(),
|
||||||
_: NoneValue => Self::none(),
|
_: 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)?,
|
v: char => Self::char(v)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ impl From<codex::Module> for Scope {
|
||||||
impl From<codex::Symbol> for Symbol {
|
impl From<codex::Symbol> for Symbol {
|
||||||
fn from(symbol: codex::Symbol) -> Self {
|
fn from(symbol: codex::Symbol) -> Self {
|
||||||
match symbol {
|
match symbol {
|
||||||
codex::Symbol::Single(c) => Symbol::single(c),
|
codex::Symbol::Single(value) => Symbol::single(value),
|
||||||
codex::Symbol::Multi(list) => Symbol::list(list),
|
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
|
// textual elements via `TEXTUAL` grouping. However, in math, this is
|
||||||
// not desirable, so we just do it on a per-element basis.
|
// not desirable, so we just do it on a per-element basis.
|
||||||
if let Some(elem) = content.to_packed::<SymbolElem>() {
|
if let Some(elem) = content.to_packed::<SymbolElem>() {
|
||||||
if let Some(m) =
|
if let Some(m) = find_regex_match_in_str(elem.text.as_str(), styles) {
|
||||||
find_regex_match_in_str(elem.text.encode_utf8(&mut [0; 4]), styles)
|
|
||||||
{
|
|
||||||
visit_regex_match(s, &[(content, styles)], m)?;
|
visit_regex_match(s, &[(content, styles)], m)?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
@ -324,7 +322,7 @@ fn visit_kind_rules<'a>(
|
||||||
// Symbols in non-math content transparently convert to `TextElem` so we
|
// Symbols in non-math content transparently convert to `TextElem` so we
|
||||||
// don't have to handle them in non-math layout.
|
// don't have to handle them in non-math layout.
|
||||||
if let Some(elem) = content.to_packed::<SymbolElem>() {
|
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() {
|
if let Some(label) = elem.label() {
|
||||||
text.set_label(label);
|
text.set_label(label);
|
||||||
}
|
}
|
||||||
|
|
@ -1238,7 +1236,7 @@ fn visit_regex_match<'a>(
|
||||||
let len = if let Some(elem) = content.to_packed::<TextElem>() {
|
let len = if let Some(elem) = content.to_packed::<TextElem>() {
|
||||||
elem.text.len()
|
elem.text.len()
|
||||||
} else if let Some(elem) = content.to_packed::<SymbolElem>() {
|
} else if let Some(elem) = content.to_packed::<SymbolElem>() {
|
||||||
elem.text.len_utf8()
|
elem.text.len()
|
||||||
} else {
|
} else {
|
||||||
1 // The rest are Ascii, so just one byte.
|
1 // The rest are Ascii, so just one byte.
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -719,9 +719,13 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (variant, c, deprecation_message) in symbol.variants() {
|
for (variant, value, deprecation_message) in symbol.variants() {
|
||||||
|
let value_char = value.parse::<char>().ok();
|
||||||
|
|
||||||
let shorthand = |list: &[(&'static str, char)]| {
|
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);
|
let name = complete(variant);
|
||||||
|
|
@ -730,9 +734,14 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
|
||||||
name,
|
name,
|
||||||
markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST),
|
markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST),
|
||||||
math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST),
|
math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST),
|
||||||
math_class: typst_utils::default_math_class(c).map(math_class_name),
|
// Matches `typst_layout::math::GlyphFragment::new`
|
||||||
codepoint: c as _,
|
math_class: value.chars().next().and_then(|c| {
|
||||||
accent: typst::math::Accent::combine(c).is_some(),
|
typst_utils::default_math_class(c).map(math_class_name)
|
||||||
|
}),
|
||||||
|
value: value.into(),
|
||||||
|
// Matches casting `Symbol` to `Accent`
|
||||||
|
accent: value_char
|
||||||
|
.is_some_and(|c| typst::math::Accent::combine(c).is_some()),
|
||||||
alternates: symbol
|
alternates: symbol
|
||||||
.variants()
|
.variants()
|
||||||
.filter(|(other, _, _)| other != &variant)
|
.filter(|(other, _, _)| other != &variant)
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ pub struct SymbolsModel {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SymbolModel {
|
pub struct SymbolModel {
|
||||||
pub name: EcoString,
|
pub name: EcoString,
|
||||||
pub codepoint: u32,
|
pub value: EcoString,
|
||||||
pub accent: bool,
|
pub accent: bool,
|
||||||
pub alternates: Vec<EcoString>,
|
pub alternates: Vec<EcoString>,
|
||||||
pub markup_shorthand: Option<&'static str>,
|
pub markup_shorthand: Option<&'static str>,
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 558 B |
|
|
@ -21,6 +21,10 @@
|
||||||
("lightning", "🖄"),
|
("lightning", "🖄"),
|
||||||
("fly", "🖅"),
|
("fly", "🖅"),
|
||||||
)
|
)
|
||||||
|
#let one = symbol(
|
||||||
|
"1",
|
||||||
|
("emoji", "1️")
|
||||||
|
)
|
||||||
|
|
||||||
#envelope
|
#envelope
|
||||||
#envelope.stamped
|
#envelope.stamped
|
||||||
|
|
@ -28,6 +32,8 @@
|
||||||
#envelope.stamped.pen
|
#envelope.stamped.pen
|
||||||
#envelope.lightning
|
#envelope.lightning
|
||||||
#envelope.fly
|
#envelope.fly
|
||||||
|
#one
|
||||||
|
#one.emoji
|
||||||
|
|
||||||
--- symbol-constructor-empty ---
|
--- symbol-constructor-empty ---
|
||||||
// Error: 2-10 expected at least one variant
|
// Error: 2-10 expected at least one variant
|
||||||
|
|
@ -82,6 +88,26 @@
|
||||||
("variant.duplicate", "y"),
|
("variant.duplicate", "y"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- symbol-constructor-empty-variant-value ---
|
||||||
|
// Error: 2:3-2:5 invalid variant value: ""
|
||||||
|
// Hint: 2:3-2:5 variant value must be exactly one grapheme cluster
|
||||||
|
// Error: 3:3-3:16 invalid variant value: ""
|
||||||
|
// Hint: 3:3-3:16 variant value must be exactly one grapheme cluster
|
||||||
|
#symbol(
|
||||||
|
"",
|
||||||
|
("empty", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
--- symbol-constructor-multi-cluster-variant-value ---
|
||||||
|
// Error: 2:3-2:7 invalid variant value: "aa"
|
||||||
|
// Hint: 2:3-2:7 variant value must be exactly one grapheme cluster
|
||||||
|
// Error: 3:3-3:14 invalid variant value: "bb"
|
||||||
|
// Hint: 3:3-3:14 variant value must be exactly one grapheme cluster
|
||||||
|
#symbol(
|
||||||
|
"aa",
|
||||||
|
("b", "bb")
|
||||||
|
)
|
||||||
|
|
||||||
--- symbol-unknown-modifier ---
|
--- symbol-unknown-modifier ---
|
||||||
// Error: 13-20 unknown symbol modifier
|
// Error: 13-20 unknown symbol modifier
|
||||||
#emoji.face.garbage
|
#emoji.face.garbage
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue