mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +00:00
feat: make compile-based symbol resolver (#269)
* feat: use new symbol resolver * feat: render emojis in symbol view
This commit is contained in:
parent
95db52b068
commit
d9df64bca7
2 changed files with 213 additions and 111 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue