restty 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/app/font-sources.d.ts +1 -1
- package/dist/app/types.d.ts +6 -0
- package/dist/{chunk-53vdvhe3.js → chunk-ym658zhj.js} +375 -42
- package/dist/fonts/manager.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/restty.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://github.com/wiedymi/restty/actions/workflows/ci.yml)
|
|
6
6
|
[](https://github.com/wiedymi/restty/actions/workflows/publish.yml)
|
|
7
7
|
[](https://restty.pages.dev/)
|
|
8
|
+
|
|
8
9
|
[](https://github.com/wiedymi)
|
|
9
10
|
[](https://x.com/wiedymi)
|
|
10
11
|
[](mailto:contact@wiedymi.com)
|
|
@@ -101,7 +102,7 @@ restty.copySelectionToClipboard();
|
|
|
101
102
|
|
|
102
103
|
### Provide custom fonts
|
|
103
104
|
|
|
104
|
-
By default, restty uses a
|
|
105
|
+
By default, restty uses a local-first font preset with CDN fallback. To fully control fonts, disable the preset and pass `fontSources`.
|
|
105
106
|
|
|
106
107
|
```ts
|
|
107
108
|
const restty = new Restty({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResttyFontPreset, ResttyFontSource } from "./types";
|
|
2
|
-
/**
|
|
2
|
+
/** Local-first default font fallback chain with CDN fallback for JetBrains Mono, Nerd symbols, emoji, and CJK support. */
|
|
3
3
|
export declare const DEFAULT_FONT_SOURCES: ResttyFontSource[];
|
|
4
4
|
/** Validates user-provided font sources or returns defaults based on preset (none returns empty array, otherwise default CDN fonts). */
|
|
5
5
|
export declare function normalizeFontSources(sources: ResttyFontSource[] | undefined, preset: ResttyFontPreset | undefined): ResttyFontSource[];
|
package/dist/app/types.d.ts
CHANGED
|
@@ -154,6 +154,12 @@ export type ResttyAppOptions = {
|
|
|
154
154
|
renderer?: "auto" | "webgpu" | "webgl2";
|
|
155
155
|
/** Font size in CSS pixels. */
|
|
156
156
|
fontSize?: number;
|
|
157
|
+
/**
|
|
158
|
+
* Font sizing mode used by text-shaper scale resolution.
|
|
159
|
+
* - em: interpret fontSize as EM size
|
|
160
|
+
* - height: interpret fontSize as full font height (ascender-descender-lineGap)
|
|
161
|
+
*/
|
|
162
|
+
fontSizeMode?: "em" | "height";
|
|
157
163
|
/**
|
|
158
164
|
* Alpha blending strategy.
|
|
159
165
|
* - native: GPU-native premultiplied alpha
|
|
@@ -9095,6 +9095,37 @@ function isLikelyEmojiCodepoint(cp) {
|
|
|
9095
9095
|
return true;
|
|
9096
9096
|
return false;
|
|
9097
9097
|
}
|
|
9098
|
+
function isVariationSelectorCodepoint(cp) {
|
|
9099
|
+
if (cp >= 65024 && cp <= 65039)
|
|
9100
|
+
return true;
|
|
9101
|
+
if (cp >= 917760 && cp <= 917999)
|
|
9102
|
+
return true;
|
|
9103
|
+
return false;
|
|
9104
|
+
}
|
|
9105
|
+
function isCombiningMarkCodepoint(cp) {
|
|
9106
|
+
if (cp >= 768 && cp <= 879)
|
|
9107
|
+
return true;
|
|
9108
|
+
if (cp >= 6832 && cp <= 6911)
|
|
9109
|
+
return true;
|
|
9110
|
+
if (cp >= 7616 && cp <= 7679)
|
|
9111
|
+
return true;
|
|
9112
|
+
if (cp >= 8400 && cp <= 8447)
|
|
9113
|
+
return true;
|
|
9114
|
+
if (cp >= 65056 && cp <= 65071)
|
|
9115
|
+
return true;
|
|
9116
|
+
return false;
|
|
9117
|
+
}
|
|
9118
|
+
function isCoverageIgnorableCodepoint(cp) {
|
|
9119
|
+
if (cp === 8204 || cp === 8205)
|
|
9120
|
+
return true;
|
|
9121
|
+
if (isVariationSelectorCodepoint(cp))
|
|
9122
|
+
return true;
|
|
9123
|
+
if (isCombiningMarkCodepoint(cp))
|
|
9124
|
+
return true;
|
|
9125
|
+
if (cp >= 917536 && cp <= 917631)
|
|
9126
|
+
return true;
|
|
9127
|
+
return false;
|
|
9128
|
+
}
|
|
9098
9129
|
function resolvePresentationPreference(text, chars) {
|
|
9099
9130
|
if (text.includes("️"))
|
|
9100
9131
|
return "emoji";
|
|
@@ -9109,7 +9140,7 @@ function resolvePresentationPreference(text, chars) {
|
|
|
9109
9140
|
}
|
|
9110
9141
|
return "auto";
|
|
9111
9142
|
}
|
|
9112
|
-
function pickFontIndexForText(state, text, expectedSpan
|
|
9143
|
+
function pickFontIndexForText(state, text, expectedSpan) {
|
|
9113
9144
|
if (!state.fonts.length)
|
|
9114
9145
|
return 0;
|
|
9115
9146
|
const cacheKey = `${expectedSpan}:${text}`;
|
|
@@ -9117,6 +9148,10 @@ function pickFontIndexForText(state, text, expectedSpan, shapeClusterWithFont) {
|
|
|
9117
9148
|
if (cached !== undefined)
|
|
9118
9149
|
return cached;
|
|
9119
9150
|
const chars = Array.from(text);
|
|
9151
|
+
const requiredChars = chars.filter((ch) => {
|
|
9152
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
9153
|
+
return !isCoverageIgnorableCodepoint(cp);
|
|
9154
|
+
});
|
|
9120
9155
|
const firstCp = text.codePointAt(0) ?? 0;
|
|
9121
9156
|
const nerdSymbol = isNerdSymbolCodepoint(firstCp);
|
|
9122
9157
|
const presentation = resolvePresentationPreference(text, chars);
|
|
@@ -9128,7 +9163,7 @@ function pickFontIndexForText(state, text, expectedSpan, shapeClusterWithFont) {
|
|
|
9128
9163
|
if (predicate && !predicate(entry))
|
|
9129
9164
|
continue;
|
|
9130
9165
|
let ok = true;
|
|
9131
|
-
for (const ch of
|
|
9166
|
+
for (const ch of requiredChars) {
|
|
9132
9167
|
if (!fontHasGlyph(entry.font, ch)) {
|
|
9133
9168
|
ok = false;
|
|
9134
9169
|
break;
|
|
@@ -26598,7 +26633,7 @@ function buildFontAtlasIfNeeded(params) {
|
|
|
26598
26633
|
if (union.size === 0) {
|
|
26599
26634
|
return { rebuilt: false, atlas: null, rgba: null, preferNearest: false };
|
|
26600
26635
|
}
|
|
26601
|
-
const useHinting =
|
|
26636
|
+
const useHinting = false;
|
|
26602
26637
|
const atlasPadding = isSymbol ? Math.max(constants.atlasPadding, constants.symbolAtlasPadding) : constants.atlasPadding;
|
|
26603
26638
|
const atlasMaxSize = isSymbol ? constants.symbolAtlasMaxSize : constants.defaultAtlasMaxSize;
|
|
26604
26639
|
const glyphPixelMode = resolveGlyphPixelMode(entry);
|
|
@@ -26690,9 +26725,79 @@ function buildFontAtlasIfNeeded(params) {
|
|
|
26690
26725
|
|
|
26691
26726
|
// src/app/font-sources.ts
|
|
26692
26727
|
var DEFAULT_FONT_SOURCES = [
|
|
26728
|
+
{
|
|
26729
|
+
type: "local",
|
|
26730
|
+
matchers: [
|
|
26731
|
+
"jetbrainsmono nerd font",
|
|
26732
|
+
"jetbrains mono nerd font",
|
|
26733
|
+
"jetbrains mono nl nerd font mono",
|
|
26734
|
+
"jetbrains mono",
|
|
26735
|
+
"jetbrainsmono"
|
|
26736
|
+
],
|
|
26737
|
+
label: "JetBrains Mono Nerd Font Regular (Local)"
|
|
26738
|
+
},
|
|
26739
|
+
{
|
|
26740
|
+
type: "local",
|
|
26741
|
+
matchers: [
|
|
26742
|
+
"jetbrainsmono nerd font bold",
|
|
26743
|
+
"jetbrains mono nerd font bold",
|
|
26744
|
+
"jetbrains mono nl nerd font mono bold",
|
|
26745
|
+
"jetbrains mono bold",
|
|
26746
|
+
"jetbrainsmono bold"
|
|
26747
|
+
],
|
|
26748
|
+
label: "JetBrains Mono Nerd Font Bold (Local)"
|
|
26749
|
+
},
|
|
26750
|
+
{
|
|
26751
|
+
type: "local",
|
|
26752
|
+
matchers: [
|
|
26753
|
+
"jetbrainsmono nerd font italic",
|
|
26754
|
+
"jetbrains mono nerd font italic",
|
|
26755
|
+
"jetbrains mono nl nerd font mono italic",
|
|
26756
|
+
"jetbrains mono italic",
|
|
26757
|
+
"jetbrainsmono italic"
|
|
26758
|
+
],
|
|
26759
|
+
label: "JetBrains Mono Nerd Font Italic (Local)"
|
|
26760
|
+
},
|
|
26761
|
+
{
|
|
26762
|
+
type: "local",
|
|
26763
|
+
matchers: [
|
|
26764
|
+
"jetbrainsmono nerd font bold italic",
|
|
26765
|
+
"jetbrains mono nerd font bold italic",
|
|
26766
|
+
"jetbrains mono nl nerd font mono bold italic",
|
|
26767
|
+
"jetbrains mono bold italic",
|
|
26768
|
+
"jetbrainsmono bold italic"
|
|
26769
|
+
],
|
|
26770
|
+
label: "JetBrains Mono Nerd Font Bold Italic (Local)"
|
|
26771
|
+
},
|
|
26772
|
+
{
|
|
26773
|
+
type: "url",
|
|
26774
|
+
url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/Regular/JetBrainsMonoNLNerdFontMono-Regular.ttf",
|
|
26775
|
+
label: "JetBrains Mono Nerd Font Regular"
|
|
26776
|
+
},
|
|
26777
|
+
{
|
|
26778
|
+
type: "url",
|
|
26779
|
+
url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/Bold/JetBrainsMonoNLNerdFontMono-Bold.ttf",
|
|
26780
|
+
label: "JetBrains Mono Nerd Font Bold"
|
|
26781
|
+
},
|
|
26693
26782
|
{
|
|
26694
26783
|
type: "url",
|
|
26695
|
-
url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/
|
|
26784
|
+
url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/Italic/JetBrainsMonoNLNerdFontMono-Italic.ttf",
|
|
26785
|
+
label: "JetBrains Mono Nerd Font Italic"
|
|
26786
|
+
},
|
|
26787
|
+
{
|
|
26788
|
+
type: "url",
|
|
26789
|
+
url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/BoldItalic/JetBrainsMonoNLNerdFontMono-BoldItalic.ttf",
|
|
26790
|
+
label: "JetBrains Mono Nerd Font Bold Italic"
|
|
26791
|
+
},
|
|
26792
|
+
{
|
|
26793
|
+
type: "local",
|
|
26794
|
+
matchers: [
|
|
26795
|
+
"symbols nerd font mono",
|
|
26796
|
+
"symbols nerd font",
|
|
26797
|
+
"nerd fonts symbols",
|
|
26798
|
+
"nerdfontssymbolsonly"
|
|
26799
|
+
],
|
|
26800
|
+
label: "Symbols Nerd Font (Local)"
|
|
26696
26801
|
},
|
|
26697
26802
|
{
|
|
26698
26803
|
type: "url",
|
|
@@ -50195,6 +50300,12 @@ var DEFAULT_SYMBOL_CONSTRAINT = {
|
|
|
50195
50300
|
align_vertical: "center",
|
|
50196
50301
|
max_constraint_width: 1
|
|
50197
50302
|
};
|
|
50303
|
+
var DEFAULT_APPLE_SYMBOLS_CONSTRAINT = {
|
|
50304
|
+
size: "cover",
|
|
50305
|
+
align_horizontal: "center",
|
|
50306
|
+
align_vertical: "center",
|
|
50307
|
+
max_constraint_width: 1
|
|
50308
|
+
};
|
|
50198
50309
|
var DEFAULT_EMOJI_CONSTRAINT = {
|
|
50199
50310
|
size: "cover",
|
|
50200
50311
|
align_horizontal: "center",
|
|
@@ -50305,6 +50416,7 @@ function createResttyApp(options) {
|
|
|
50305
50416
|
const OVERLAY_SCROLLBAR_MARGIN_CSS_PX = 4;
|
|
50306
50417
|
const OVERLAY_SCROLLBAR_INSET_Y_CSS_PX = 2;
|
|
50307
50418
|
const OVERLAY_SCROLLBAR_MIN_THUMB_CSS_PX = 28;
|
|
50419
|
+
const OVERLAY_SCROLLBAR_CAP_SUPERSAMPLE = 8;
|
|
50308
50420
|
let paused = false;
|
|
50309
50421
|
let backend = "none";
|
|
50310
50422
|
let preferredRenderer = options.renderer ?? "auto";
|
|
@@ -50627,24 +50739,63 @@ function createResttyApp(options) {
|
|
|
50627
50739
|
const y02 = Math.round(y);
|
|
50628
50740
|
const width = Math.max(1, Math.round(w));
|
|
50629
50741
|
const height = Math.max(1, Math.round(h));
|
|
50630
|
-
const radius = Math.min(
|
|
50742
|
+
const radius = Math.min(width * 0.5, height * 0.5);
|
|
50631
50743
|
if (radius <= 0) {
|
|
50632
50744
|
pushRectBox(out, x02, y02, width, height, color);
|
|
50633
50745
|
return;
|
|
50634
50746
|
}
|
|
50635
|
-
const
|
|
50747
|
+
const capRows = Math.min(height, Math.max(1, Math.ceil(radius)));
|
|
50748
|
+
const middleStart = capRows;
|
|
50749
|
+
const middleEnd = Math.max(middleStart, height - capRows);
|
|
50750
|
+
const middleH = middleEnd - middleStart;
|
|
50636
50751
|
if (middleH > 0) {
|
|
50637
|
-
pushRectBox(out, x02, y02 +
|
|
50752
|
+
pushRectBox(out, x02, y02 + middleStart, width, middleH, color);
|
|
50638
50753
|
}
|
|
50639
50754
|
const radiusSq = radius * radius;
|
|
50640
|
-
|
|
50641
|
-
|
|
50642
|
-
|
|
50643
|
-
|
|
50644
|
-
|
|
50645
|
-
|
|
50646
|
-
|
|
50647
|
-
|
|
50755
|
+
const centerX = width * 0.5;
|
|
50756
|
+
const topCenterY = radius;
|
|
50757
|
+
const bottomCenterY = height - radius;
|
|
50758
|
+
const samplesPerAxis = Math.max(1, OVERLAY_SCROLLBAR_CAP_SUPERSAMPLE | 0);
|
|
50759
|
+
const totalSamples = samplesPerAxis * samplesPerAxis;
|
|
50760
|
+
const invSamples = 1 / totalSamples;
|
|
50761
|
+
const alphaBase = color[3];
|
|
50762
|
+
const alphaEpsilon = 1 / 255;
|
|
50763
|
+
const sampleCapPixelCoverage = (localX, localY, centerY) => {
|
|
50764
|
+
let hits = 0;
|
|
50765
|
+
for (let sy = 0;sy < samplesPerAxis; sy += 1) {
|
|
50766
|
+
const sampleY = localY + (sy + 0.5) / samplesPerAxis;
|
|
50767
|
+
for (let sx = 0;sx < samplesPerAxis; sx += 1) {
|
|
50768
|
+
const sampleX = localX + (sx + 0.5) / samplesPerAxis;
|
|
50769
|
+
const dx = sampleX - centerX;
|
|
50770
|
+
const dy = sampleY - centerY;
|
|
50771
|
+
if (dx * dx + dy * dy <= radiusSq)
|
|
50772
|
+
hits += 1;
|
|
50773
|
+
}
|
|
50774
|
+
}
|
|
50775
|
+
return hits * invSamples;
|
|
50776
|
+
};
|
|
50777
|
+
for (let row = 0;row < capRows; row += 1) {
|
|
50778
|
+
const topY = y02 + row;
|
|
50779
|
+
const bottomY = y02 + height - 1 - row;
|
|
50780
|
+
for (let col = 0;col < width; col += 1) {
|
|
50781
|
+
const coverageTop = sampleCapPixelCoverage(col, row, topCenterY);
|
|
50782
|
+
if (coverageTop > 0) {
|
|
50783
|
+
const alpha = alphaBase * coverageTop;
|
|
50784
|
+
if (alpha > alphaEpsilon) {
|
|
50785
|
+
out.push(x02 + col, topY, 1, 1, color[0], color[1], color[2], alpha);
|
|
50786
|
+
}
|
|
50787
|
+
}
|
|
50788
|
+
if (bottomY !== topY) {
|
|
50789
|
+
const localBottomY = height - 1 - row;
|
|
50790
|
+
const coverageBottom = sampleCapPixelCoverage(col, localBottomY, bottomCenterY);
|
|
50791
|
+
if (coverageBottom > 0) {
|
|
50792
|
+
const alpha = alphaBase * coverageBottom;
|
|
50793
|
+
if (alpha > alphaEpsilon) {
|
|
50794
|
+
out.push(x02 + col, bottomY, 1, 1, color[0], color[1], color[2], alpha);
|
|
50795
|
+
}
|
|
50796
|
+
}
|
|
50797
|
+
}
|
|
50798
|
+
}
|
|
50648
50799
|
}
|
|
50649
50800
|
}
|
|
50650
50801
|
function appendOverlayScrollbar(overlayData, total, offset, len) {
|
|
@@ -51833,7 +51984,7 @@ function createResttyApp(options) {
|
|
|
51833
51984
|
font: null,
|
|
51834
51985
|
fonts: [],
|
|
51835
51986
|
fontSizePx: 0,
|
|
51836
|
-
sizeMode: "height",
|
|
51987
|
+
sizeMode: options.fontSizeMode === "em" ? "em" : "height",
|
|
51837
51988
|
fontPickCache: new Map
|
|
51838
51989
|
};
|
|
51839
51990
|
const fontConfig = {
|
|
@@ -53312,6 +53463,33 @@ function createResttyApp(options) {
|
|
|
53312
53463
|
const normalizedMatchers = matchers.map((matcher) => matcher.toLowerCase()).filter(Boolean);
|
|
53313
53464
|
if (!normalizedMatchers.length)
|
|
53314
53465
|
return null;
|
|
53466
|
+
const detectStyleHint = (value) => {
|
|
53467
|
+
const text = value.toLowerCase();
|
|
53468
|
+
let weight = 400;
|
|
53469
|
+
if (/\b(thin|hairline)\b/.test(text))
|
|
53470
|
+
weight = 100;
|
|
53471
|
+
else if (/\b(extra[- ]?light|ultra[- ]?light)\b/.test(text))
|
|
53472
|
+
weight = 200;
|
|
53473
|
+
else if (/\blight\b/.test(text))
|
|
53474
|
+
weight = 300;
|
|
53475
|
+
else if (/\bmedium\b/.test(text))
|
|
53476
|
+
weight = 500;
|
|
53477
|
+
else if (/\b(semi[- ]?bold|demi[- ]?bold)\b/.test(text))
|
|
53478
|
+
weight = 600;
|
|
53479
|
+
else if (/\bbold\b/.test(text))
|
|
53480
|
+
weight = 700;
|
|
53481
|
+
else if (/\b(extra[- ]?bold|ultra[- ]?bold)\b/.test(text))
|
|
53482
|
+
weight = 800;
|
|
53483
|
+
else if (/\b(black|heavy)\b/.test(text))
|
|
53484
|
+
weight = 900;
|
|
53485
|
+
return {
|
|
53486
|
+
bold: /\b(bold|semi[- ]?bold|demi[- ]?bold|extra[- ]?bold|black|heavy)\b/.test(text),
|
|
53487
|
+
italic: /\b(italic|oblique)\b/.test(text),
|
|
53488
|
+
regular: /\b(regular|book|roman|normal)\b/.test(text),
|
|
53489
|
+
weight
|
|
53490
|
+
};
|
|
53491
|
+
};
|
|
53492
|
+
const sourceHint = detectStyleHint(`${label} ${normalizedMatchers.join(" ")}`);
|
|
53315
53493
|
const queryPermission = nav.permissions?.query;
|
|
53316
53494
|
if (queryPermission) {
|
|
53317
53495
|
try {
|
|
@@ -53323,13 +53501,49 @@ function createResttyApp(options) {
|
|
|
53323
53501
|
}
|
|
53324
53502
|
try {
|
|
53325
53503
|
const fonts = await queryLocalFonts();
|
|
53326
|
-
const
|
|
53504
|
+
const matches = fonts.filter((font) => {
|
|
53327
53505
|
const name = `${font.family ?? ""} ${font.fullName ?? ""} ${font.postscriptName ?? ""}`.toLowerCase();
|
|
53328
53506
|
return normalizedMatchers.some((matcher) => name.includes(matcher));
|
|
53329
53507
|
});
|
|
53330
|
-
if (
|
|
53508
|
+
if (matches.length) {
|
|
53509
|
+
const scoreMatch = (font) => {
|
|
53510
|
+
const name = `${font.family ?? ""} ${font.fullName ?? ""} ${font.postscriptName ?? ""}`.toLowerCase();
|
|
53511
|
+
const hint = detectStyleHint(name);
|
|
53512
|
+
let score = 0;
|
|
53513
|
+
for (let i3 = 0;i3 < normalizedMatchers.length; i3 += 1) {
|
|
53514
|
+
if (name.includes(normalizedMatchers[i3]))
|
|
53515
|
+
score += 8;
|
|
53516
|
+
}
|
|
53517
|
+
if (sourceHint.bold || sourceHint.italic) {
|
|
53518
|
+
score += sourceHint.bold === hint.bold ? 40 : -40;
|
|
53519
|
+
score += sourceHint.italic === hint.italic ? 40 : -40;
|
|
53520
|
+
} else {
|
|
53521
|
+
score += !hint.bold && !hint.italic ? 60 : -30;
|
|
53522
|
+
}
|
|
53523
|
+
const targetWeight = sourceHint.bold ? 700 : 400;
|
|
53524
|
+
score -= Math.abs((hint.weight ?? 400) - targetWeight) * 0.25;
|
|
53525
|
+
if (!sourceHint.bold && hint.weight === 400)
|
|
53526
|
+
score += 12;
|
|
53527
|
+
if (!sourceHint.bold && hint.weight < 350)
|
|
53528
|
+
score -= 12;
|
|
53529
|
+
if (!sourceHint.bold && hint.weight > 650)
|
|
53530
|
+
score -= 8;
|
|
53531
|
+
if (sourceHint.regular && !hint.bold && !hint.italic)
|
|
53532
|
+
score += 20;
|
|
53533
|
+
return score;
|
|
53534
|
+
};
|
|
53535
|
+
let match = matches[0];
|
|
53536
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
53537
|
+
for (let i3 = 0;i3 < matches.length; i3 += 1) {
|
|
53538
|
+
const candidate = matches[i3];
|
|
53539
|
+
const candidateScore = scoreMatch(candidate);
|
|
53540
|
+
if (candidateScore > bestScore) {
|
|
53541
|
+
bestScore = candidateScore;
|
|
53542
|
+
match = candidate;
|
|
53543
|
+
}
|
|
53544
|
+
}
|
|
53331
53545
|
const matchedName = `${match.family ?? ""} ${match.fullName ?? ""} ${match.postscriptName ?? ""}`.trim();
|
|
53332
|
-
console.log(`[font] local matched (${label}): ${matchedName || "unnamed"}`);
|
|
53546
|
+
console.log(`[font] local matched (${label}): ${matchedName || "unnamed"} score=${bestScore}`);
|
|
53333
53547
|
const blob = await match.blob();
|
|
53334
53548
|
return blob.arrayBuffer();
|
|
53335
53549
|
}
|
|
@@ -53536,6 +53750,51 @@ function createResttyApp(options) {
|
|
|
53536
53750
|
return true;
|
|
53537
53751
|
return false;
|
|
53538
53752
|
}
|
|
53753
|
+
function isVariationSelectorCodepoint2(cp) {
|
|
53754
|
+
if (cp >= 65024 && cp <= 65039)
|
|
53755
|
+
return true;
|
|
53756
|
+
if (cp >= 917760 && cp <= 917999)
|
|
53757
|
+
return true;
|
|
53758
|
+
return false;
|
|
53759
|
+
}
|
|
53760
|
+
function isCombiningMarkCodepoint2(cp) {
|
|
53761
|
+
if (cp >= 768 && cp <= 879)
|
|
53762
|
+
return true;
|
|
53763
|
+
if (cp >= 6832 && cp <= 6911)
|
|
53764
|
+
return true;
|
|
53765
|
+
if (cp >= 7616 && cp <= 7679)
|
|
53766
|
+
return true;
|
|
53767
|
+
if (cp >= 8400 && cp <= 8447)
|
|
53768
|
+
return true;
|
|
53769
|
+
if (cp >= 65056 && cp <= 65071)
|
|
53770
|
+
return true;
|
|
53771
|
+
return false;
|
|
53772
|
+
}
|
|
53773
|
+
function isEmojiModifierCodepoint(cp) {
|
|
53774
|
+
return cp >= 127995 && cp <= 127999;
|
|
53775
|
+
}
|
|
53776
|
+
function isCoverageIgnorableCodepoint2(cp) {
|
|
53777
|
+
if (cp === 8204 || cp === 8205)
|
|
53778
|
+
return true;
|
|
53779
|
+
if (isVariationSelectorCodepoint2(cp))
|
|
53780
|
+
return true;
|
|
53781
|
+
if (isCombiningMarkCodepoint2(cp))
|
|
53782
|
+
return true;
|
|
53783
|
+
if (cp >= 917536 && cp <= 917631)
|
|
53784
|
+
return true;
|
|
53785
|
+
return false;
|
|
53786
|
+
}
|
|
53787
|
+
function shouldMergeTrailingClusterCodepoint(cp) {
|
|
53788
|
+
if (cp === 8204 || cp === 8205)
|
|
53789
|
+
return true;
|
|
53790
|
+
if (isVariationSelectorCodepoint2(cp))
|
|
53791
|
+
return true;
|
|
53792
|
+
if (isCombiningMarkCodepoint2(cp))
|
|
53793
|
+
return true;
|
|
53794
|
+
if (isEmojiModifierCodepoint(cp))
|
|
53795
|
+
return true;
|
|
53796
|
+
return false;
|
|
53797
|
+
}
|
|
53539
53798
|
function resolvePresentationPreference2(text, chars) {
|
|
53540
53799
|
if (text.includes("️"))
|
|
53541
53800
|
return "emoji";
|
|
@@ -53550,18 +53809,33 @@ function createResttyApp(options) {
|
|
|
53550
53809
|
}
|
|
53551
53810
|
return "auto";
|
|
53552
53811
|
}
|
|
53553
|
-
function pickFontIndexForText2(text, expectedSpan = 1) {
|
|
53812
|
+
function pickFontIndexForText2(text, expectedSpan = 1, stylePreference = "regular") {
|
|
53554
53813
|
if (!fontState.fonts.length)
|
|
53555
53814
|
return 0;
|
|
53556
|
-
const cacheKey = `${expectedSpan}:${text}`;
|
|
53815
|
+
const cacheKey = `${expectedSpan}:${stylePreference}:${text}`;
|
|
53557
53816
|
const cached = fontState.fontPickCache.get(cacheKey);
|
|
53558
53817
|
if (cached !== undefined)
|
|
53559
53818
|
return cached;
|
|
53560
53819
|
const chars = Array.from(text);
|
|
53820
|
+
const requiredChars = chars.filter((ch) => {
|
|
53821
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
53822
|
+
return !isCoverageIgnorableCodepoint2(cp);
|
|
53823
|
+
});
|
|
53561
53824
|
const firstCp = text.codePointAt(0) ?? 0;
|
|
53562
53825
|
const nerdSymbol = isNerdSymbolCodepoint(firstCp);
|
|
53563
53826
|
const presentation = resolvePresentationPreference2(text, chars);
|
|
53564
|
-
const
|
|
53827
|
+
const styleHintsEnabled = stylePreference !== "regular" && presentation !== "emoji" && !nerdSymbol;
|
|
53828
|
+
const hasBoldHint = (entry) => /\bbold\b/i.test(entry.label ?? "");
|
|
53829
|
+
const hasItalicHint = (entry) => /\b(italic|oblique)\b/i.test(entry.label ?? "");
|
|
53830
|
+
const stylePredicates = stylePreference === "bold_italic" ? [
|
|
53831
|
+
(entry) => hasBoldHint(entry) && hasItalicHint(entry),
|
|
53832
|
+
(entry) => hasBoldHint(entry),
|
|
53833
|
+
(entry) => hasItalicHint(entry)
|
|
53834
|
+
] : stylePreference === "bold" ? [(entry) => hasBoldHint(entry) && !hasItalicHint(entry), (entry) => hasBoldHint(entry)] : stylePreference === "italic" ? [
|
|
53835
|
+
(entry) => hasItalicHint(entry) && !hasBoldHint(entry),
|
|
53836
|
+
(entry) => hasItalicHint(entry)
|
|
53837
|
+
] : [];
|
|
53838
|
+
const pickFirstMatch = (predicate, allowSequenceShapingFallback = false) => {
|
|
53565
53839
|
for (let i3 = 0;i3 < fontState.fonts.length; i3 += 1) {
|
|
53566
53840
|
const entry = fontState.fonts[i3];
|
|
53567
53841
|
if (!entry?.font)
|
|
@@ -53569,17 +53843,36 @@ function createResttyApp(options) {
|
|
|
53569
53843
|
if (predicate && !predicate(entry))
|
|
53570
53844
|
continue;
|
|
53571
53845
|
let ok = true;
|
|
53572
|
-
for (const ch of
|
|
53846
|
+
for (const ch of requiredChars) {
|
|
53573
53847
|
if (!fontHasGlyph2(entry.font, ch)) {
|
|
53574
53848
|
ok = false;
|
|
53575
53849
|
break;
|
|
53576
53850
|
}
|
|
53577
53851
|
}
|
|
53852
|
+
if (!ok && allowSequenceShapingFallback) {
|
|
53853
|
+
const shaped = shapeClusterWithFont(entry, text);
|
|
53854
|
+
ok = shaped.glyphs.some((glyph) => (glyph.glyphId ?? 0) !== 0);
|
|
53855
|
+
}
|
|
53578
53856
|
if (ok)
|
|
53579
53857
|
return i3;
|
|
53580
53858
|
}
|
|
53581
53859
|
return -1;
|
|
53582
53860
|
};
|
|
53861
|
+
const pickWithStyle = (predicate, allowSequenceShapingFallback = false) => {
|
|
53862
|
+
if (styleHintsEnabled) {
|
|
53863
|
+
for (let i3 = 0;i3 < stylePredicates.length; i3 += 1) {
|
|
53864
|
+
const stylePredicate = stylePredicates[i3];
|
|
53865
|
+
const styledIndex = pickFirstMatch((entry) => {
|
|
53866
|
+
if (!stylePredicate(entry))
|
|
53867
|
+
return false;
|
|
53868
|
+
return predicate ? !!predicate(entry) : true;
|
|
53869
|
+
}, allowSequenceShapingFallback);
|
|
53870
|
+
if (styledIndex >= 0)
|
|
53871
|
+
return styledIndex;
|
|
53872
|
+
}
|
|
53873
|
+
}
|
|
53874
|
+
return pickFirstMatch(predicate, allowSequenceShapingFallback);
|
|
53875
|
+
};
|
|
53583
53876
|
const tryIndex = (index) => {
|
|
53584
53877
|
if (index < 0)
|
|
53585
53878
|
return null;
|
|
@@ -53587,13 +53880,13 @@ function createResttyApp(options) {
|
|
|
53587
53880
|
return index;
|
|
53588
53881
|
};
|
|
53589
53882
|
if (nerdSymbol) {
|
|
53590
|
-
const symbolIndex =
|
|
53883
|
+
const symbolIndex = pickWithStyle((entry) => isNerdSymbolFont(entry) || isSymbolFont(entry));
|
|
53591
53884
|
const result = tryIndex(symbolIndex);
|
|
53592
53885
|
if (result !== null)
|
|
53593
53886
|
return result;
|
|
53594
53887
|
}
|
|
53595
53888
|
if (presentation === "emoji") {
|
|
53596
|
-
const emojiIndex = pickFirstMatch((entry) => isColorEmojiFont(entry));
|
|
53889
|
+
const emojiIndex = pickFirstMatch((entry) => isColorEmojiFont(entry), true);
|
|
53597
53890
|
const result = tryIndex(emojiIndex);
|
|
53598
53891
|
if (result !== null)
|
|
53599
53892
|
return result;
|
|
@@ -53603,7 +53896,7 @@ function createResttyApp(options) {
|
|
|
53603
53896
|
if (result !== null)
|
|
53604
53897
|
return result;
|
|
53605
53898
|
}
|
|
53606
|
-
const firstIndex =
|
|
53899
|
+
const firstIndex = pickWithStyle();
|
|
53607
53900
|
if (firstIndex >= 0) {
|
|
53608
53901
|
setBoundedMap(fontState.fontPickCache, cacheKey, firstIndex, FONT_PICK_CACHE_LIMIT);
|
|
53609
53902
|
return firstIndex;
|
|
@@ -53611,6 +53904,24 @@ function createResttyApp(options) {
|
|
|
53611
53904
|
setBoundedMap(fontState.fontPickCache, cacheKey, 0, FONT_PICK_CACHE_LIMIT);
|
|
53612
53905
|
return 0;
|
|
53613
53906
|
}
|
|
53907
|
+
function stylePreferenceFromFlags(bold, italic) {
|
|
53908
|
+
if (bold && italic)
|
|
53909
|
+
return "bold_italic";
|
|
53910
|
+
if (bold)
|
|
53911
|
+
return "bold";
|
|
53912
|
+
if (italic)
|
|
53913
|
+
return "italic";
|
|
53914
|
+
return "regular";
|
|
53915
|
+
}
|
|
53916
|
+
function isAppleSymbolsFont(entry) {
|
|
53917
|
+
return !!entry && /\bapple symbols\b/i.test(entry.label ?? "");
|
|
53918
|
+
}
|
|
53919
|
+
function fontEntryHasBoldStyle(entry) {
|
|
53920
|
+
return !!entry && /\bbold\b/i.test(entry.label ?? "");
|
|
53921
|
+
}
|
|
53922
|
+
function fontEntryHasItalicStyle(entry) {
|
|
53923
|
+
return !!entry && /\b(italic|oblique)\b/i.test(entry.label ?? "");
|
|
53924
|
+
}
|
|
53614
53925
|
function computeCellMetrics2() {
|
|
53615
53926
|
const primary = fontState.fonts[0];
|
|
53616
53927
|
if (!primary)
|
|
@@ -54285,10 +54596,13 @@ function createResttyApp(options) {
|
|
|
54285
54596
|
}
|
|
54286
54597
|
let nextSeqIdx = idx + baseSpan;
|
|
54287
54598
|
let guard = 0;
|
|
54288
|
-
while (
|
|
54599
|
+
while (nextSeqIdx < rowEnd && guard < 12) {
|
|
54289
54600
|
const next = readCellCluster(nextSeqIdx);
|
|
54290
54601
|
if (!next || !next.cp || isSpaceCp(next.cp))
|
|
54291
54602
|
break;
|
|
54603
|
+
const shouldMerge = text.endsWith("") || shouldMergeTrailingClusterCodepoint(next.cp);
|
|
54604
|
+
if (!shouldMerge)
|
|
54605
|
+
break;
|
|
54292
54606
|
text += next.text;
|
|
54293
54607
|
baseSpan += next.span;
|
|
54294
54608
|
mergedEmojiSkip[nextSeqIdx] = 1;
|
|
@@ -54319,7 +54633,7 @@ function createResttyApp(options) {
|
|
|
54319
54633
|
}
|
|
54320
54634
|
if (extra > 0 && text.trim() === "")
|
|
54321
54635
|
continue;
|
|
54322
|
-
const fontIndex = pickFontIndexForText2(text, baseSpan);
|
|
54636
|
+
const fontIndex = pickFontIndexForText2(text, baseSpan, stylePreferenceFromFlags(bold, italic));
|
|
54323
54637
|
const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
|
|
54324
54638
|
const shaped = shapeClusterWithFont(fontEntry, text);
|
|
54325
54639
|
if (!shaped.glyphs.length)
|
|
@@ -54588,7 +54902,8 @@ function createResttyApp(options) {
|
|
|
54588
54902
|
let y = item.baseY + baselineAdjust - metrics.bearingY * bitmapScale - glyph.yOffset * itemScale;
|
|
54589
54903
|
if (!glyphConstrained && symbolLike && item.cp) {
|
|
54590
54904
|
const nerdConstraint = resolveSymbolConstraint(item.cp);
|
|
54591
|
-
const
|
|
54905
|
+
const defaultConstraint = isAppleSymbolsFont(entry) ? DEFAULT_APPLE_SYMBOLS_CONSTRAINT : DEFAULT_SYMBOL_CONSTRAINT;
|
|
54906
|
+
const constraint = nerdConstraint ?? (colorGlyph ? DEFAULT_EMOJI_CONSTRAINT : defaultConstraint);
|
|
54592
54907
|
const rowY = item.baseY - yPad - baselineOffset;
|
|
54593
54908
|
const constraintWidth = Math.max(1, item.constraintWidth ?? Math.round(maxWidth / cellW));
|
|
54594
54909
|
const adjusted = constrainGlyphBox({
|
|
@@ -54625,17 +54940,24 @@ function createResttyApp(options) {
|
|
|
54625
54940
|
const glyphData = useNearest ? glyphDataNearest : glyphDataLinear;
|
|
54626
54941
|
const italic = !!item.italic;
|
|
54627
54942
|
const bold = !!item.bold;
|
|
54628
|
-
const
|
|
54629
|
-
const
|
|
54943
|
+
const syntheticItalic = italic && !fontEntryHasItalicStyle(entry);
|
|
54944
|
+
const syntheticBold = bold && !fontEntryHasBoldStyle(entry);
|
|
54945
|
+
const slant = syntheticItalic && !colorGlyph ? gh * ITALIC_SLANT : 0;
|
|
54946
|
+
const boldOffset = syntheticBold && !colorGlyph ? Math.max(1, Math.round(gw * BOLD_OFFSET)) : 0;
|
|
54630
54947
|
const renderMode = colorGlyph ? GLYPH_RENDER_MODE_COLOR : GLYPH_RENDER_MODE_MONO;
|
|
54631
54948
|
const pushGlyph = (xPos) => {
|
|
54632
54949
|
glyphData.push(xPos, py, gw, gh, u02, v02, u12, v12, item.fg[0], item.fg[1], item.fg[2], item.fg[3], bg[0], bg[1], bg[2], bg[3], slant, renderMode);
|
|
54633
54950
|
};
|
|
54634
54951
|
pushGlyph(px);
|
|
54635
54952
|
if (boldOffset > 0) {
|
|
54636
|
-
const
|
|
54637
|
-
const
|
|
54638
|
-
|
|
54953
|
+
const minGlyphX = Math.round(item.x);
|
|
54954
|
+
const maxGlyphX = Math.round(item.x + maxWidth - gw);
|
|
54955
|
+
let bx = clamp(px + boldOffset, minGlyphX, maxGlyphX);
|
|
54956
|
+
if (bx === px)
|
|
54957
|
+
bx = clamp(px - boldOffset, minGlyphX, maxGlyphX);
|
|
54958
|
+
if (bx === px)
|
|
54959
|
+
pushGlyph(px);
|
|
54960
|
+
else
|
|
54639
54961
|
pushGlyph(bx);
|
|
54640
54962
|
}
|
|
54641
54963
|
penX += glyph.xAdvance;
|
|
@@ -55143,10 +55465,13 @@ function createResttyApp(options) {
|
|
|
55143
55465
|
}
|
|
55144
55466
|
let nextSeqIdx = idx + baseSpan;
|
|
55145
55467
|
let guard = 0;
|
|
55146
|
-
while (
|
|
55468
|
+
while (nextSeqIdx < rowEnd && guard < 12) {
|
|
55147
55469
|
const next = readCellCluster(nextSeqIdx);
|
|
55148
55470
|
if (!next || !next.cp || isSpaceCp(next.cp))
|
|
55149
55471
|
break;
|
|
55472
|
+
const shouldMerge = text.endsWith("") || shouldMergeTrailingClusterCodepoint(next.cp);
|
|
55473
|
+
if (!shouldMerge)
|
|
55474
|
+
break;
|
|
55150
55475
|
text += next.text;
|
|
55151
55476
|
baseSpan += next.span;
|
|
55152
55477
|
mergedEmojiSkip[nextSeqIdx] = 1;
|
|
@@ -55177,7 +55502,7 @@ function createResttyApp(options) {
|
|
|
55177
55502
|
}
|
|
55178
55503
|
if (extra > 0 && text.trim() === "")
|
|
55179
55504
|
continue;
|
|
55180
|
-
const fontIndex = pickFontIndexForText2(text, baseSpan);
|
|
55505
|
+
const fontIndex = pickFontIndexForText2(text, baseSpan, stylePreferenceFromFlags(bold, italic));
|
|
55181
55506
|
const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
|
|
55182
55507
|
const shaped = shapeClusterWithFont(fontEntry, text);
|
|
55183
55508
|
if (!shaped.glyphs.length)
|
|
@@ -55547,7 +55872,8 @@ function createResttyApp(options) {
|
|
|
55547
55872
|
let y = item.baseY + baselineAdjust - metrics.bearingY * bitmapScale - glyph.yOffset * itemScale;
|
|
55548
55873
|
if (!glyphConstrained && symbolLike && item.cp) {
|
|
55549
55874
|
const nerdConstraint = resolveSymbolConstraint(item.cp);
|
|
55550
|
-
const
|
|
55875
|
+
const defaultConstraint = isAppleSymbolsFont(entry) ? DEFAULT_APPLE_SYMBOLS_CONSTRAINT : DEFAULT_SYMBOL_CONSTRAINT;
|
|
55876
|
+
const constraint = nerdConstraint ?? (colorGlyph ? DEFAULT_EMOJI_CONSTRAINT : defaultConstraint);
|
|
55551
55877
|
const rowY = item.baseY - yPad - baselineOffset;
|
|
55552
55878
|
const constraintWidth = Math.max(1, item.constraintWidth ?? Math.round(maxWidth / cellW));
|
|
55553
55879
|
const adjusted = constrainGlyphBox({
|
|
@@ -55576,17 +55902,24 @@ function createResttyApp(options) {
|
|
|
55576
55902
|
const v12 = (metrics.atlasY + metrics.height - insetY) / atlasH;
|
|
55577
55903
|
const italic = !!item.italic;
|
|
55578
55904
|
const bold = !!item.bold;
|
|
55579
|
-
const
|
|
55580
|
-
const
|
|
55905
|
+
const syntheticItalic = italic && !fontEntryHasItalicStyle(entry);
|
|
55906
|
+
const syntheticBold = bold && !fontEntryHasBoldStyle(entry);
|
|
55907
|
+
const slant = syntheticItalic && !colorGlyph ? gh * ITALIC_SLANT : 0;
|
|
55908
|
+
const boldOffset = syntheticBold && !colorGlyph ? Math.max(1, Math.round(gw * BOLD_OFFSET)) : 0;
|
|
55581
55909
|
const renderMode = colorGlyph ? GLYPH_RENDER_MODE_COLOR : GLYPH_RENDER_MODE_MONO;
|
|
55582
55910
|
const pushGlyph = (xPos) => {
|
|
55583
55911
|
glyphData.push(xPos, py, gw, gh, u02, v02, u12, v12, item.fg[0], item.fg[1], item.fg[2], item.fg[3], bg[0], bg[1], bg[2], bg[3], slant, renderMode);
|
|
55584
55912
|
};
|
|
55585
55913
|
pushGlyph(px);
|
|
55586
55914
|
if (boldOffset > 0) {
|
|
55587
|
-
const
|
|
55588
|
-
const
|
|
55589
|
-
|
|
55915
|
+
const minGlyphX = Math.round(item.x);
|
|
55916
|
+
const maxGlyphX = Math.round(item.x + maxWidth - gw);
|
|
55917
|
+
let bx = clamp(px + boldOffset, minGlyphX, maxGlyphX);
|
|
55918
|
+
if (bx === px)
|
|
55919
|
+
bx = clamp(px - boldOffset, minGlyphX, maxGlyphX);
|
|
55920
|
+
if (bx === px)
|
|
55921
|
+
pushGlyph(px);
|
|
55922
|
+
else
|
|
55590
55923
|
pushGlyph(bx);
|
|
55591
55924
|
}
|
|
55592
55925
|
penX += glyph.xAdvance;
|
package/dist/fonts/manager.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare function glyphWidthUnits(entry: FontEntry, glyphId: number | unde
|
|
|
27
27
|
* Select the best font index from the manager's font list for rendering the
|
|
28
28
|
* given text cluster, searching in fallback order similar to Ghostty.
|
|
29
29
|
*/
|
|
30
|
-
export declare function pickFontIndexForText(state: FontManagerState, text: string, expectedSpan: number
|
|
30
|
+
export declare function pickFontIndexForText(state: FontManagerState, text: string, expectedSpan: number): number;
|
|
31
31
|
/** Fetch a font file from a URL and return its ArrayBuffer, or null on failure. */
|
|
32
32
|
export declare function tryFetchFontBuffer(url: string): Promise<ArrayBuffer | null>;
|
|
33
33
|
/** Query locally installed fonts via the Local Font Access API and return the first match, or null. */
|
package/dist/internal.js
CHANGED
package/dist/restty.js
CHANGED