feat: generate resource symbol svg in server and improve viewBox (#2109)
Some checks are pending
tinymist::auto_tag / auto-tag (push) Waiting to run
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / announce (push) Blocked by required conditions
tinymist::ci / build (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run

- feat: move symbol svg generation logic from client to client, and
directly display svg elements instead of mask-image. This significantly
boosts loading speed.
- fix: use font ascender and descender to bound view box, avoid
clipping.
This commit is contained in:
QuadnucYard 2025-09-10 19:17:06 +08:00 committed by GitHub
parent 6799951ee3
commit 4c8d34cdb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 312 additions and 465 deletions

View file

@ -5,7 +5,7 @@ mod prelude {
pub use std::collections::HashMap;
pub use reflexo_vec2svg::ir::{GlyphItem, GlyphRef};
pub use reflexo_vec2svg::ir::GlyphItem;
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value as JsonValue;
pub use sync_ls::*;

View file

@ -5,7 +5,6 @@ use reflexo_typst::TypstPagedDocument;
use reflexo_typst::{vector::font::GlyphId, TypstFont};
use reflexo_vec2svg::SvgGlyphBuilder;
use sync_ls::LspResult;
use tinymist_std::typst::TypstDocument;
use typst::foundations::Bytes;
use typst::{syntax::VirtualPath, World};
@ -16,16 +15,23 @@ use crate::world::{base::ShadowApi, EntryState, TaskInputs};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResourceSymbolResponse {
symbols: BTreeMap<String, ResourceSymbolItem>,
font_selects: Vec<FontItem>,
glyph_defs: HashMap<String, String>,
symbols: Vec<ResourceSymbolItem>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResourceSymbolItem {
id: String,
category: SymCategory,
unicode: u32,
#[serde(skip_serializing_if = "Option::is_none")]
glyph: Option<String>,
}
#[derive(Debug)]
struct SymbolItem {
category: SymCategory,
unicode: u32,
glyphs: Vec<ResourceGlyphDesc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
@ -60,32 +66,7 @@ enum SymCategory {
DoubleStruck,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResourceGlyphDesc {
font_index: u32,
x_advance: Option<u16>,
y_advance: Option<u16>,
x_min: Option<i16>,
x_max: Option<i16>,
y_min: Option<i16>,
y_max: Option<i16>,
name: Option<String>,
shape: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FontItem {
family: String,
cap_height: f32,
ascender: f32,
descender: f32,
units_per_em: f32,
// vertical: bool,
}
type ResourceSymbolMap = BTreeMap<String, ResourceSymbolItem>;
type ResourceSymbolMap = BTreeMap<String, SymbolItem>;
static CAT_MAP: LazyLock<HashMap<&str, SymCategory>> = LazyLock::new(|| {
use SymCategory::*;
@ -951,155 +932,13 @@ static CAT_MAP: LazyLock<HashMap<&str, SymCategory>> = LazyLock::new(|| {
impl ServerState {
/// Get the all valid symbols
pub async fn get_symbol_resources(snap: LspComputeGraph) -> LspResult<JsonValue> {
let mut symbols = ResourceSymbolMap::new();
let symbols = collect_symbols(&snap)?;
let std = snap
.library()
.std
.read()
.scope()
.ok_or_else(|| internal_error("cannot get std scope"))?;
let sym = std
.get("sym")
.ok_or_else(|| internal_error("cannot get sym"))?;
let glyph_mapping = render_symbols(&snap, &symbols)?;
if let Some(scope) = sym.read().scope() {
populate_scope(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 symbols = render_glyphs(&symbols, &glyph_mapping)?;
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: {math_shaping_text}");
let symbols_ref = symbols.keys().cloned().collect::<Vec<_>>();
let font = {
let entry_path: Arc<Path> = Path::new("/._sym_.typ").into();
let new_entry = EntryState::new_rootless(VirtualPath::new(&entry_path));
let mut forked = snap.world().task(TaskInputs {
entry: Some(new_entry),
..TaskInputs::default()
});
forked
.map_shadow_by_id(forked.main(), Bytes::from_string(math_shaping_text))
.map_err(|e| error_once!("cannot map shadow", err: e))
.map_err(internal_error)?;
let sym_doc = typst::compile::<TypstPagedDocument>(&forked)
.output
.map_err(|e| error_once!("cannot compile symbols", err: format!("{e:?}")))
.map_err(internal_error)?;
log::debug!("sym doc: {sym_doc:?}");
Some(trait_symbol_fonts(
&TypstDocument::Paged(Arc::new(sym_doc)),
&symbols_ref,
))
};
let mut glyph_defs = HashMap::new();
let mut collected_fonts = None;
if let Some(glyph_mapping) = font.clone() {
let glyph_provider = reflexo_vec2svg::GlyphProvider::default();
let glyph_pass =
reflexo_typst::vector::pass::ConvertInnerImpl::new(glyph_provider, false);
let mut glyphs = vec![];
let font_collected = glyph_mapping
.values()
.map(|e| e.0.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
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 width = font.ttf().glyph_hor_advance(id);
let height = font.ttf().glyph_ver_advance(id);
let bbox = font.ttf().glyph_bounding_box(id);
let glyph = glyph_pass.must_flat_glyph(&GlyphItem::Raw(font.clone(), id))?;
let g_ref = GlyphRef {
font_hash: font_index,
glyph_idx: id.0 as u32,
};
glyphs.push((g_ref, glyph));
Some(ResourceGlyphDesc {
font_index,
x_advance: width,
y_advance: height,
x_min: bbox.map(|e| e.x_min),
x_max: bbox.map(|e| e.x_max),
y_min: bbox.map(|e| e.y_min),
y_max: bbox.map(|e| e.y_max),
name: font.ttf().glyph_name(id).map(|e| e.to_owned()),
shape: Some(g_ref.as_svg_id("g")),
})
};
for (k, v) in symbols.iter_mut() {
let Some(desc) = render_sym(k) else {
continue;
};
v.glyphs.push(desc);
}
let mut builder = SvgGlyphBuilder::new();
glyph_defs = glyphs
.iter()
.map(|(id, item)| {
let glyph_id = id.as_svg_id("g");
let rendered = builder.render_glyph("", item).unwrap_or_default();
(glyph_id, rendered)
})
.collect();
collected_fonts = Some(font_collected);
}
let resp = ResourceSymbolResponse {
symbols,
font_selects: collected_fonts
.map(|e| e.into_iter())
.into_iter()
.flatten()
.map(|e| FontItem {
family: e.info().family.clone(),
cap_height: e.metrics().cap_height.get() as f32,
ascender: e.metrics().ascender.get() as f32,
descender: e.metrics().descender.get() as f32,
units_per_em: e.metrics().units_per_em as f32,
})
.collect::<Vec<_>>(),
glyph_defs,
};
let resp = ResourceSymbolResponse { symbols };
serde_json::to_value(resp)
.context("cannot serialize response")
@ -1107,82 +946,42 @@ impl ServerState {
}
}
fn trait_symbol_fonts(
doc: &TypstDocument,
symbols: &[String],
) -> HashMap<String, (TypstFont, GlyphId)> {
use typst::layout::Frame;
use typst::layout::FrameItem;
fn collect_symbols(snap: &LspComputeGraph) -> LspResult<BTreeMap<String, SymbolItem>> {
let mut symbols = ResourceSymbolMap::new();
let mut worker = Worker {
symbols,
active: "",
res: HashMap::new(),
};
worker.work(doc);
let res = worker.res;
let std = snap
.library()
.std
.read()
.scope()
.ok_or_else(|| internal_error("cannot get std scope"))?;
let sym = std
.get("sym")
.ok_or_else(|| internal_error("cannot get sym"))?;
struct Worker<'a> {
symbols: &'a [String],
active: &'a str,
res: HashMap<String, (TypstFont, GlyphId)>,
if let Some(scope) = sym.read().scope() {
populate_scope(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);
impl Worker<'_> {
fn work(&mut self, doc: &TypstDocument) {
match doc {
TypstDocument::Paged(paged_doc) => {
for (pg, s) in paged_doc.pages.iter().zip(self.symbols.iter()) {
self.active = s;
self.work_frame(&pg.frame);
}
}
// todo: handle html
TypstDocument::Html(..) => {}
}
}
Ok(symbols)
}
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::Link(_, _)
| FrameItem::Tag(_) => continue,
};
fn populate_scope(
sym: &Scope,
mod_name: &str,
fallback_cat: SymCategory,
out: &mut ResourceSymbolMap,
) {
for (k, b) in sym.iter() {
let Value::Symbol(sym) = b.read() else {
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)));
}
}
}
populate(sym, mod_name, k, fallback_cat, out)
}
res
}
fn populate(
@ -1208,26 +1007,188 @@ fn populate(
let category = CAT_MAP.get(name.as_str()).cloned().unwrap_or(fallback_cat);
out.insert(
name,
ResourceSymbolItem {
SymbolItem {
category,
unicode: ch as u32,
glyphs: vec![],
},
);
}
}
fn populate_scope(
sym: &Scope,
mod_name: &str,
fallback_cat: SymCategory,
out: &mut ResourceSymbolMap,
) {
for (k, b) in sym.iter() {
let Value::Symbol(sym) = b.read() else {
continue;
};
fn render_symbols(
snap: &LspComputeGraph,
symbols: &BTreeMap<String, SymbolItem>,
) -> LspResult<HashMap<String, (TypstFont, GlyphId)>> {
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",
))
"#;
populate(sym, mod_name, k, fallback_cat, out)
}
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: {math_shaping_text}");
let entry_path: Arc<Path> = Path::new("/._sym_.typ").into();
let new_entry = EntryState::new_rootless(VirtualPath::new(&entry_path));
let mut forked = snap.world().task(TaskInputs {
entry: Some(new_entry),
..TaskInputs::default()
});
forked
.map_shadow_by_id(forked.main(), Bytes::from_string(math_shaping_text))
.map_err(|e| error_once!("cannot map shadow", err: e))
.map_err(internal_error)?;
let sym_doc = typst::compile::<TypstPagedDocument>(&forked)
.output
.map_err(|e| error_once!("cannot compile symbols", err: format!("{e:?}")))
.map_err(internal_error)?;
log::debug!("sym doc: {sym_doc:?}");
let res = extract_rendered_symbols(&sym_doc, symbols.keys());
Ok(res)
}
fn extract_rendered_symbols<'a>(
doc: &TypstPagedDocument,
symbols: impl Iterator<Item = &'a String>,
) -> HashMap<String, (TypstFont, GlyphId)> {
use typst::layout::Frame;
use typst::layout::FrameItem;
struct Worker {
res: HashMap<String, (TypstFont, GlyphId)>,
}
impl Worker {
fn work<'a>(
&mut self,
paged_doc: &TypstPagedDocument,
symbols: impl Iterator<Item = &'a String>,
) {
for (pg, s) in paged_doc.pages.iter().zip(symbols) {
self.work_frame(&pg.frame, s);
}
}
fn work_frame(&mut self, k: &Frame, active: &str) {
for (_, item) in k.items() {
let text = match item {
FrameItem::Group(g) => {
self.work_frame(&g.frame, active);
continue;
}
FrameItem::Text(text) => text,
_ => 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})", chc = ch as u32);
self.res
.insert(active.to_owned(), (font.clone(), GlyphId(g.id)));
}
}
}
}
let mut worker = Worker {
res: HashMap::new(),
};
worker.work(doc, symbols);
worker.res
}
fn render_glyphs(
symbols: &BTreeMap<String, SymbolItem>,
glyph_mapping: &HashMap<String, (TypstFont, GlyphId)>,
) -> LspResult<Vec<ResourceSymbolItem>> {
let glyph_provider = reflexo_vec2svg::GlyphProvider::default();
let glyph_pass = reflexo_typst::vector::pass::ConvertInnerImpl::new(glyph_provider, false);
let mut builder = SvgGlyphBuilder::new();
let mut render_sym = |u| {
let (font, id) = glyph_mapping.get(u)?.clone();
let glyph = glyph_pass.must_flat_glyph(&GlyphItem::Raw(font.clone(), id))?;
let rendered = builder.render_glyph("", &glyph)?; // the glyph_id does not matter here
Some(create_display_svg(&font, id, &rendered))
};
let rendered_symbols = symbols
.iter()
.map(|(k, v)| ResourceSymbolItem {
id: k.clone(),
category: v.category,
unicode: v.unicode,
glyph: render_sym(k),
})
.collect();
Ok(rendered_symbols)
}
fn create_display_svg(font: &TypstFont, gid: GlyphId, svg_path: &str) -> String {
let face = font.ttf();
let (x_min, x_max) = face
.glyph_bounding_box(gid)
.map(|bbox| (bbox.x_min as f32, bbox.x_max as f32))
.unwrap_or_default();
// Font-wide metrics
let units_per_em = font.metrics().units_per_em as f32;
let ascender = font.metrics().ascender.get() as f32 * units_per_em;
let descender = font.metrics().descender.get() as f32 * units_per_em; // usually negative
// Horizontal advance (fallback to em)
let x_advance = face
.glyph_hor_advance(gid)
.map(f32::from)
.unwrap_or(units_per_em);
// Start viewBox.x at left-most ink or 0, whichever is smaller (to include left overhang)
let view_x = x_min.min(0.0);
// Start view width as the advance; enlarge if ink extends past that
let view_w = x_advance.max(x_max - view_x);
// Vertical viewBox uses font ascender/descent so baseline is at y=0
let view_y = -ascender;
let view_h = ascender - descender; // ascender - (negative descender) -> total height
let svg_content = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{view_x} {view_y} {view_w} {view_h}" preserveAspectRatio="xMidYMid meet">
<g transform="scale(1 -1)">{svg_path}</g>
</svg>"#
);
svg_content
}

View file

@ -38,7 +38,7 @@ export const SymbolCell = (sym: SymbolItem) => {
const fallback = () => {
const key = stripSymPrefix(sym.id);
return span(NOPRINT_SYMBOLS[key] ?? key);
return span({ class: "symbol-glyph" }, NOPRINT_SYMBOLS[key] ?? key);
};
const symbolName = stripSymPrefix(sym.id);
@ -50,7 +50,7 @@ export const SymbolCell = (sym: SymbolItem) => {
title: `Click to insert: ${symbolName}`,
onclick: handleClick,
},
div({ class: "symbol-glyph" }, sym.rendered ?? fallback()),
(sym.glyph && div({ class: "symbol-glyph", innerHTML: sym.glyph })) ?? fallback(),
div(
{ class: "symbol-details" },
div(

View file

@ -6,7 +6,6 @@ import { CanvasPanel } from "./components/canvas-panel";
import { SymbolPicker } from "./components/symbol-picker";
import { SearchBar, ViewModeToggle } from "./components/toolbox";
import { useDetypifyFilter } from "./detypify-filter";
import { prerenderSymbols } from "./render";
import { useSymbolSearch } from "./search-filter";
import type { SymbolItem, SymbolResource } from "./symbols";
@ -18,15 +17,14 @@ function useSymbolResource() {
const symbols = van.state<SymbolItem[]>(
symbolInformationData.startsWith(":")
? []
: prerenderSymbols(JSON.parse(base64Decode(symbolInformationData))),
: (JSON.parse(base64Decode(symbolInformationData)) as SymbolResource).symbols,
);
if (import.meta.env.DEV) {
// Dynamically import mock data in development mode if no real data is present
import("./mock-data.json").then((json) => {
const symbolResource = json as SymbolResource;
symbols.val = prerenderSymbols(symbolResource);
console.log("symbols", symbols.val);
symbols.val = symbolResource.symbols;
});
}

View file

@ -1,148 +1,117 @@
{
"fontSelects": [
"symbols": [
{
"ascender": 0.72802734375,
"capHeight": 0.7001953125,
"descender": -0.21044921875,
"family": "Segoe UI Symbol",
"unitsPerEm": 2048
},
{
"ascender": 0.8059999942779541,
"capHeight": 0.6830000281333923,
"descender": -0.1940000057220459,
"family": "New Computer Modern Math",
"unitsPerEm": 1000
}
],
"glyphDefs": {
"gAQAAAHU": "<path id=\"gAQAAAHU\" class=\"outline_glyph\" d=\"M 336 698 C 324 698 313 692 304 680 L 187 531 L 208 509 L 356 628 C 368 637 374 648 374 660 C 374 680 356 698 336 698 Z \"/>",
"gAQAAABQD": "<path id=\"gAQAAABQD\" class=\"outline_glyph\" d=\"M 377 536 C 432 565 484 604 534 651 L 519 673 C 480 636 446 618 419 618 C 406 618 389 623 368 633 C 295 668 244 686 215 686 C 156 686 109 650 76 578 C 58 540 42 502 27 464 L 51 449 C 69 499 91 537 117 564 C 137 584 162 594 193 594 C 210 594 228 590 248 583 Z M 462 510 C 475 533 494 555 517 576 L 498 587 C 452 558 405 517 357 463 C 356 448 355 436 355 428 C 355 407 358 375 363 333 C 370 282 373 240 373 208 C 373 148 356 82 330 56 C 310 35 289 25 266 25 C 241 25 219 34 201 53 C 181 86 166 114 155 137 L 128 109 C 87 67 57 32 37 4 L 45 -19 L 99 19 C 134 -12 165 -27 193 -27 C 268 -27 339 7 406 74 C 445 114 465 171 465 246 C 465 293 461 344 453 399 C 451 412 450 434 450 463 C 450 482 454 497 462 510 Z \"/>",
"gAQAAAGw": "<path id=\"gAQAAAGw\" class=\"outline_glyph\" d=\"M 430 0 C 439 0 444 5 444 14 L 444 16 C 425 96 395 171 353 242 C 396 313 426 388 444 467 L 444 469 C 444 478 439 483 430 483 L 428 483 C 421 483 417 480 416 474 C 386 388 340 314 278 252 C 276 250 275 247 274 242 C 274 237 276 233 279 232 C 341 169 387 95 416 10 C 418 3 422 0 428 0 Z M 267 0 C 276 0 281 5 281 14 L 281 16 C 262 96 232 171 190 242 C 233 313 263 388 281 467 L 281 469 C 281 478 276 483 267 483 L 265 483 C 259 483 255 480 253 474 C 223 389 177 315 115 252 C 113 250 112 247 111 242 C 111 237 113 233 116 232 C 178 169 224 95 253 10 C 255 3 259 0 265 0 Z \"/>"
},
"symbols": {
"sym.acute": {
"category": "accent",
"glyphs": [
{
"fontIndex": 1,
"name": "acute",
"shape": "gAQAAAHU",
"xAdvance": 500,
"xMax": 374,
"xMin": 187,
"yAdvance": null,
"yMax": 698,
"yMin": 509
}
],
"glyph": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -806 500 1000\" preserveAspectRatio=\"xMidYMid meet\">\n<g transform=\"scale(1 -1)\"><path id=\"\" class=\"outline_glyph\" d=\"M 336 698 C 324 698 313 692 304 680 L 187 531 L 208 509 L 356 628 C 368 637 374 648 374 660 C 374 680 356 698 336 698 Z \"/></g>\n</svg>",
"id": "sym.acute",
"unicode": 180
},
"sym.Im": {
{
"category": "misc",
"glyphs": [
{
"fontIndex": 1,
"name": "uni2111",
"shape": "gAQAAABQD",
"xAdvance": 554,
"xMax": 534,
"xMin": 27,
"yAdvance": null,
"yMax": 686,
"yMin": -27
}
],
"glyph": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -806 554 1000\" preserveAspectRatio=\"xMidYMid meet\">\n<g transform=\"scale(1 -1)\"><path id=\"\" class=\"outline_glyph\" d=\"M 377 536 C 432 565 484 604 534 651 L 519 673 C 480 636 446 618 419 618 C 406 618 389 623 368 633 C 295 668 244 686 215 686 C 156 686 109 650 76 578 C 58 540 42 502 27 464 L 51 449 C 69 499 91 537 117 564 C 137 584 162 594 193 594 C 210 594 228 590 248 583 Z M 462 510 C 475 533 494 555 517 576 L 498 587 C 452 558 405 517 357 463 C 356 448 355 436 355 428 C 355 407 358 375 363 333 C 370 282 373 240 373 208 C 373 148 356 82 330 56 C 310 35 289 25 266 25 C 241 25 219 34 201 53 C 181 86 166 114 155 137 L 128 109 C 87 67 57 32 37 4 L 45 -19 L 99 19 C 134 -12 165 -27 193 -27 C 268 -27 339 7 406 74 C 445 114 465 171 465 246 C 465 293 461 344 453 399 C 451 412 450 434 450 463 C 450 482 454 497 462 510 Z \"/></g>\n</svg>",
"id": "sym.Im",
"unicode": 8465
},
"sym.quote.angle.l.double": {
{
"category": "quote",
"glyphs": [
{
"fontIndex": 1,
"name": "guillemotleft",
"shape": "gAQAAAGw",
"xAdvance": 556,
"xMax": 444,
"xMin": 111,
"yAdvance": null,
"yMax": 483,
"yMin": 0
}
],
"glyph": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -806 556 1000\" preserveAspectRatio=\"xMidYMid meet\">\n<g transform=\"scale(1 -1)\"><path id=\"\" class=\"outline_glyph\" d=\"M 430 0 C 439 0 444 5 444 14 L 444 16 C 425 96 395 171 353 242 C 396 313 426 388 444 467 L 444 469 C 444 478 439 483 430 483 L 428 483 C 421 483 417 480 416 474 C 386 388 340 314 278 252 C 276 250 275 247 274 242 C 274 237 276 233 279 232 C 341 169 387 95 416 10 C 418 3 422 0 428 0 Z M 267 0 C 276 0 281 5 281 14 L 281 16 C 262 96 232 171 190 242 C 233 313 263 388 281 467 L 281 469 C 281 478 276 483 267 483 L 265 483 C 259 483 255 480 253 474 C 223 389 177 315 115 252 C 113 250 112 247 111 242 C 111 237 113 233 116 232 C 178 169 224 95 253 10 C 255 3 259 0 265 0 Z \"/></g>\n</svg>",
"id": "sym.quote.angle.l.double",
"unicode": 171
},
"sym.lrm": {
{
"category": "control",
"glyphs": [],
"id": "sym.lrm",
"unicode": 8206
},
"sym.rlm": {
{
"category": "control",
"glyphs": [],
"id": "sym.rlm",
"unicode": 8207
},
"sym.wj": {
{
"category": "control",
"glyphs": [],
"id": "sym.wj",
"unicode": 8288
},
"sym.zwj": {
{
"category": "control",
"glyphs": [],
"id": "sym.zwj",
"unicode": 8205
},
"sym.zwnj": {
{
"category": "control",
"glyphs": [],
"id": "sym.zwnj",
"unicode": 8204
},
"sym.zws": {
{
"category": "control",
"glyphs": [],
"id": "sym.zws",
"unicode": 8203
},
"sym.space": {
{
"category": "space",
"glyphs": [],
"id": "sym.space",
"unicode": 32
},
"sym.space.en": {
{
"category": "space",
"glyphs": [],
"id": "sym.space.en",
"unicode": 8194
},
"sym.space.med": {
{
"category": "space",
"glyphs": [],
"id": "sym.space.fig",
"unicode": 8199
},
{
"category": "space",
"id": "sym.space.hair",
"unicode": 8202
},
{
"category": "space",
"id": "sym.space.med",
"unicode": 8287
},
"sym.space.nobreak": {
{
"category": "space",
"glyphs": [],
"id": "sym.space.nobreak",
"unicode": 160
},
"sym.space.nobreak.narrow": {
{
"category": "space",
"glyphs": [],
"id": "sym.space.nobreak.narrow",
"unicode": 8239
},
"sym.space.quad": {
{
"category": "space",
"glyphs": [],
"id": "sym.space.punct",
"unicode": 8200
},
{
"category": "space",
"id": "sym.space.quad",
"unicode": 8195
},
"sym.space.quarter": {
{
"category": "space",
"glyphs": [],
"id": "sym.space.quarter",
"unicode": 8197
},
"sym.space.thin": {
{
"category": "space",
"glyphs": [],
"id": "sym.space.sixth",
"unicode": 8198
},
{
"category": "space",
"id": "sym.space.thin",
"unicode": 8201
},
{
"category": "space",
"id": "sym.space.third",
"unicode": 8196
}
}
]
}

View file

@ -1,64 +0,0 @@
import van from "vanjs-core";
import type { FontItem, GlyphDesc, SymbolItem, SymbolResource } from "./symbols";
const { div } = van.tags;
function renderSymbol(
mask: HTMLElement,
primaryGlyph: GlyphDesc,
fontSelected: FontItem,
path: string,
) {
const diff = (min?: number | null, max?: number | null) => {
return Math.abs((max ?? 0) - (min ?? 0));
};
const bboxXWidth = diff(primaryGlyph.xMin, primaryGlyph.xMax);
const xWidth = Math.max(bboxXWidth, primaryGlyph.xAdvance ?? fontSelected.unitsPerEm);
const yReal = diff(primaryGlyph.yMin, primaryGlyph.yMax);
const yGlobal = primaryGlyph.yAdvance ?? fontSelected.unitsPerEm;
const yWidth = Math.max(yReal, yGlobal);
// keep viewBox in glyph units
const viewBox = `0 0 ${xWidth} ${yWidth}`;
const yShift =
yReal >= yGlobal
? Math.abs(primaryGlyph.yMax ?? 0)
: (Math.abs(primaryGlyph.yMax ?? 0) + yWidth) / 2;
// Center the symbol horizontally
const xShift = -(primaryGlyph.xMin ?? 0) + (xWidth - bboxXWidth) / 2;
const imageData = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" preserveAspectRatio="xMidYMid meet">
<g transform="translate(${xShift}, ${yShift}) scale(1, -1)">${path}</g>
</svg>`;
mask.style.maskImage = `url('data:image/svg+xml;utf8,${encodeURIComponent(imageData)}')`;
return mask;
}
export function prerenderSymbols(symRes: SymbolResource): SymbolItem[] {
return Object.entries(symRes.symbols).map(([id, sym]) => {
const primaryGlyph = sym.glyphs[0];
const mask = div();
const renderedSym: SymbolItem = {
id,
category: sym.category,
unicode: sym.unicode,
rendered: primaryGlyph ? mask : undefined,
};
if (primaryGlyph?.fontIndex && primaryGlyph?.shape) {
const fontSelected = symRes.fontSelects[primaryGlyph.fontIndex];
if (fontSelected) {
const glyphPath = (primaryGlyph.shape && symRes.glyphDefs[primaryGlyph.shape]) ?? "";
setTimeout(() => {
renderSymbol(mask, sym.glyphs[0], fontSelected, glyphPath);
});
}
}
return renderedSym;
});
}

View file

@ -25,8 +25,8 @@
flex-direction: column;
justify-content: center;
align-items: center;
width: 2rem;
height: 2rem;
width: 2.5rem;
height: 2.5rem;
border: 1px solid transparent;
border-radius: 0.25rem;
background: var(--vscode-list-inactiveSelectionBackground, rgba(201, 209, 217, 0.15));
@ -35,6 +35,10 @@
transition: all 0.2s ease;
}
.symbol-cell:hover {
overflow: visible
}
.symbol-picker.detailed .symbol-cell {
width: 4rem;
height: 4rem;
@ -62,21 +66,28 @@
}
/* Symbol glyph mask */
.symbol-glyph > div {
width: 1.75rem;
height: 1.75rem;
mask-repeat: no-repeat;
mask-position: center;
background-color: currentColor;
div.symbol-glyph svg {
width: 2rem;
height: 2rem;
fill: currentColor;
transition: fill 200ms;
}
.symbol-cell:hover div.symbol-glyph svg {
fill: var(--vscode-button-hoverBackground, #1177bb);
}
/* Symbol glyph fallback text */
.symbol-glyph > span {
span.symbol-glyph {
font-size: 0.75rem;
font-style: italic;
font-weight: 500;
}
.symbol-cell:hover span.symbol-glyph {
color: var(--vscode-button-hoverBackground, #1177bb);
}
/* Symbol details */
.symbol-details {
display: flex;

View file

@ -33,7 +33,7 @@ export const CATEGORY_NAMES = {
export type SymbolCategory = keyof typeof CATEGORY_NAMES;
export const NOPRINT_SYMBOLS: { [key: string]: string } = {
export const NOPRINT_SYMBOLS: Record<string, string> = {
space: "␣",
"space.en": "ensp",
"space.quad": "emsp",
@ -50,45 +50,17 @@ export const NOPRINT_SYMBOLS: { [key: string]: string } = {
zws: "zwsp",
};
export interface GlyphDesc {
fontIndex: number | null;
xAdvance?: number | null;
yAdvance?: number | null;
xMin?: number | null;
yMin?: number | null;
xMax?: number | null;
yMax?: number | null;
name?: string | null;
shape?: string | null;
}
export type SymbolId = string;
export interface SymbolItem {
id: SymbolId;
category: SymbolCategory;
unicode: number;
rendered?: HTMLElement;
}
export interface FontItem {
family: string;
capHeight: number;
ascender: number;
descender: number;
unitsPerEm: number;
}
export interface RawSymbolItem {
category: SymbolCategory;
unicode: number;
glyphs: GlyphDesc[];
glyph?: string;
}
export interface SymbolResource {
symbols: Record<string, RawSymbolItem>;
fontSelects: FontItem[];
glyphDefs: Record<string, string>;
symbols: SymbolItem[];
}
export function stripSymPrefix(name: string): string {