feat: make compile-based symbol resolver (#269)

* feat: use new symbol resolver

* feat: render emojis in symbol view
This commit is contained in:
Myriad-Dreamin 2024-05-09 12:33:19 +08:00 committed by GitHub
parent 95db52b068
commit d9df64bca7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 213 additions and 111 deletions

View file

@ -1,9 +1,14 @@
use std::{collections::BTreeMap, path::Path, sync::Arc};
use typst_ts_compiler::{service::EntryManager, ShadowApi};
use typst_ts_core::{config::compiler::EntryState, font::GlyphId, TypstDocument, TypstFont};
pub use super::prelude::*;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResourceSymbolResponse {
symbols: HashMap<String, ResourceSymbolItem>,
symbols: BTreeMap<String, ResourceSymbolItem>,
font_selects: Vec<FontItem>,
glyph_defs: String,
}
@ -15,12 +20,14 @@ struct ResourceSymbolItem {
glyphs: Vec<ResourceGlyphDesc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum SymCategory {
Accent,
Greek,
ControlOrSpace,
Misc,
Emoji,
}
#[derive(Debug, Serialize, Deserialize)]
@ -48,7 +55,7 @@ struct FontItem {
// vertical: bool,
}
type ResourceSymbolMap = HashMap<String, ResourceSymbolItem>;
type ResourceSymbolMap = BTreeMap<String, ResourceSymbolItem>;
static CAT_MAP: Lazy<HashMap<&str, SymCategory>> = Lazy::new(|| {
use SymCategory::*;
@ -128,6 +135,25 @@ static CAT_MAP: Lazy<HashMap<&str, SymCategory>> = Lazy::new(|| {
("sym.sigma.alt", Greek),
("sym.theta.alt", Greek),
("sym.ell", Greek),
("sym.lrm", ControlOrSpace),
("sym.rlm", ControlOrSpace),
("sym.wj", ControlOrSpace),
("sym.zwj", ControlOrSpace),
("sym.zwnj", ControlOrSpace),
("sym.zws", ControlOrSpace),
("sym.space", ControlOrSpace),
("sym.space.nobreak", ControlOrSpace),
("sym.space.nobreak.narrow", ControlOrSpace),
("sym.space.en", ControlOrSpace),
("sym.space.quad", ControlOrSpace),
("sym.space.third", ControlOrSpace),
("sym.space.quarter", ControlOrSpace),
("sym.space.sixth", ControlOrSpace),
("sym.space.med", ControlOrSpace),
("sym.space.fig", ControlOrSpace),
("sym.space.punct", ControlOrSpace),
("sym.space.thin", ControlOrSpace),
("sym.space.hair", ControlOrSpace),
])
});
@ -135,73 +161,59 @@ impl TypstLanguageServer {
/// Get the all valid symbols
pub fn get_symbol_resources(&self) -> ZResult<JsonValue> {
let mut symbols = ResourceSymbolMap::new();
populate_scope(typst::symbols::sym().scope(), "sym", &mut symbols);
// currently we don't have plan on emoji
// populate_scope(typst::symbols::emoji().scope(), "emoji", &mut symbols);
use typst::symbols::{emoji, sym};
populate_scope(sym().scope(), "sym", SymCategory::Misc, &mut symbols);
// todo: disabling emoji module, as there is performant issue on emojis
let _ = emoji;
// populate_scope(emoji().scope(), "emoji", SymCategory::Emoji, &mut symbols);
let chars = symbols
.values()
.map(|e| char::from_u32(e.unicode).unwrap())
.collect::<String>();
const PRELUDE: &str = r#"#show math.equation: set text(font: (
"New Computer Modern Math",
"Latin Modern Math",
"STIX Two Math",
"Cambria Math",
"New Computer Modern",
"Cambria",
))
"#;
let math_shaping_text = symbols.iter().fold(PRELUDE.to_owned(), |mut o, (k, e)| {
use std::fmt::Write;
writeln!(o, "$#{k}$/* {} */#pagebreak()", e.unicode).ok();
o
});
log::debug!("math shaping text: {text}", text = math_shaping_text);
let symbols_ref = symbols.keys().cloned().collect::<Vec<_>>();
let font = self
.primary()
.steal(move |e| {
use typst::text::FontVariant;
use typst::World;
let book = e.compiler.world().book();
let entry_path: Arc<Path> = Path::new("/._sym_.typ").into();
// todo: bad font fallback
let new_entry = EntryState::new_rootless(entry_path.clone())?;
let old_entry = e.compiler.world_mut().mutate_entry(new_entry).ok()?;
let prepared = e
.compiler
.map_shadow(&entry_path, math_shaping_text.into_bytes().into())
.is_ok();
let sym_doc = prepared.then(|| e.compiler.pure_compile(&mut Default::default()));
e.compiler.world_mut().mutate_entry(old_entry).ok()?;
let fonts = &["New Computer Modern Math", "Latin Modern Math", "Cambria"];
let preferred_fonts = fonts
.iter()
.flat_map(|f| {
let f = f.to_lowercase();
book.select(&f, FontVariant::default())
.and_then(|i| e.compiler.world().font(i))
})
.collect::<Vec<_>>();
let mut last_hit: Option<typst_ts_core::TypstFont> = None;
log::info!("font init: {hit:?}", hit = last_hit);
let fonts = chars
.chars()
.map(|c| {
for font in &preferred_fonts {
if font.info().coverage.contains(c as u32) {
return Some(font.clone());
}
}
if let Some(last_hit) = &last_hit {
if last_hit.info().coverage.contains(c as u32) {
return Some(last_hit.clone());
}
}
let hit =
book.select_fallback(None, FontVariant::default(), &c.to_string())?;
last_hit = e.compiler.world().font(hit);
log::info!("font hit: {hit:?}", hit = last_hit);
last_hit.clone()
})
.collect::<Vec<_>>();
fonts
log::debug!(
"sym doc: {doc:?}",
doc = sym_doc.as_ref().map(|e| e.as_ref().map(|_| ()))
);
let doc = sym_doc.transpose().ok()??;
Some(trait_symbol_fonts(&doc, &symbols_ref))
})
.ok();
.ok()
.flatten();
let mut glyph_def = String::new();
let mut collected_fonts = None;
if let Some(fonts) = font.clone() {
if let Some(glyph_mapping) = font.clone() {
let glyph_provider = typst_ts_core::font::GlyphProvider::default();
let glyph_pass =
typst_ts_core::vector::pass::ConvertInnerImpl::new(glyph_provider, false);
@ -209,19 +221,17 @@ impl TypstLanguageServer {
let mut glyph_renderer = Svg::default();
let mut glyphs = vec![];
let font_collected = fonts
.iter()
.flatten()
let font_collected = glyph_mapping
.values()
.map(|e| e.0.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.cloned()
.collect::<Vec<_>>();
let mut render_char = |font: Option<&typst_ts_core::TypstFont>, u| {
let font = font?;
let font_index = font_collected.iter().position(|e| e == font).unwrap() as u32;
let mut render_sym = |u| {
let (font, id) = glyph_mapping.get(u)?.clone();
let font_index = font_collected.iter().position(|e| e == &font).unwrap() as u32;
let id = font.ttf().glyph_index(char::from_u32(u).unwrap())?;
let width = font.ttf().glyph_hor_advance(id);
let height = font.ttf().glyph_ver_advance(id);
let bbox = font.ttf().glyph_bounding_box(id);
@ -248,8 +258,8 @@ impl TypstLanguageServer {
})
};
for ((_, v), font) in symbols.iter_mut().zip(fonts.iter()) {
let Some(desc) = render_char(font.as_ref(), v.unicode) else {
for (k, v) in symbols.iter_mut() {
let Some(desc) = render_sym(k) else {
continue;
};
@ -289,7 +299,84 @@ impl TypstLanguageServer {
}
}
fn populate(sym: &Symbol, mod_name: &str, sym_name: &str, out: &mut ResourceSymbolMap) {
fn trait_symbol_fonts(
doc: &TypstDocument,
symbols: &[String],
) -> HashMap<String, (TypstFont, GlyphId)> {
use typst::layout::Frame;
use typst::layout::FrameItem;
let mut worker = Worker {
symbols,
active: "",
res: HashMap::new(),
};
worker.work(doc);
let res = worker.res;
struct Worker<'a> {
symbols: &'a [String],
active: &'a str,
res: HashMap<String, (TypstFont, GlyphId)>,
}
impl Worker<'_> {
fn work(&mut self, doc: &TypstDocument) {
for (pg, s) in doc.pages.iter().zip(self.symbols.iter()) {
self.active = s;
self.work_frame(&pg.frame);
}
}
fn work_frame(&mut self, k: &Frame) {
for (_, item) in k.items() {
let text = match item {
FrameItem::Group(g) => {
self.work_frame(&g.frame);
continue;
}
FrameItem::Text(text) => text,
FrameItem::Shape(_, _) | FrameItem::Image(_, _, _) | FrameItem::Meta(_, _) => {
continue
}
};
let font = text.font.clone();
for g in &text.glyphs {
let g_text = &text.text[g.range()];
let chars_count = g_text.chars().count();
if chars_count > 1 {
log::warn!("multi char glyph: {g_text}");
continue;
}
let Some(ch) = g_text.chars().next() else {
continue;
};
if ch.is_whitespace() {
continue;
}
log::debug!(
"glyph: {active} => {ch} ({chc:x})",
active = self.active,
chc = ch as u32
);
self.res
.insert(self.active.to_owned(), (font.clone(), GlyphId(g.id)));
}
}
}
}
res
}
fn populate(
sym: &Symbol,
mod_name: &str,
sym_name: &str,
fallback_cat: SymCategory,
out: &mut ResourceSymbolMap,
) {
for (modifier_name, ch) in sym.variants() {
let mut name =
String::with_capacity(mod_name.len() + sym_name.len() + modifier_name.len() + 2);
@ -303,10 +390,7 @@ fn populate(sym: &Symbol, mod_name: &str, sym_name: &str, out: &mut ResourceSymb
name.push_str(modifier_name);
}
let category = CAT_MAP
.get(name.as_str())
.cloned()
.unwrap_or(SymCategory::Misc);
let category = CAT_MAP.get(name.as_str()).cloned().unwrap_or(fallback_cat);
out.insert(
name,
ResourceSymbolItem {
@ -318,12 +402,17 @@ fn populate(sym: &Symbol, mod_name: &str, sym_name: &str, out: &mut ResourceSymb
}
}
fn populate_scope(sym: &Scope, mod_name: &str, out: &mut ResourceSymbolMap) {
fn populate_scope(
sym: &Scope,
mod_name: &str,
fallback_cat: SymCategory,
out: &mut ResourceSymbolMap,
) {
for (k, v) in sym.iter() {
let Value::Symbol(sym) = v else {
continue;
};
populate(sym, mod_name, k, out)
populate(sym, mod_name, k, fallback_cat, out)
}
}

View file

@ -296,6 +296,10 @@ const CATEGORY_INFO: SymbolCategory[] = [
value: "greek",
name: "Greek Letters",
},
{
value: "controlOrSpace",
name: "Control Or Space",
},
{
value: "hebrew",
name: "Hebrew Letters",
@ -328,6 +332,10 @@ const CATEGORY_INFO: SymbolCategory[] = [
value: "misc",
name: "Miscellaneous",
},
{
value: "emoji",
name: "Emoji",
},
{
value: "letterStyle",
name: "Letter Styles",
@ -371,53 +379,58 @@ export const SymbolPicker = () => {
});
const SymbolCell = (sym: SymbolItem) => {
let maskInfo = "";
let maskInfo = div();
let symCellWidth = "36px";
if (sym.glyphs?.length && sym.glyphs[0].shape) {
let fontSelected = symInfo.val.fontSelects![sym.glyphs[0].fontIndex];
let primaryGlyph = sym.glyphs[0];
const path = symbolDefs.querySelector(`#${primaryGlyph.shape}`);
setTimeout(() => {
let fontSelected = symInfo.val.fontSelects![sym.glyphs[0].fontIndex];
let primaryGlyph = sym.glyphs[0];
const path = symbolDefs.querySelector(`#${primaryGlyph.shape}`);
const diff = (min?: number, max?: number) => {
if (min === undefined || max === undefined) return 0;
return Math.abs(max - min);
};
const diff = (min?: number, max?: number) => {
if (min === undefined || max === undefined) return 0;
return Math.abs(max - min);
};
const bboxXWidth = diff(primaryGlyph.xMin, primaryGlyph.xMax);
let xWidth = Math.max(
bboxXWidth,
primaryGlyph.xAdvance || fontSelected.unitsPerEm
);
const bboxXWidth = diff(primaryGlyph.xMin, primaryGlyph.xMax);
let xWidth = Math.max(
bboxXWidth,
primaryGlyph.xAdvance || fontSelected.unitsPerEm
);
let yReal = diff(primaryGlyph.yMin, primaryGlyph.yMax);
let yGlobal = primaryGlyph.yAdvance || fontSelected.unitsPerEm;
let yWidth = Math.max(yReal, yGlobal);
let yReal = diff(primaryGlyph.yMin, primaryGlyph.yMax);
let yGlobal = primaryGlyph.yAdvance || fontSelected.unitsPerEm;
let yWidth = Math.max(yReal, yGlobal);
let symWidth;
let symHeight;
if (xWidth < yWidth) {
// = `${(primaryGlyph.xAdvance / fontSelected.unitsPerEm) * 33}px`;
symWidth = `${(xWidth / yWidth) * 33}px`;
symHeight = "33px";
} else {
symWidth = "33px";
symHeight = `${(yWidth / xWidth) * 33}px`;
}
let symWidth;
let symHeight;
if (xWidth < yWidth) {
// = `${(primaryGlyph.xAdvance / fontSelected.unitsPerEm) * 33}px`;
symWidth = `${(xWidth / yWidth) * 33}px`;
symHeight = "33px";
} else {
symWidth = "33px";
symHeight = `${(yWidth / xWidth) * 33}px`;
}
let yShift =
yReal >= yGlobal
? Math.abs(primaryGlyph.yMax || 0)
: (Math.abs(primaryGlyph.yMax || 0) + yWidth) / 2;
let yShift =
yReal >= yGlobal
? Math.abs(primaryGlyph.yMax || 0)
: (Math.abs(primaryGlyph.yMax || 0) + yWidth) / 2;
// centering-x the symbol
let xShift = -(primaryGlyph.xMin || 0) + (xWidth - bboxXWidth) / 2;
// centering-x the symbol
let xShift = -(primaryGlyph.xMin || 0) + (xWidth - bboxXWidth) / 2;
// translate(0, ${fontSelected.ascender * fontSelected.unitsPerEm})
const imageData = `<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="${symWidth}" height="${symHeight}" viewBox="0 0 ${xWidth} ${yWidth}" xmlns="http://www.w3.org/2000/svg" ><g transform="translate(${xShift}, ${yShift}) scale(1, -1)">${path?.outerHTML || ""}</g></svg>`;
// console.log(sym.typstCode, div({ innerHTML: imageData }));
maskInfo = `width: ${symWidth}; height: ${symHeight}; -webkit-mask-image: url('data:image/svg+xml;utf8,${encodeURIComponent(imageData)}'); -webkit-mask-size: auto ${symHeight}; -webkit-mask-repeat: no-repeat; transition: background-color 200ms; background-color: currentColor;`;
// translate(0, ${fontSelected.ascender * fontSelected.unitsPerEm})
const imageData = `<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="${symWidth}" height="${symHeight}" viewBox="0 0 ${xWidth} ${yWidth}" xmlns="http://www.w3.org/2000/svg" ><g transform="translate(${xShift}, ${yShift}) scale(1, -1)">${path?.outerHTML || ""}</g></svg>`;
// console.log(sym.typstCode, div({ innerHTML: imageData }));
maskInfo.setAttribute(
"style",
`width: ${symWidth}; height: ${symHeight}; -webkit-mask-image: url('data:image/svg+xml;utf8,${encodeURIComponent(imageData)}'); -webkit-mask-size: auto ${symHeight}; -webkit-mask-repeat: no-repeat; transition: background-color 200ms; background-color: currentColor;`
);
}, 1);
}
return div(
@ -445,7 +458,7 @@ export const SymbolPicker = () => {
});
},
},
maskInfo ? div({ style: maskInfo }) : null
maskInfo
);
};