create-mithril-lynx 2.2.0 → 2.4.0
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 +44 -15
- package/package.json +1 -1
- package/src/fonts-wire.js +318 -0
- package/src/index.js +234 -127
- package/templates/_shared/ts/src/background.ts +5 -1
- package/templates/android/app-scripts/android.mjs +21 -0
- package/templates/android/host/app/src/main/java/package-path/App.kt +7 -0
- package/templates/android/host/app/src/main/java/package-path/AssetFontFaceLoader.kt +34 -0
- package/templates/android/host/app/src/main/java/package-path/MainActivity.kt +10 -0
- package/templates/android/host/app/src/main/java/package-path/NoopGenericResourceFetcher.kt +47 -0
package/README.md
CHANGED
|
@@ -49,8 +49,8 @@ Every flag, in one table — `npx create-mithril-lynx --help` prints the same li
|
|
|
49
49
|
| `--android` / `--target android` / `--target=android` / `target=android` | Also scaffold the sibling `<name>-android/` Gradle project. |
|
|
50
50
|
| `--android-id <id>` | `applicationId` / `namespace` (default `com.example.<name>`). |
|
|
51
51
|
| `--app-name <name>` | Launcher label (default: the project name). |
|
|
52
|
-
| `--with-font <file.ttf>` | Bundle the font into `src/assets/fonts/` and register it with `lynx.addFont()
|
|
53
|
-
| `--find-font <term>` | Search [Fontsource](https://fontsource.org)'s catalog for `<term>`, prompt you to pick a family and
|
|
52
|
+
| `--with-font <file.ttf>` | Bundle the font into `src/assets/fonts/` and register it with `lynx.addFont()` in a loop. **DEV** (`npm run dev` / Lynx Go): `require()` inlines a `data:` URI so the font shows up in Explorer. **PROD** (`npm run build` / APK): `asset:///fonts/<file>` resolved by `AssetFontFaceLoader` on the host (keeps the bundle small — embedding `data:` URIs in the APK re-introduces the cold-start cost tracked in [lynx#9431](https://github.com/lynx-family/lynx/issues/9431)). Comma-separate for more than one file. |
|
|
53
|
+
| `--find-font <term>` | Search [Fontsource](https://fontsource.org)'s catalog for `<term>`, prompt you to pick a family and one or more weights/styles, download each `.ttf`, and bundle them exactly like `--with-font`. Needs a real terminal (the picking is inherently interactive — use `--with-font <file.ttf>` in scripts/CI). Mutually exclusive with `--with-font`. Comma-separate terms for more than one — quote the whole thing if any term has a space. |
|
|
54
54
|
| `--font-family <name>` | Override the family name derived from the font's file name (or from Fontsource, with `--find-font`). Only valid with exactly one font. |
|
|
55
55
|
|
|
56
56
|
Anything not listed — in particular the four Android flags — requires `--android`
|
|
@@ -65,18 +65,41 @@ monospace one for code, for example:
|
|
|
65
65
|
npx create-mithril-lynx my-app --blank --android --find-font "Inter,JetBrains Mono"
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
Each font
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
Each font is one entry in a `FONTS` array in `src/background.ts`, registered
|
|
69
|
+
with a single `for…of` + `lynx.addFont()` loop (DEV `require` / PROD
|
|
70
|
+
`asset:///` — see the `--with-font` row above).
|
|
71
|
+
|
|
72
|
+
The CSS side lives in a separate, **generated** file, `src/fonts.css`, which
|
|
73
|
+
`src/style.css` pulls in with a single `@import "fonts.css";` line at the top.
|
|
74
|
+
`fonts.css` holds `text { font-family: ...; }` for the **first** font and, when
|
|
75
|
+
there is more than one font, one class per family/variant:
|
|
73
76
|
|
|
74
77
|
```css
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
+
.Ubuntu_Regular { font-family: "Ubuntu Regular", sans-serif; }
|
|
79
|
+
.Ubuntu_Bold { font-family: "Ubuntu Bold", sans-serif; }
|
|
80
|
+
.Inter_Regular { font-family: "Inter Regular", sans-serif; }
|
|
78
81
|
```
|
|
79
82
|
|
|
83
|
+
`fonts.css` is owned by the tooling and rewritten from the full font list on
|
|
84
|
+
every run (scaffold or `add-font`) — don't edit it by hand; put your own rules
|
|
85
|
+
in `style.css`. (Unused classes can be pruned by a later PostCSS pass.)
|
|
86
|
+
|
|
87
|
+
### Adding fonts after the project exists
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cd my-app
|
|
91
|
+
npx create-mithril-lynx add-font --with-font ./Another.ttf
|
|
92
|
+
npx create-mithril-lynx add-font --find-font "Roboto Mono"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Copying the `.ttf` into `src/assets/fonts/` (and the Android host's
|
|
96
|
+
`assets/fonts/` when present) is always safe. `src/fonts.css` is always
|
|
97
|
+
regenerated, and `style.css` only ever gets the one `@import "fonts.css";`
|
|
98
|
+
line (added if missing). Wiring `background.ts` is automatic **only while it
|
|
99
|
+
still matches the stock template** (including a previously generated `FONTS`
|
|
100
|
+
block on top of it); if you've edited it, the command prints the exact block
|
|
101
|
+
to paste by hand and leaves your file untouched.
|
|
102
|
+
|
|
80
103
|
### `--find-font`, in more detail
|
|
81
104
|
|
|
82
105
|
Fontsource's own API (`api.fontsource.org`) has no free-text search — only
|
|
@@ -84,11 +107,13 @@ exact `id`/`family` filters — so `--find-font` downloads the whole font list
|
|
|
84
107
|
once (~2100 fonts, ~540KB as of 2026-09), caches it for 24h in the OS temp
|
|
85
108
|
directory, and filters client-side on `family`/`id` substrings. Picking a
|
|
86
109
|
family fetches that font's own detail (its real weight/style/subset `.ttf`
|
|
87
|
-
URLs), lets you pick one
|
|
88
|
-
downloads
|
|
110
|
+
URLs), lets you pick one or more variants (space to toggle; defaulting to 400/normal if available),
|
|
111
|
+
downloads each to a temp file, and hands them to the same code path
|
|
89
112
|
`--with-font <file>` uses — there's no separate font-loading mechanism to
|
|
90
113
|
maintain.
|
|
91
114
|
|
|
115
|
+
Every picked variant is registered as `<Family> <Variant>` (`Inter Regular`, `Inter Bold`, `Inter Light Italic`…), with a matching class in `src/fonts.css` (`.Inter_Bold`). The first one (400/normal if picked) is the default `text` font.
|
|
116
|
+
|
|
92
117
|
```bash
|
|
93
118
|
npx create-mithril-lynx my-app --blank --android --find-font Inter
|
|
94
119
|
```
|
|
@@ -99,7 +124,7 @@ The sibling `<name>-android/` project is a complete, no-Android-Studio Gradle CL
|
|
|
99
124
|
|
|
100
125
|
- the Gradle wrapper (`gradlew`, `gradlew.bat`, `gradle-wrapper.jar`), so nothing needs to be installed besides a JDK and the Android SDK;
|
|
101
126
|
- `local.properties` with `sdk.dir` auto-detected from `ANDROID_HOME`/`ANDROID_SDK_ROOT` (with a written warning if it can't be found);
|
|
102
|
-
- the Kotlin host: the `Application
|
|
127
|
+
- the Kotlin host: the `Application` (registers `AssetFontFaceLoader`), `MainActivity` (async `AssetTemplateProvider`, splash screen, `NoopGenericResourceFetcher` for the fast `data:` font path — [lynx#9431](https://github.com/lynx-family/lynx/issues/9431));
|
|
103
128
|
- resource files that compile as-is (theme, splash theme, adaptive launcher icon);
|
|
104
129
|
- an opt-in release signing setup driven by a gitignored `keystore.properties`.
|
|
105
130
|
|
|
@@ -127,8 +152,9 @@ This part of the tool is framework-agnostic — it just wraps whatever bundle `r
|
|
|
127
152
|
|
|
128
153
|
```
|
|
129
154
|
create-mithril-lynx/
|
|
130
|
-
src/index.js the CLI itself
|
|
155
|
+
src/index.js the CLI itself (+ add-font subcommand)
|
|
131
156
|
src/fontsource.js --find-font: local search over Fontsource's catalog
|
|
157
|
+
src/fonts-wire.js FONTS block builder + stock-template detect/patch
|
|
132
158
|
templates/
|
|
133
159
|
_shared/
|
|
134
160
|
common/ gitignore, project README — shared by every template
|
|
@@ -146,12 +172,15 @@ create-mithril-lynx/
|
|
|
146
172
|
ts/src/{app-bar.ts, background.ts, screens/{home,detail}.ts}
|
|
147
173
|
android/ only used with --android
|
|
148
174
|
host/ the Gradle project -> <name>-android/
|
|
175
|
+
(includes AssetFontFaceLoader +
|
|
176
|
+
NoopGenericResourceFetcher for lynx#9431)
|
|
149
177
|
app-scripts/android.mjs the bridge -> <name>/scripts/android.mjs
|
|
178
|
+
(syncs bundle + src/assets/fonts/ → assets/fonts/)
|
|
150
179
|
```
|
|
151
180
|
|
|
152
181
|
`src/index.js` copies, in order, `templates/_shared/common` → `templates/_shared/ts` → `templates/<template>/common` → `templates/<template>/ts` into the target directory — later copies overwrite same-named files from earlier ones, which is exactly how **Basic Activity**'s own routing-based `src/background.ts` replaces `_shared/ts`'s generic single-view one (Hello World and Blank don't ship their own, so they keep the shared file). It then renames `gitignore` to `.gitignore` (npm doesn't publish dotfiles reliably otherwise) and replaces `{{PROJECT_NAME}}`/`{{MITHRIL_LYNX_VERSION}}` placeholders in `package.json` and `README.md`.
|
|
153
182
|
|
|
154
|
-
With `--android` it then copies `templates/android/host` into a sibling `<name>-android/` (renaming `package-path` to the real package directory and `App.kt` to the Application class) and substitutes `{{PACKAGE_NAME}}`, `{{APP_CLASS}}`, `{{APP_NAME}}`, `{{ANDROID_DIR}}` and `{{SDK_DIR}}` across the tree. Binary files (the Gradle wrapper jar, a `--with-font` `.ttf`) are never text-substituted. With `--with-font`,
|
|
183
|
+
With `--android` it then copies `templates/android/host` into a sibling `<name>-android/` (renaming `package-path` to the real package directory and `App.kt` to the Application class) and substitutes `{{PACKAGE_NAME}}`, `{{APP_CLASS}}`, `{{APP_NAME}}`, `{{ANDROID_DIR}}` and `{{SDK_DIR}}` across the tree. Binary files (the Gradle wrapper jar, a `--with-font` `.ttf`) are never text-substituted. With `--with-font`, each font is copied into the JS project (`src/assets/fonts/`) *and* seeded into the Android host's `assets/fonts/`, wired up with a `FONTS` loop + `lynx.addFont()` in `src/background.ts` plus a generated `src/fonts.css` (`text { font-family }` for the first font, one class per font when there are several) imported from `src/style.css`. `scripts/android.mjs` keeps `assets/fonts/` in sync on every `npm run android` / `android:sync`.
|
|
155
184
|
|
|
156
185
|
**Not carried over from v1 (yet)**: the JavaScript variant. The old tool generated either TypeScript or JavaScript for every template; this rewrite ships TypeScript only for now — doubling every template for a parallel JS copy wasn't part of this pass.
|
|
157
186
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font registration wiring shared by scaffold (`--with-font` / `--find-font`)
|
|
3
|
+
* and the post-init `add-font` subcommand.
|
|
4
|
+
*
|
|
5
|
+
* Strategy for patching an existing project (add-font):
|
|
6
|
+
* 1. Always copy .ttf/.otf/.ttc into src/assets/fonts/ (and the Android
|
|
7
|
+
* host's assets/fonts/ when present) — that part is always safe.
|
|
8
|
+
* 2. If src/background.ts still matches a stock template (optionally with
|
|
9
|
+
* our generated FONTS block on top), rewrite the font block in place.
|
|
10
|
+
* 3. Always (re)generate src/fonts.css — a system-owned file holding the
|
|
11
|
+
* `text { font-family }` default and one class per family/variant — and
|
|
12
|
+
* make sure src/style.css has `@import "fonts.css";`. Users never edit
|
|
13
|
+
* fonts.css by hand; every add-font run rewrites it from the full list.
|
|
14
|
+
* 4. Otherwise print the exact lines the user must paste by hand.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const packageRoot = path.join(scriptDir, "..");
|
|
22
|
+
|
|
23
|
+
const FONT_EXT = /\.(ttf|otf|ttc)$/i;
|
|
24
|
+
|
|
25
|
+
/** Normalize for equality checks (EOL + trailing whitespace per line). */
|
|
26
|
+
export function normalizeSource(text) {
|
|
27
|
+
return (
|
|
28
|
+
text
|
|
29
|
+
.replace(/\r\n/g, "\n")
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((line) => line.replace(/\s+$/u, ""))
|
|
32
|
+
.join("\n")
|
|
33
|
+
.replace(/\n+$/u, "") + "\n"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildFontBlock(fonts) {
|
|
38
|
+
const defaultFamily = fonts[0].family;
|
|
39
|
+
const requireEntries = fonts
|
|
40
|
+
.map(
|
|
41
|
+
({ family, file }) =>
|
|
42
|
+
`\t\t{ family: "${family}", file: "${file}", url: require("./assets/fonts/${file}") },`,
|
|
43
|
+
)
|
|
44
|
+
.join("\n");
|
|
45
|
+
const assetEntries = fonts
|
|
46
|
+
.map(
|
|
47
|
+
({ family, file }) =>
|
|
48
|
+
`\t\t{ family: "${family}", file: "${file}", url: "asset:///fonts/${file}" },`,
|
|
49
|
+
)
|
|
50
|
+
.join("\n");
|
|
51
|
+
|
|
52
|
+
return [
|
|
53
|
+
fonts.length === 1
|
|
54
|
+
? `// Font: "${defaultFamily}", from src/assets/fonts/${fonts[0].file}.`
|
|
55
|
+
: `// Fonts: ${fonts.map((f) => `"${f.family}"`).join(", ")}, from src/assets/fonts/.`,
|
|
56
|
+
"// Registered in a loop via lynx.addFont(). Split by build mode:",
|
|
57
|
+
"// • DEV (Lynx Go / `npm run dev`): require() → data: URI inline",
|
|
58
|
+
"// • PROD (Android APK / `npm run build`): asset:///fonts/<file>",
|
|
59
|
+
"// resolved by AssetFontFaceLoader (lynx#9431 workaround).",
|
|
60
|
+
"// The default `text { font-family }` and one .Family_Variant class per font",
|
|
61
|
+
"// live in src/fonts.css, which is generated — don't edit it by hand.",
|
|
62
|
+
"type FontEntry = { family: string; file: string; url: string };",
|
|
63
|
+
"const FONTS: FontEntry[] = import.meta.env.DEV",
|
|
64
|
+
"\t? [",
|
|
65
|
+
requireEntries,
|
|
66
|
+
"\t ]",
|
|
67
|
+
"\t: [",
|
|
68
|
+
assetEntries,
|
|
69
|
+
"\t ];",
|
|
70
|
+
"for (const { family, url } of FONTS) {",
|
|
71
|
+
'\tlynx.addFont({ "font-family": family, src: `url("${url}")` }, () => {});',
|
|
72
|
+
"}",
|
|
73
|
+
"",
|
|
74
|
+
"",
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `Ubuntu Light Italic` → `Ubuntu_Light_Italic` (usable as a CSS class name). */
|
|
79
|
+
export function cssClassName(family) {
|
|
80
|
+
return family.trim().replace(/[^\w-]+/g, "_");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const FONTS_CSS_FILE = "fonts.css";
|
|
84
|
+
const FONTS_CSS_IMPORT = `@import "${FONTS_CSS_FILE}";`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Contents of src/fonts.css. With one font: just the `text` default. With
|
|
88
|
+
* more than one: also a `.Family_Variant { font-family }` class per font, so
|
|
89
|
+
* a later (postcss) pass can drop the classes the app never uses.
|
|
90
|
+
*/
|
|
91
|
+
export function buildFontsCss(fonts) {
|
|
92
|
+
const lines = [
|
|
93
|
+
"/* GENERATED by create-mithril-lynx (--with-font / --find-font / add-font).",
|
|
94
|
+
" * Owned by the tooling: it is rewritten from the full font list on every",
|
|
95
|
+
" * add-font run, so do not edit it by hand — put your own rules in style.css. */",
|
|
96
|
+
"text {",
|
|
97
|
+
` font-family: "${fonts[0].family}", sans-serif;`,
|
|
98
|
+
"}",
|
|
99
|
+
"",
|
|
100
|
+
];
|
|
101
|
+
if (fonts.length > 1) {
|
|
102
|
+
for (const { family } of fonts) {
|
|
103
|
+
lines.push(`.${cssClassName(family)} {`, ` font-family: "${family}", sans-serif;`, "}", "");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Match our generated font block at the top of background.ts (if any). */
|
|
110
|
+
const FONT_BLOCK_RE =
|
|
111
|
+
/^(?:\/\/ Font[^\n]*\n)(?:\/\/[^\n]*\n)*type FontEntry = \{ family: string; file: string; url: string \};\nconst FONTS: FontEntry\[\] = import\.meta\.env\.DEV\n\t\? \[[\s\S]*?\t \]\n\t: \[[\s\S]*?\t \];\nfor \(const \{ family, url \} of FONTS\) \{\n\tlynx\.addFont\(\{ "font-family": family, src: `url\("\$\{url\}"\)` \}, \(\) => \{\}\);\n\}\n\n/;
|
|
112
|
+
|
|
113
|
+
const CSS_FONT_RE = /^text \{\n font-family: "[^"]+", sans-serif;\n\}\n\n?/;
|
|
114
|
+
|
|
115
|
+
export function stripFontBlock(backgroundSource) {
|
|
116
|
+
const normalized = normalizeSource(backgroundSource);
|
|
117
|
+
const match = normalized.match(FONT_BLOCK_RE);
|
|
118
|
+
if (!match) return { block: null, rest: normalized, fonts: [] };
|
|
119
|
+
return {
|
|
120
|
+
block: match[0],
|
|
121
|
+
rest: normalized.slice(match[0].length),
|
|
122
|
+
fonts: parseFontsFromBlock(match[0]),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function parseFontsFromBlock(block) {
|
|
127
|
+
const fonts = [];
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
for (const match of block.matchAll(/\{\s*family:\s*"([^"]+)",\s*file:\s*"([^"]+)"/g)) {
|
|
130
|
+
const [, family, file] = match;
|
|
131
|
+
if (seen.has(file)) continue;
|
|
132
|
+
seen.add(file);
|
|
133
|
+
fonts.push({ family, file });
|
|
134
|
+
}
|
|
135
|
+
return fonts;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function stripCssFontBlock(cssSource) {
|
|
139
|
+
const normalized = normalizeSource(cssSource);
|
|
140
|
+
const match = normalized.match(CSS_FONT_RE);
|
|
141
|
+
if (!match) return { block: null, rest: normalized, family: null };
|
|
142
|
+
const familyMatch = match[0].match(/font-family: "([^"]+)"/);
|
|
143
|
+
return {
|
|
144
|
+
block: match[0],
|
|
145
|
+
rest: normalized.slice(match[0].length),
|
|
146
|
+
family: familyMatch ? familyMatch[1] : null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function loadBaseBackgrounds() {
|
|
151
|
+
return [
|
|
152
|
+
normalizeSource(fs.readFileSync(path.join(packageRoot, "templates/_shared/ts/src/background.ts"), "utf8")),
|
|
153
|
+
normalizeSource(
|
|
154
|
+
fs.readFileSync(path.join(packageRoot, "templates/basic-activity/ts/src/background.ts"), "utf8"),
|
|
155
|
+
),
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function isStockBackground(restSource) {
|
|
160
|
+
const normalized = normalizeSource(restSource);
|
|
161
|
+
return loadBaseBackgrounds().some((base) => base === normalized);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Copy font files into the JS project (and Android host assets when found).
|
|
166
|
+
* Always safe — does not touch background.ts / style.css / fonts.css.
|
|
167
|
+
*/
|
|
168
|
+
export function copyFontFiles({ projectRoot, fonts, androidDir = null }) {
|
|
169
|
+
const jsFontsDir = path.join(projectRoot, "src", "assets", "fonts");
|
|
170
|
+
fs.mkdirSync(jsFontsDir, { recursive: true });
|
|
171
|
+
const copied = [];
|
|
172
|
+
|
|
173
|
+
for (const font of fonts) {
|
|
174
|
+
const dest = path.join(jsFontsDir, font.file);
|
|
175
|
+
fs.copyFileSync(font.sourcePath, dest);
|
|
176
|
+
copied.push({ role: "js", path: dest });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (androidDir != null) {
|
|
180
|
+
const androidFontsDir = path.join(androidDir, "app", "src", "main", "assets", "fonts");
|
|
181
|
+
fs.mkdirSync(androidFontsDir, { recursive: true });
|
|
182
|
+
for (const font of fonts) {
|
|
183
|
+
const dest = path.join(androidFontsDir, font.file);
|
|
184
|
+
fs.copyFileSync(font.sourcePath, dest);
|
|
185
|
+
copied.push({ role: "android", path: dest });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return copied;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Merge newly requested fonts with ones already registered in background.ts
|
|
194
|
+
* (or already sitting in src/assets/fonts/). `dedupeFontFiles` mutates `.file`.
|
|
195
|
+
*/
|
|
196
|
+
export function mergeFontLists(existing, incoming, dedupeFontFiles) {
|
|
197
|
+
const merged = [];
|
|
198
|
+
const byFile = new Set();
|
|
199
|
+
for (const font of existing) {
|
|
200
|
+
if (byFile.has(font.file)) continue;
|
|
201
|
+
byFile.add(font.file);
|
|
202
|
+
merged.push({ ...font });
|
|
203
|
+
}
|
|
204
|
+
for (const font of incoming) {
|
|
205
|
+
merged.push({
|
|
206
|
+
sourcePath: font.sourcePath,
|
|
207
|
+
file: font.file,
|
|
208
|
+
family: font.family,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
dedupeFontFiles(merged);
|
|
212
|
+
return merged;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Try to rewrite background.ts. Returns { ok, reason?, manualBlock? }.
|
|
217
|
+
*/
|
|
218
|
+
export function applyBackgroundFonts(projectRoot, fonts) {
|
|
219
|
+
const bgPath = path.join(projectRoot, "src", "background.ts");
|
|
220
|
+
if (!fs.existsSync(bgPath)) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
reason: "src/background.ts not found",
|
|
224
|
+
manualBlock: buildFontBlock(fonts),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const raw = fs.readFileSync(bgPath, "utf8");
|
|
228
|
+
const { rest } = stripFontBlock(raw);
|
|
229
|
+
if (!isStockBackground(rest)) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
reason: "src/background.ts no longer matches the stock template",
|
|
233
|
+
manualBlock: buildFontBlock(fonts),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
fs.writeFileSync(bgPath, `${buildFontBlock(fonts)}${rest}`);
|
|
237
|
+
return { ok: true };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Write src/fonts.css from the full font list and make sure src/style.css
|
|
242
|
+
* imports it. style.css is only ever touched to add the one `@import` line
|
|
243
|
+
* (and to drop the `text { font-family }` block older versions put there).
|
|
244
|
+
* Returns { ok, reason?, manualBlock? } — ok=false only when style.css
|
|
245
|
+
* couldn't be found to add the import.
|
|
246
|
+
*/
|
|
247
|
+
export function applyFontsCss(projectRoot, fonts) {
|
|
248
|
+
const srcDir = path.join(projectRoot, "src");
|
|
249
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
250
|
+
fs.writeFileSync(path.join(srcDir, FONTS_CSS_FILE), buildFontsCss(fonts));
|
|
251
|
+
|
|
252
|
+
const cssPath = path.join(srcDir, "style.css");
|
|
253
|
+
if (!fs.existsSync(cssPath)) {
|
|
254
|
+
return { ok: false, reason: "src/style.css not found", manualBlock: `${FONTS_CSS_IMPORT}\n` };
|
|
255
|
+
}
|
|
256
|
+
const raw = fs.readFileSync(cssPath, "utf8");
|
|
257
|
+
const { block, rest } = stripCssFontBlock(raw);
|
|
258
|
+
if (/@import\s+(?:url\()?["']?(?:\.\/)?fonts\.css["']?\)?\s*;/.test(rest)) {
|
|
259
|
+
if (block != null) fs.writeFileSync(cssPath, rest);
|
|
260
|
+
return { ok: true, skipped: true };
|
|
261
|
+
}
|
|
262
|
+
// @import must come before any other rule, so it goes at the very top.
|
|
263
|
+
fs.writeFileSync(cssPath, `${FONTS_CSS_IMPORT}\n\n${rest}`);
|
|
264
|
+
return { ok: true };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Resolve sibling Android host: scripts/android.mjs ANDROID_DIR, or <name>-android/. */
|
|
268
|
+
export function findAndroidDir(projectRoot) {
|
|
269
|
+
const scriptPath = path.join(projectRoot, "scripts", "android.mjs");
|
|
270
|
+
if (fs.existsSync(scriptPath)) {
|
|
271
|
+
const src = fs.readFileSync(scriptPath, "utf8");
|
|
272
|
+
const match = src.match(/path\.resolve\(projectRoot,\s*"([^"]+)"\)/);
|
|
273
|
+
if (match) {
|
|
274
|
+
const resolved = path.resolve(projectRoot, match[1]);
|
|
275
|
+
if (fs.existsSync(path.join(resolved, "settings.gradle.kts"))) return resolved;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const parent = path.dirname(projectRoot);
|
|
279
|
+
const guess = path.join(parent, `${path.basename(projectRoot)}-android`);
|
|
280
|
+
if (fs.existsSync(path.join(guess, "settings.gradle.kts"))) return guess;
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Walk up from cwd looking for a mithril-lynx app (lynx.config.ts + src/). */
|
|
285
|
+
export function findProjectRoot(startDir) {
|
|
286
|
+
let dir = path.resolve(startDir);
|
|
287
|
+
for (;;) {
|
|
288
|
+
if (
|
|
289
|
+
fs.existsSync(path.join(dir, "lynx.config.ts")) &&
|
|
290
|
+
fs.existsSync(path.join(dir, "src", "background.ts"))
|
|
291
|
+
) {
|
|
292
|
+
return dir;
|
|
293
|
+
}
|
|
294
|
+
const parent = path.dirname(dir);
|
|
295
|
+
if (parent === dir) return null;
|
|
296
|
+
dir = parent;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function listFontsOnDisk(projectRoot) {
|
|
301
|
+
const fontsDir = path.join(projectRoot, "src", "assets", "fonts");
|
|
302
|
+
if (!fs.existsSync(fontsDir)) return [];
|
|
303
|
+
return fs
|
|
304
|
+
.readdirSync(fontsDir)
|
|
305
|
+
.filter((name) => FONT_EXT.test(name))
|
|
306
|
+
.map((file) => ({
|
|
307
|
+
file,
|
|
308
|
+
family: file
|
|
309
|
+
.replace(FONT_EXT, "")
|
|
310
|
+
.split(/[_\-.]+/)
|
|
311
|
+
.filter(Boolean)
|
|
312
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
313
|
+
.join(" "),
|
|
314
|
+
sourcePath: path.join(fontsDir, file),
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export { FONT_EXT };
|
package/src/index.js
CHANGED
|
@@ -5,9 +5,20 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
|
|
8
|
+
import { cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
9
9
|
|
|
10
10
|
import { fetchFontDetail, fetchFontList, listVariants, searchFonts } from "./fontsource.js";
|
|
11
|
+
import {
|
|
12
|
+
applyBackgroundFonts,
|
|
13
|
+
applyFontsCss,
|
|
14
|
+
buildFontBlock,
|
|
15
|
+
cssClassName,
|
|
16
|
+
copyFontFiles,
|
|
17
|
+
findAndroidDir,
|
|
18
|
+
findProjectRoot,
|
|
19
|
+
mergeFontLists,
|
|
20
|
+
stripFontBlock,
|
|
21
|
+
} from "./fonts-wire.js";
|
|
11
22
|
|
|
12
23
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
24
|
const packageRoot = path.join(scriptDir, "..");
|
|
@@ -162,16 +173,6 @@ function dedupeFontFiles(fonts) {
|
|
|
162
173
|
}
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
/** A valid JS identifier (camelCase, "Font" suffix) for a font-family string. */
|
|
166
|
-
function jsIdentifierFor(family) {
|
|
167
|
-
const words = family.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
168
|
-
const camel = words
|
|
169
|
-
.map((word, i) => (i === 0 ? word[0].toLowerCase() + word.slice(1) : word[0].toUpperCase() + word.slice(1)))
|
|
170
|
-
.join("");
|
|
171
|
-
const safe = /^[0-9]/.test(camel) ? `f${camel}` : camel;
|
|
172
|
-
return `${safe || "font"}Font`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
176
|
function xmlEscape(value) {
|
|
176
177
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
177
178
|
}
|
|
@@ -254,6 +255,12 @@ const WEIGHT_NAMES = {
|
|
|
254
255
|
900: "Black",
|
|
255
256
|
};
|
|
256
257
|
|
|
258
|
+
/** "Bold", "Light Italic" — the part appended to the family name. */
|
|
259
|
+
function variantName(variant) {
|
|
260
|
+
const weightName = WEIGHT_NAMES[variant.weight] ?? String(variant.weight);
|
|
261
|
+
return `${weightName}${variant.style === "italic" ? " Italic" : ""}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
257
264
|
function variantLabel(variant) {
|
|
258
265
|
const weightName = WEIGHT_NAMES[variant.weight] ?? String(variant.weight);
|
|
259
266
|
const style = variant.style === "italic" ? " Italic" : "";
|
|
@@ -263,8 +270,8 @@ function variantLabel(variant) {
|
|
|
263
270
|
/**
|
|
264
271
|
* Interactive search-download flow for `--find-font <term>`: queries
|
|
265
272
|
* Fontsource's catalog locally (see src/fontsource.js — the API itself has
|
|
266
|
-
* no free-text search), lets the user pick a family and one
|
|
267
|
-
* downloads
|
|
273
|
+
* no free-text search), lets the user pick a family and one or more weights/styles,
|
|
274
|
+
* downloads each .ttf to a temp file, and returns them in the same shape
|
|
268
275
|
* `--with-font <file>` expects, so the rest of the pipeline (entirely
|
|
269
276
|
* JS-side — see patchJsProject()) doesn't need to know which path was used.
|
|
270
277
|
*/
|
|
@@ -321,33 +328,45 @@ async function findFontInteractively(term) {
|
|
|
321
328
|
s2.stop(`${variants.length || allVariants.length} variant(s) available.`);
|
|
322
329
|
|
|
323
330
|
const pickFrom = variants.length > 0 ? variants : allVariants;
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
331
|
+
const defaultVariant = pickFrom.find((v) => v.weight === 400 && v.style === "normal") ?? pickFrom[0];
|
|
332
|
+
const chosenVariants = await multiselect({
|
|
333
|
+
message: "Which weights/styles? (space to toggle, enter to confirm)",
|
|
334
|
+
initialValues: [defaultVariant],
|
|
335
|
+
required: true,
|
|
327
336
|
options: pickFrom.map((v) => ({ value: v, label: variantLabel(v) })),
|
|
328
337
|
});
|
|
329
|
-
if (isCancel(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
try {
|
|
335
|
-
const response = await fetch(chosenVariant.url);
|
|
336
|
-
if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
|
|
337
|
-
bytes = Buffer.from(await response.arrayBuffer());
|
|
338
|
-
} catch (error) {
|
|
339
|
-
s3.stop("Download failed.");
|
|
340
|
-
cancel(error instanceof Error ? error.message : String(error));
|
|
341
|
-
process.exit(1);
|
|
342
|
-
}
|
|
343
|
-
const tempFile = path.join(
|
|
344
|
-
os.tmpdir(),
|
|
345
|
-
`${chosenId}-${chosenVariant.weight}-${chosenVariant.style}-${chosenVariant.subset}.ttf`,
|
|
338
|
+
if (isCancel(chosenVariants)) return null;
|
|
339
|
+
// Regular first (it becomes the family's plain name), then by weight/style.
|
|
340
|
+
chosenVariants.sort(
|
|
341
|
+
(a, b) =>
|
|
342
|
+
(b === defaultVariant) - (a === defaultVariant) || a.weight - b.weight || a.style.localeCompare(b.style),
|
|
346
343
|
);
|
|
347
|
-
fs.writeFileSync(tempFile, bytes);
|
|
348
|
-
s3.stop(`Downloaded ${(bytes.length / 1024).toFixed(1)} kB.`);
|
|
349
344
|
|
|
350
|
-
|
|
345
|
+
const results = [];
|
|
346
|
+
for (const [index, variant] of chosenVariants.entries()) {
|
|
347
|
+
const s3 = spinner();
|
|
348
|
+
s3.start(`Downloading ${detail.family} ${variantLabel(variant)}…`);
|
|
349
|
+
let bytes;
|
|
350
|
+
try {
|
|
351
|
+
const response = await fetch(variant.url);
|
|
352
|
+
if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
|
|
353
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
354
|
+
} catch (error) {
|
|
355
|
+
s3.stop("Download failed.");
|
|
356
|
+
cancel(error instanceof Error ? error.message : String(error));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
const tempFile = path.join(
|
|
360
|
+
os.tmpdir(),
|
|
361
|
+
`${chosenId}-${variant.weight}-${variant.style}-${variant.subset}.ttf`,
|
|
362
|
+
);
|
|
363
|
+
fs.writeFileSync(tempFile, bytes);
|
|
364
|
+
s3.stop(`Downloaded ${(bytes.length / 1024).toFixed(1)} kB.`);
|
|
365
|
+
// Each file is registered under its own family name (addFont has no
|
|
366
|
+
// weight/style axis here): "Ubuntu Regular", "Ubuntu Bold Italic"…
|
|
367
|
+
results.push({ sourcePath: tempFile, base: detail.family, family: `${detail.family} ${variantName(variant)}` });
|
|
368
|
+
}
|
|
369
|
+
return results;
|
|
351
370
|
}
|
|
352
371
|
|
|
353
372
|
// ---------------------------------------------------------------------------
|
|
@@ -393,38 +412,67 @@ async function resolveAndroidOptions(android, { rawName, projectName, canPrompt
|
|
|
393
412
|
}
|
|
394
413
|
appName = appName ?? path.basename(rawName);
|
|
395
414
|
|
|
415
|
+
const fonts = await resolveFontsFromFlags(android, { baseDir: cwd, requireFonts: false });
|
|
416
|
+
if (fonts == null) return null;
|
|
417
|
+
|
|
418
|
+
return { androidId, appName, fonts };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Shared by scaffold and `add-font`: resolve --with-font / --find-font /
|
|
423
|
+
* --font-family into `{ sourcePath, file, family }[]`. Returns null if the
|
|
424
|
+
* interactive --find-font flow was cancelled; exits on hard errors.
|
|
425
|
+
* When `requireFonts` is false (scaffold without --with-font), returns [].
|
|
426
|
+
*/
|
|
427
|
+
async function resolveFontsFromFlags(android, { baseDir, requireFonts }) {
|
|
396
428
|
if (android.fontPath != null && android.findFontTerm != null) {
|
|
397
429
|
cancel("--with-font and --find-font are mutually exclusive — pick one.");
|
|
398
430
|
process.exit(1);
|
|
399
431
|
}
|
|
400
432
|
|
|
401
|
-
// Both flags accept a comma-separated list, so more than one font (e.g.
|
|
402
|
-
// a body font and a monospace one for code) can be bundled in one run.
|
|
403
433
|
const fontPaths = android.fontPath != null ? splitList(android.fontPath) : [];
|
|
404
434
|
const findFontTerms = android.findFontTerm != null ? splitList(android.findFontTerm) : [];
|
|
405
435
|
const fontCount = fontPaths.length + findFontTerms.length;
|
|
406
436
|
|
|
407
437
|
if (android.fontFamily != null && fontCount > 1) {
|
|
408
|
-
cancel(
|
|
438
|
+
cancel(
|
|
439
|
+
"--font-family only makes sense with exactly one font — omit it when bundling more than one, or use the classes in src/fonts.css.",
|
|
440
|
+
);
|
|
409
441
|
process.exit(1);
|
|
410
442
|
}
|
|
411
443
|
if (android.fontFamily != null && fontCount === 0) {
|
|
412
444
|
cancel("--font-family only makes sense together with --with-font or --find-font.");
|
|
413
445
|
process.exit(1);
|
|
414
446
|
}
|
|
447
|
+
if (fontCount === 0) {
|
|
448
|
+
if (requireFonts) {
|
|
449
|
+
cancel("Pass --with-font <file.ttf> or --find-font <term> (comma-separate for more than one).");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
415
454
|
|
|
416
455
|
const fonts = [];
|
|
417
456
|
for (const term of findFontTerms) {
|
|
418
457
|
const found = await findFontInteractively(term);
|
|
419
458
|
if (found == null) return null;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
459
|
+
for (const variant of found) {
|
|
460
|
+
fonts.push({
|
|
461
|
+
sourcePath: variant.sourcePath,
|
|
462
|
+
file: path.basename(variant.sourcePath),
|
|
463
|
+
// --font-family replaces the family part; the variant suffix stays
|
|
464
|
+
// unless there is only one variant.
|
|
465
|
+
family:
|
|
466
|
+
android.fontFamily == null
|
|
467
|
+
? variant.family
|
|
468
|
+
: found.length === 1
|
|
469
|
+
? android.fontFamily
|
|
470
|
+
: variant.family.replace(variant.base, android.fontFamily),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
425
473
|
}
|
|
426
474
|
for (const rawPath of fontPaths) {
|
|
427
|
-
const resolved = path.resolve(
|
|
475
|
+
const resolved = path.resolve(baseDir, rawPath);
|
|
428
476
|
if (!fs.existsSync(resolved)) {
|
|
429
477
|
cancel(`Font file not found: ${rawPath}`);
|
|
430
478
|
process.exit(1);
|
|
@@ -440,8 +488,99 @@ async function resolveAndroidOptions(android, { rawName, projectName, canPrompt
|
|
|
440
488
|
});
|
|
441
489
|
}
|
|
442
490
|
dedupeFontFiles(fonts);
|
|
491
|
+
return fonts;
|
|
492
|
+
}
|
|
443
493
|
|
|
444
|
-
|
|
494
|
+
function printManualBlock(title, block) {
|
|
495
|
+
console.log("");
|
|
496
|
+
console.log(` ${title}`);
|
|
497
|
+
console.log(" ────────────────────────────────────────");
|
|
498
|
+
for (const line of block.replace(/\n$/, "").split("\n")) {
|
|
499
|
+
console.log(` ${line}`);
|
|
500
|
+
}
|
|
501
|
+
console.log(" ────────────────────────────────────────");
|
|
502
|
+
console.log("");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Post-init: add one or more fonts to an existing mithril-lynx project.
|
|
507
|
+
* Always copies the files; patches background.ts / style.css only when they
|
|
508
|
+
* still match the stock template (optionally with our FONTS block on top).
|
|
509
|
+
*/
|
|
510
|
+
async function runAddFont(args) {
|
|
511
|
+
intro("create-mithril-lynx add-font");
|
|
512
|
+
|
|
513
|
+
const android = parseAndroidArgs(args);
|
|
514
|
+
if (android.fontPath == null && android.findFontTerm == null) {
|
|
515
|
+
cancel("Pass --with-font <file.ttf> or --find-font <term>.");
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const projectRoot = findProjectRoot(cwd);
|
|
520
|
+
if (projectRoot == null) {
|
|
521
|
+
cancel(
|
|
522
|
+
"No mithril-lynx project found here (need lynx.config.ts + src/background.ts).\n" +
|
|
523
|
+
" cd into the app directory and try again.",
|
|
524
|
+
);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const incoming = await resolveFontsFromFlags(android, { baseDir: cwd, requireFonts: true });
|
|
529
|
+
if (incoming == null) return;
|
|
530
|
+
|
|
531
|
+
const bgPath = path.join(projectRoot, "src", "background.ts");
|
|
532
|
+
const { fonts: existingInBg } = stripFontBlock(
|
|
533
|
+
fs.existsSync(bgPath) ? fs.readFileSync(bgPath, "utf8") : "",
|
|
534
|
+
);
|
|
535
|
+
const merged = mergeFontLists(existingInBg, incoming, dedupeFontFiles);
|
|
536
|
+
|
|
537
|
+
const androidDir = findAndroidDir(projectRoot);
|
|
538
|
+
const copied = copyFontFiles({
|
|
539
|
+
projectRoot,
|
|
540
|
+
fonts: incoming,
|
|
541
|
+
androidDir,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
for (const item of copied) {
|
|
545
|
+
const rel = path.relative(cwd, item.path);
|
|
546
|
+
console.log(` → ${rel}${item.role === "android" ? " (android host)" : ""}`);
|
|
547
|
+
}
|
|
548
|
+
if (androidDir == null) {
|
|
549
|
+
console.log(
|
|
550
|
+
" ℹ No Android host found next to this app — fonts were only copied into src/assets/fonts/.\n" +
|
|
551
|
+
" (PROD asset:/// registration needs the host's AssetFontFaceLoader.)",
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const bgResult = applyBackgroundFonts(projectRoot, merged);
|
|
556
|
+
if (bgResult.ok) {
|
|
557
|
+
console.log(" ✔ Updated src/background.ts");
|
|
558
|
+
} else {
|
|
559
|
+
console.log(` ✖ Could not auto-edit src/background.ts (${bgResult.reason}).`);
|
|
560
|
+
console.log(" Paste this block at the top of src/background.ts:");
|
|
561
|
+
printManualBlock("src/background.ts", bgResult.manualBlock);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const cssResult = applyFontsCss(projectRoot, merged);
|
|
565
|
+
console.log(" ✔ Regenerated src/fonts.css (generated — don't edit it by hand)");
|
|
566
|
+
if (cssResult.ok && !cssResult.skipped) {
|
|
567
|
+
console.log(' ✔ Added @import "fonts.css"; to src/style.css');
|
|
568
|
+
} else if (!cssResult.ok) {
|
|
569
|
+
console.log(` ✖ Could not add the import to src/style.css (${cssResult.reason}).`);
|
|
570
|
+
console.log(" Add this line at the very top of your stylesheet:");
|
|
571
|
+
printManualBlock("src/style.css", cssResult.manualBlock);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (merged.length > 1) {
|
|
575
|
+
console.log(
|
|
576
|
+
` · Use the classes in src/fonts.css: ${merged.map((f) => "." + cssClassName(f.family)).join(", ")}`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
outro(
|
|
581
|
+
`Done. Font${merged.length > 1 ? "s" : ""} ${merged.map((f) => `"${f.family}"`).join(", ")} ` +
|
|
582
|
+
(bgResult.ok ? "registered." : "copied — finish registration with the lines above."),
|
|
583
|
+
);
|
|
445
584
|
}
|
|
446
585
|
|
|
447
586
|
function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
@@ -470,15 +609,11 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
|
470
609
|
// best-effort
|
|
471
610
|
}
|
|
472
611
|
|
|
473
|
-
// 2. Fonts
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
// data:
|
|
477
|
-
//
|
|
478
|
-
// any kind, renders the font correctly) and with no cold-start cost
|
|
479
|
-
// (three-run A/B on the same device: 724-745ms with the font vs.
|
|
480
|
-
// 715-826ms without — indistinguishable). See patchJsProject() for
|
|
481
|
-
// where the font actually gets wired up (entirely JS-side).
|
|
612
|
+
// 2. Fonts: the host always ships AssetFontFaceLoader +
|
|
613
|
+
// NoopGenericResourceFetcher (lynx#9431 workaround). Production
|
|
614
|
+
// bundles register fonts as asset:///fonts/<file>; Lynx Go / dev
|
|
615
|
+
// uses inlined data: URIs. See patchJsProject() for the JS wiring
|
|
616
|
+
// and templates/android/host/.../NoopGenericResourceFetcher.kt.
|
|
482
617
|
const sdkDir = findAndroidSdk();
|
|
483
618
|
|
|
484
619
|
// 3. Text substitutions across the whole Android host (skipping the
|
|
@@ -496,7 +631,17 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
|
496
631
|
],
|
|
497
632
|
]);
|
|
498
633
|
|
|
499
|
-
// 4.
|
|
634
|
+
// 4. Seed assets/fonts/ so the first APK build works even before
|
|
635
|
+
// scripts/android.mjs syncs (it also keeps them in sync later).
|
|
636
|
+
if (fonts.length > 0) {
|
|
637
|
+
const androidFontsDir = path.join(androidDir, "app", "src", "main", "assets", "fonts");
|
|
638
|
+
fs.mkdirSync(androidFontsDir, { recursive: true });
|
|
639
|
+
for (const { sourcePath, file } of fonts) {
|
|
640
|
+
fs.copyFileSync(sourcePath, path.join(androidFontsDir, file));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// 5. The script that joins the two halves, inside the JS project.
|
|
500
645
|
const scriptsDir = path.join(targetDir, "scripts");
|
|
501
646
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
502
647
|
fs.copyFileSync(path.join(ANDROID_TEMPLATE_ROOT, "app-scripts", "android.mjs"), path.join(scriptsDir, "android.mjs"));
|
|
@@ -527,76 +672,20 @@ function patchJsProject({ targetDir, android }) {
|
|
|
527
672
|
};
|
|
528
673
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
529
674
|
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
// bundler can import it. templates/_shared/ts/lynx.config.ts already
|
|
534
|
-
// sets `dataUriLimit: Infinity`, which inlines that import as a
|
|
535
|
-
// `data:font/ttf;base64,...` URI directly in the bundle — no separate
|
|
536
|
-
// file, no asset-path puzzle to solve for a packaged native host.
|
|
537
|
-
// - lynx.addFont() (not @font-face) registers it, called from
|
|
538
|
-
// background.ts. A data: URI resolves the same way on every host —
|
|
539
|
-
// confirmed on a real device with a stock generated Android host (no
|
|
540
|
-
// AssetFontFaceLoader, no custom resource fetcher) and with LynxExplorer/
|
|
541
|
-
// Lynx Go, with no cold-start cost (a three-run A/B on the same device
|
|
542
|
-
// measured 724-745ms with the font vs. 715-826ms without —
|
|
543
|
-
// indistinguishable; the old +1-2s regression was specific to
|
|
544
|
-
// @font-face's forced synchronous resolution, which doesn't apply here).
|
|
675
|
+
// Fonts: copy + register via lynx.addFont() loop (DEV data: / PROD
|
|
676
|
+
// asset:///) — see src/fonts-wire.js and the Android host's
|
|
677
|
+
// AssetFontFaceLoader / NoopGenericResourceFetcher (lynx#9431).
|
|
545
678
|
if (android.fonts.length > 0) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const addFontLines = [];
|
|
552
|
-
for (const { family, sourcePath, file } of android.fonts) {
|
|
553
|
-
fs.copyFileSync(sourcePath, path.join(fontsDir, file));
|
|
554
|
-
|
|
555
|
-
let varName = jsIdentifierFor(family);
|
|
556
|
-
while (usedVarNames.has(varName)) varName = `${varName}2`;
|
|
557
|
-
usedVarNames.add(varName);
|
|
558
|
-
|
|
559
|
-
importLines.push(`import ${varName} from "./assets/fonts/${file}";`);
|
|
560
|
-
addFontLines.push(
|
|
561
|
-
`lynx.addFont({ "font-family": "${family}", src: \`url("\${${varName}}")\` }, () => {});`,
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Only the first font gets an automatic CSS rule (the app-wide
|
|
566
|
-
// default, matching --with-font's single-font behavior exactly) —
|
|
567
|
-
// with more than one, there's no way to guess which elements should
|
|
568
|
-
// use which, so the rest are just registered via lynx.addFont() and
|
|
569
|
-
// left for you to assign in your own CSS classes.
|
|
570
|
-
const defaultFamily = android.fonts[0].family;
|
|
571
|
-
const extraFamilies = android.fonts.slice(1).map((f) => f.family);
|
|
572
|
-
|
|
679
|
+
copyFontFiles({
|
|
680
|
+
projectRoot: targetDir,
|
|
681
|
+
fonts: android.fonts,
|
|
682
|
+
androidDir: android.androidDir,
|
|
683
|
+
});
|
|
573
684
|
const bgPath = path.join(targetDir, "src", "background.ts");
|
|
574
|
-
const fontBlock = [
|
|
575
|
-
android.fonts.length === 1
|
|
576
|
-
? `// Font: "${defaultFamily}", bundled from src/assets/fonts/${android.fonts[0].file}. lynx.addFont()`
|
|
577
|
-
: `// Fonts: ${android.fonts.map((f) => `"${f.family}"`).join(", ")}, bundled from src/assets/fonts/. lynx.addFont()`,
|
|
578
|
-
"// registers each directly — dataUriLimit: Infinity (lynx.config.ts) inlines",
|
|
579
|
-
"// every import as a data: URI, which resolves the same way on every host,",
|
|
580
|
-
"// no native code needed.",
|
|
581
|
-
...(extraFamilies.length > 0
|
|
582
|
-
? [
|
|
583
|
-
`// Only "${defaultFamily}" got the automatic text { font-family: ... }`,
|
|
584
|
-
`// rule below — assign ${extraFamilies.map((f) => `"${f}"`).join(" / ")} to your own`,
|
|
585
|
-
"// classes in src/style.css, e.g. `.code { font-family: \"" + extraFamilies[0] + "\"; }`.",
|
|
586
|
-
]
|
|
587
|
-
: []),
|
|
588
|
-
...importLines,
|
|
589
|
-
...addFontLines,
|
|
590
|
-
"",
|
|
591
|
-
"",
|
|
592
|
-
].join("\n");
|
|
593
685
|
const existingBg = fs.existsSync(bgPath) ? fs.readFileSync(bgPath, "utf8") : "";
|
|
594
|
-
fs.writeFileSync(bgPath, `${
|
|
686
|
+
fs.writeFileSync(bgPath, `${buildFontBlock(android.fonts)}${existingBg}`);
|
|
595
687
|
|
|
596
|
-
|
|
597
|
-
const cssBlock = [`text {`, ` font-family: "${defaultFamily}", sans-serif;`, `}`, ""].join("\n");
|
|
598
|
-
const existingCss = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, "utf8") : "";
|
|
599
|
-
fs.writeFileSync(cssPath, `${cssBlock}\n${existingCss}`);
|
|
688
|
+
applyFontsCss(targetDir, android.fonts);
|
|
600
689
|
}
|
|
601
690
|
|
|
602
691
|
// README: how the two halves are used together.
|
|
@@ -622,7 +711,7 @@ function patchJsProject({ targetDir, android }) {
|
|
|
622
711
|
`- Application class: \`${android.appClass}\` · Activity: \`MainActivity\``,
|
|
623
712
|
...android.fonts.map(
|
|
624
713
|
(f) =>
|
|
625
|
-
`- Font \`${f.family}\`
|
|
714
|
+
`- Font \`${f.family}\` (\`src/assets/fonts/${f.file}\`): \`lynx.addFont()\` — DEV inlines a \`data:\` URI for Lynx Go; PROD uses \`asset:///fonts/${f.file}\` via \`AssetFontFaceLoader\` ([lynx#9431](https://github.com/lynx-family/lynx/issues/9431)).`,
|
|
626
715
|
),
|
|
627
716
|
"",
|
|
628
717
|
].join("\n");
|
|
@@ -638,6 +727,8 @@ create-mithril-lynx — scaffold a mithril-lynx app (and, optionally, its Androi
|
|
|
638
727
|
Usage:
|
|
639
728
|
npm create mithril-lynx@latest [name] [options]
|
|
640
729
|
npx create-mithril-lynx <name> --blank --android
|
|
730
|
+
npx create-mithril-lynx add-font --with-font ./Foo.ttf
|
|
731
|
+
npx create-mithril-lynx add-font --find-font Inter
|
|
641
732
|
|
|
642
733
|
Template (prompted for if omitted):
|
|
643
734
|
--hello-world | --blank | --basic-activity
|
|
@@ -648,9 +739,10 @@ Android host:
|
|
|
648
739
|
--android-id <id> applicationId / namespace (default com.example.<name>)
|
|
649
740
|
--app-name <name> launcher label (default: the project name)
|
|
650
741
|
--with-font <file.ttf> bundle the font into the JS project and register it
|
|
651
|
-
with lynx.addFont() (
|
|
652
|
-
|
|
653
|
-
|
|
742
|
+
with lynx.addFont() (DEV: data: URI for Lynx Go;
|
|
743
|
+
PROD: asset:/// + AssetFontFaceLoader — see README).
|
|
744
|
+
Comma-separate for more than one,
|
|
745
|
+
e.g. --with-font a.ttf,b.ttf
|
|
654
746
|
--find-font <term> search Fontsource (fontsource.org) for a font,
|
|
655
747
|
pick a family and a weight/style interactively,
|
|
656
748
|
and bundle it the same way as --with-font.
|
|
@@ -660,6 +752,15 @@ Android host:
|
|
|
660
752
|
--font-family <name> override the family name derived from the file
|
|
661
753
|
name (only valid with exactly one font)
|
|
662
754
|
|
|
755
|
+
Post-init (run inside an existing app directory):
|
|
756
|
+
add-font copy font file(s) into src/assets/fonts/ (and the
|
|
757
|
+
Android host's assets/fonts/ when present). If
|
|
758
|
+
background.ts / style.css still match the stock
|
|
759
|
+
template, wire them up automatically; otherwise
|
|
760
|
+
print the lines to paste by hand.
|
|
761
|
+
Same --with-font / --find-font / --font-family
|
|
762
|
+
flags as above.
|
|
763
|
+
|
|
663
764
|
Other:
|
|
664
765
|
--no-install don't install dependencies
|
|
665
766
|
-h, --help print this
|
|
@@ -671,6 +772,7 @@ async function main() {
|
|
|
671
772
|
// create-mithril-lynx my-app --basic-activity --no-install
|
|
672
773
|
// create-mithril-lynx my-app --blank --android --android-id com.acme.miapp
|
|
673
774
|
// create-mithril-lynx my-app --blank --with-font ./UbuntuMono-Regular.ttf
|
|
775
|
+
// create-mithril-lynx add-font --with-font ./Extra.ttf
|
|
674
776
|
const args = process.argv.slice(2);
|
|
675
777
|
|
|
676
778
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -678,6 +780,11 @@ async function main() {
|
|
|
678
780
|
return;
|
|
679
781
|
}
|
|
680
782
|
|
|
783
|
+
if (args[0] === "add-font") {
|
|
784
|
+
await runAddFont(args.slice(1));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
681
788
|
const positional = findPositional(args);
|
|
682
789
|
const templateFlag = TEMPLATE_VALUES.find((t) => args.includes(`--${t}`));
|
|
683
790
|
const noInstall = args.includes("--no-install");
|
|
@@ -810,7 +917,7 @@ async function main() {
|
|
|
810
917
|
`${androidDirName}/local.properties (sdk.dir=...).`
|
|
811
918
|
: `Android SDK found at ${sdkDir}.`,
|
|
812
919
|
fonts.length > 0
|
|
813
|
-
? `Font${fonts.length > 1 ? "s" : ""} ${fonts.map((f) => `"${f.family}"`).join(", ")}
|
|
920
|
+
? `Font${fonts.length > 1 ? "s" : ""} ${fonts.map((f) => `"${f.family}"`).join(", ")} registered via lynx.addFont() — Lynx Go (data:) and the APK (asset:///) both covered.`
|
|
814
921
|
: "No custom font — pass --with-font <file.ttf> (or --find-font <term>) to bundle one.",
|
|
815
922
|
];
|
|
816
923
|
outro(`Done! Next steps:\n\n ${steps.join("\n ")}\n\n${notes.join("\n")}`);
|
|
@@ -25,7 +25,11 @@ declare const module: {
|
|
|
25
25
|
accept(path: string, callback: () => void): void;
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
|
-
|
|
28
|
+
// Overloads: HMR reloads index.js; DEV font registration (prepended by
|
|
29
|
+
// create-mithril-lynx when --with-font/--find-font is used) requires .ttf
|
|
30
|
+
// paths as string URLs.
|
|
31
|
+
declare function require(id: "./index.js"): typeof indexModule;
|
|
32
|
+
declare function require(id: string): string;
|
|
29
33
|
|
|
30
34
|
if (module.hot) {
|
|
31
35
|
module.hot.accept("./index.js", () => {
|
|
@@ -34,6 +34,13 @@ const BUNDLE_NAME = "main-thread.bundle";
|
|
|
34
34
|
const BUNDLE_SRC = path.join(projectRoot, "dist", BUNDLE_NAME);
|
|
35
35
|
const BUNDLE_DEST = path.join(ANDROID_DIR, "app", "src", "main", "assets", BUNDLE_NAME);
|
|
36
36
|
|
|
37
|
+
// Custom fonts (src/assets/fonts/*.ttf) — production bundles resolve them via
|
|
38
|
+
// asset:///fonts/<file> (AssetFontFaceLoader in the Android host). Keep the
|
|
39
|
+
// host's assets/fonts/ in sync with the JS project on every android sync.
|
|
40
|
+
const FONTS_SRC = path.join(projectRoot, "src", "assets", "fonts");
|
|
41
|
+
const FONTS_DEST = path.join(ANDROID_DIR, "app", "src", "main", "assets", "fonts");
|
|
42
|
+
const FONT_EXT = /\.(ttf|otf|ttc)$/i;
|
|
43
|
+
|
|
37
44
|
const USAGE = `
|
|
38
45
|
Usage: npm run android -- [flags] [-- gradle-args]
|
|
39
46
|
|
|
@@ -97,6 +104,19 @@ function requireAndroidDir() {
|
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
function syncFonts() {
|
|
108
|
+
if (!fs.existsSync(FONTS_SRC)) return;
|
|
109
|
+
const files = fs.readdirSync(FONTS_SRC).filter((name) => FONT_EXT.test(name));
|
|
110
|
+
if (files.length === 0) return;
|
|
111
|
+
fs.mkdirSync(FONTS_DEST, { recursive: true });
|
|
112
|
+
for (const name of files) {
|
|
113
|
+
const dest = path.join(FONTS_DEST, name);
|
|
114
|
+
fs.copyFileSync(path.join(FONTS_SRC, name), dest);
|
|
115
|
+
const kb = (fs.statSync(dest).size / 1024).toFixed(1);
|
|
116
|
+
console.log(` → ${path.relative(projectRoot, dest)} (${kb} kB)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
function syncBundle() {
|
|
101
121
|
if (!fs.existsSync(BUNDLE_SRC)) {
|
|
102
122
|
fail(`${path.relative(projectRoot, BUNDLE_SRC)} doesn't exist. Run "npm run build" first (or drop --no-build).`);
|
|
@@ -106,6 +126,7 @@ function syncBundle() {
|
|
|
106
126
|
fs.copyFileSync(BUNDLE_SRC, BUNDLE_DEST);
|
|
107
127
|
const kb = (fs.statSync(BUNDLE_DEST).size / 1024).toFixed(1);
|
|
108
128
|
console.log(` → ${path.relative(projectRoot, BUNDLE_DEST)} (${kb} kB)`);
|
|
129
|
+
syncFonts();
|
|
109
130
|
}
|
|
110
131
|
|
|
111
132
|
function generateKeystore() {
|
|
@@ -3,6 +3,7 @@ package {{PACKAGE_NAME}}
|
|
|
3
3
|
import android.app.Application
|
|
4
4
|
import com.lynx.service.log.LynxLogService
|
|
5
5
|
import com.lynx.tasm.LynxEnv
|
|
6
|
+
import com.lynx.tasm.loader.LynxFontFaceLoader
|
|
6
7
|
import com.lynx.tasm.service.LynxServiceCenter
|
|
7
8
|
|
|
8
9
|
class {{APP_CLASS}} : Application() {
|
|
@@ -16,6 +17,12 @@ class {{APP_CLASS}} : Application() {
|
|
|
16
17
|
LynxServiceCenter.inst().registerService(LynxLogService)
|
|
17
18
|
LynxLogService.switchLogToSystem(true)
|
|
18
19
|
|
|
20
|
+
// Lets FontFaceManager resolve "asset:///" (production fonts under
|
|
21
|
+
// assets/fonts/) — see AssetFontFaceLoader. Pairs with
|
|
22
|
+
// NoopGenericResourceFetcher in MainActivity for the fast @font-face
|
|
23
|
+
// path (https://github.com/lynx-family/lynx/issues/9431).
|
|
24
|
+
LynxFontFaceLoader.setLoader(AssetFontFaceLoader)
|
|
25
|
+
|
|
19
26
|
LynxEnv.inst().init(this, null, null, null)
|
|
20
27
|
}
|
|
21
28
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.graphics.Typeface
|
|
4
|
+
import com.lynx.tasm.behavior.LynxContext
|
|
5
|
+
import com.lynx.tasm.fontface.FontFace
|
|
6
|
+
import com.lynx.tasm.loader.LynxFontFaceLoader
|
|
7
|
+
|
|
8
|
+
// Public Lynx extension point (com.lynx.tasm.loader.LynxFontFaceLoader, in the
|
|
9
|
+
// core `lynx` artifact) for resolving custom @font-face / addFont src schemes.
|
|
10
|
+
// Without a registered Loader, Lynx's default never resolves "asset:///" —
|
|
11
|
+
// FontFaceManager only handles asset:/// inline when a "FONT"
|
|
12
|
+
// LynxResourceProvider is registered (we don't), and prefetchFont()'s
|
|
13
|
+
// non-http/non-data: branch goes ONLY through this Loader.
|
|
14
|
+
//
|
|
15
|
+
// Production builds register fonts as asset:///fonts/<file> (see the generated
|
|
16
|
+
// background.ts) so the APK stays small and cold-start stays fast; Lynx Go /
|
|
17
|
+
// `rspeedy dev` still uses inlined data: URIs via NoopGenericResourceFetcher.
|
|
18
|
+
// See https://github.com/lynx-family/lynx/issues/9431
|
|
19
|
+
object AssetFontFaceLoader : LynxFontFaceLoader.Loader() {
|
|
20
|
+
private const val ASSET_PREFIX = "asset:///"
|
|
21
|
+
|
|
22
|
+
override fun onLoadFontFace(
|
|
23
|
+
context: LynxContext,
|
|
24
|
+
type: FontFace.TYPE,
|
|
25
|
+
src: String,
|
|
26
|
+
): Typeface? {
|
|
27
|
+
if (!src.startsWith(ASSET_PREFIX)) return null
|
|
28
|
+
return try {
|
|
29
|
+
Typeface.createFromAsset(context.context.assets, src.removePrefix(ASSET_PREFIX))
|
|
30
|
+
} catch (e: Exception) {
|
|
31
|
+
null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -3,6 +3,7 @@ package {{PACKAGE_NAME}}
|
|
|
3
3
|
import android.os.Bundle
|
|
4
4
|
import androidx.appcompat.app.AppCompatActivity
|
|
5
5
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
6
|
+
import com.lynx.tasm.LynxBooleanOption
|
|
6
7
|
import com.lynx.tasm.LynxViewBuilder
|
|
7
8
|
import com.lynx.tasm.ThreadStrategyForRendering
|
|
8
9
|
import com.lynx.xelement.XElementBehaviors
|
|
@@ -19,6 +20,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
19
20
|
// keyboard. Remove this line (and the xelement dependencies) if you
|
|
20
21
|
// don't use them.
|
|
21
22
|
builder.addBehaviors(XElementBehaviors().create())
|
|
23
|
+
// lynx-family/lynx's own explorer/android registers a
|
|
24
|
+
// GenericResourceFetcher unconditionally (LynxViewShellActivity —
|
|
25
|
+
// "used inside LynxEngine for resource loading capabilities of
|
|
26
|
+
// components such as Text"). Without one, @font-face / addFont
|
|
27
|
+
// data: URIs fall back to a ~1.4-1.5s legacy path — see
|
|
28
|
+
// NoopGenericResourceFetcher and
|
|
29
|
+
// https://github.com/lynx-family/lynx/issues/9431
|
|
30
|
+
builder.setEnableGenericResourceFetcher(LynxBooleanOption.TRUE)
|
|
31
|
+
builder.setGenericResourceFetcher(NoopGenericResourceFetcher())
|
|
22
32
|
builder.setThreadStrategyForRendering(ThreadStrategyForRendering.ALL_ON_UI)
|
|
23
33
|
// Reads the bundle from assets on a separate thread — see
|
|
24
34
|
// AssetTemplateProvider's comment: reading it synchronously here cost
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.util.Base64
|
|
4
|
+
import com.lynx.tasm.resourceprovider.LynxResourceCallback
|
|
5
|
+
import com.lynx.tasm.resourceprovider.LynxResourceRequest
|
|
6
|
+
import com.lynx.tasm.resourceprovider.LynxResourceResponse
|
|
7
|
+
import com.lynx.tasm.resourceprovider.generic.LynxGenericResourceFetcher
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Decodes `data:` URIs for @font-face / lynx.addFont() src resolution —
|
|
11
|
+
* nothing else (this host never fetches real remote resources this way).
|
|
12
|
+
*
|
|
13
|
+
* Root cause of the ~1.4-1.5s cold-start cost custom fonts added, found by
|
|
14
|
+
* diffing LynxExplorer's logcat while it loaded a @font-face-heavy bundle
|
|
15
|
+
* fast (2026-09-10): it logs `FontFaceManager: Try to loadTypeface with
|
|
16
|
+
* GenericLynxResourceFetcher` → success in ~13ms. @font-face src resolution
|
|
17
|
+
* — even for a `data:` URI — routes through
|
|
18
|
+
* LynxGenericResourceFetcher.fetchResource(); WITHOUT one registered, that
|
|
19
|
+
* fast path fails and the engine falls back to a much slower legacy
|
|
20
|
+
* font-loading path (~1.4-1.5s fixed cost).
|
|
21
|
+
*
|
|
22
|
+
* Tracked upstream: https://github.com/lynx-family/lynx/issues/9431
|
|
23
|
+
*/
|
|
24
|
+
class NoopGenericResourceFetcher : LynxGenericResourceFetcher() {
|
|
25
|
+
@Suppress("UNCHECKED_CAST")
|
|
26
|
+
override fun fetchResource(request: LynxResourceRequest, callback: LynxResourceCallback<ByteArray>) {
|
|
27
|
+
val url = request.url
|
|
28
|
+
val commaIndex = url.indexOf(',')
|
|
29
|
+
if (url.startsWith("data:") && commaIndex != -1) {
|
|
30
|
+
try {
|
|
31
|
+
val bytes = Base64.decode(url.substring(commaIndex + 1), Base64.DEFAULT)
|
|
32
|
+
callback.onResponse(LynxResourceResponse.onSuccess(bytes))
|
|
33
|
+
return
|
|
34
|
+
} catch (e: IllegalArgumentException) {
|
|
35
|
+
// fall through to failure below
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
val response = LynxResourceResponse.onFailed(Throwable("not supported: $url")) as LynxResourceResponse<ByteArray>
|
|
39
|
+
callback.onResponse(response)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Suppress("UNCHECKED_CAST")
|
|
43
|
+
override fun fetchResourcePath(request: LynxResourceRequest, callback: LynxResourceCallback<String>) {
|
|
44
|
+
val response = LynxResourceResponse.onFailed(Throwable("not supported")) as LynxResourceResponse<String>
|
|
45
|
+
callback.onResponse(response)
|
|
46
|
+
}
|
|
47
|
+
}
|