@jant/core 0.6.15 → 0.6.16
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/bin/commands/import-site.js +7 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +1 -1
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/client/__tests__/compose-bridge.test.ts +252 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/toast.test.ts +65 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
- package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +47 -3
- package/src/client/components/jant-command-palette.ts +12 -6
- package/src/client/components/jant-compose-dialog.ts +1464 -680
- package/src/client/components/jant-compose-editor.ts +225 -93
- package/src/client/components/jant-compose-fullscreen.ts +3 -5
- package/src/client/components/jant-post-menu.ts +91 -21
- package/src/client/compose-bridge.ts +143 -122
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/post-refresh.ts +135 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/toast.ts +130 -38
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +20 -0
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/locales/public/en.po +89 -21
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +88 -20
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +88 -20
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/hugo-markdown.ts +5 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/post-display.ts +42 -9
- package/src/lib/timeline.ts +39 -9
- package/src/lib/tiptap-render.ts +4 -2
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +28 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/posts.ts +13 -9
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/posts.ts +7 -0
- package/src/routes/compose.tsx +6 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/archive.tsx +65 -17
- package/src/routes/pages/page.tsx +5 -2
- package/src/services/__tests__/collection.test.ts +32 -1
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +277 -0
- package/src/services/collection.ts +8 -10
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export.ts +3 -5
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +297 -100
- package/src/services/search.ts +9 -0
- package/src/styles/components.css +0 -14
- package/src/styles/site-media.css +4 -4
- package/src/styles/tokens.css +62 -1
- package/src/styles/ui.css +1363 -1283
- package/src/types/entities.ts +5 -0
- package/src/types/props.ts +12 -0
- package/src/types/views.ts +6 -0
- package/src/ui/compose/ComposeDialog.tsx +17 -17
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/PostFooter.tsx +35 -12
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/post-article-attributes.ts +6 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
|
@@ -11,6 +11,429 @@ var __exportAll = (all, no_symbols) => {
|
|
|
11
11
|
return target;
|
|
12
12
|
};
|
|
13
13
|
//#endregion
|
|
14
|
+
//#region src/lib/translit.ts
|
|
15
|
+
/**
|
|
16
|
+
* Dictionary-free transliteration helpers for slug generation.
|
|
17
|
+
*
|
|
18
|
+
* These converters cover the scripts limax handles poorly:
|
|
19
|
+
*
|
|
20
|
+
* - Korean hangul → Revised Romanization (limax strips hangul entirely)
|
|
21
|
+
* - Japanese kana → Hepburn romaji (limax mangles long vowels and digraphs)
|
|
22
|
+
*
|
|
23
|
+
* Both are pure algorithms over Unicode code points — no dictionaries — so
|
|
24
|
+
* they stay small enough for the Workers bundle and the lazy client chunk.
|
|
25
|
+
* Japanese kanji is intentionally out of scope: readings are context-dependent
|
|
26
|
+
* and need a morphological dictionary, which is why `slugify` falls back to a
|
|
27
|
+
* random ID for kanji-heavy Japanese titles instead of guessing.
|
|
28
|
+
*/ /** Matches hiragana, katakana, and the chōonpu (ー) long-vowel mark. */ var KANA_RE = /[ぁ-ゖァ-ヺー]/u;
|
|
29
|
+
/** Matches precomposed hangul syllables and compatibility jamo. */ var HANGUL_RE = /[가-힣ㄱ-ㆎ]/u;
|
|
30
|
+
/** Matches Han (CJK ideograph) characters. */ var HAN_RE = /\p{Script=Han}/u;
|
|
31
|
+
var HANGUL_SYLLABLE_BASE = 44032;
|
|
32
|
+
var HANGUL_SYLLABLE_END = 55203;
|
|
33
|
+
var VOWEL_COUNT = 21;
|
|
34
|
+
var FINAL_COUNT = 28;
|
|
35
|
+
var RR_INITIALS = [
|
|
36
|
+
"g",
|
|
37
|
+
"kk",
|
|
38
|
+
"n",
|
|
39
|
+
"d",
|
|
40
|
+
"tt",
|
|
41
|
+
"r",
|
|
42
|
+
"m",
|
|
43
|
+
"b",
|
|
44
|
+
"pp",
|
|
45
|
+
"s",
|
|
46
|
+
"ss",
|
|
47
|
+
"",
|
|
48
|
+
"j",
|
|
49
|
+
"jj",
|
|
50
|
+
"ch",
|
|
51
|
+
"k",
|
|
52
|
+
"t",
|
|
53
|
+
"p",
|
|
54
|
+
"h"
|
|
55
|
+
];
|
|
56
|
+
var RR_VOWELS = [
|
|
57
|
+
"a",
|
|
58
|
+
"ae",
|
|
59
|
+
"ya",
|
|
60
|
+
"yae",
|
|
61
|
+
"eo",
|
|
62
|
+
"e",
|
|
63
|
+
"yeo",
|
|
64
|
+
"ye",
|
|
65
|
+
"o",
|
|
66
|
+
"wa",
|
|
67
|
+
"wae",
|
|
68
|
+
"oe",
|
|
69
|
+
"yo",
|
|
70
|
+
"u",
|
|
71
|
+
"wo",
|
|
72
|
+
"we",
|
|
73
|
+
"wi",
|
|
74
|
+
"yu",
|
|
75
|
+
"eu",
|
|
76
|
+
"ui",
|
|
77
|
+
"i"
|
|
78
|
+
];
|
|
79
|
+
var RR_FINALS = [
|
|
80
|
+
"",
|
|
81
|
+
"k",
|
|
82
|
+
"k",
|
|
83
|
+
"k",
|
|
84
|
+
"n",
|
|
85
|
+
"n",
|
|
86
|
+
"n",
|
|
87
|
+
"t",
|
|
88
|
+
"l",
|
|
89
|
+
"k",
|
|
90
|
+
"m",
|
|
91
|
+
"l",
|
|
92
|
+
"l",
|
|
93
|
+
"l",
|
|
94
|
+
"p",
|
|
95
|
+
"l",
|
|
96
|
+
"m",
|
|
97
|
+
"p",
|
|
98
|
+
"p",
|
|
99
|
+
"t",
|
|
100
|
+
"t",
|
|
101
|
+
"ng",
|
|
102
|
+
"t",
|
|
103
|
+
"t",
|
|
104
|
+
"k",
|
|
105
|
+
"t",
|
|
106
|
+
"p",
|
|
107
|
+
"t"
|
|
108
|
+
];
|
|
109
|
+
var RR_LIAISON = [
|
|
110
|
+
["", ""],
|
|
111
|
+
["", "g"],
|
|
112
|
+
["", "kk"],
|
|
113
|
+
["k", "s"],
|
|
114
|
+
["", "n"],
|
|
115
|
+
["n", "j"],
|
|
116
|
+
["n", ""],
|
|
117
|
+
["", "d"],
|
|
118
|
+
["", "r"],
|
|
119
|
+
["l", "g"],
|
|
120
|
+
["l", "m"],
|
|
121
|
+
["l", "b"],
|
|
122
|
+
["l", "s"],
|
|
123
|
+
["l", "t"],
|
|
124
|
+
["l", "p"],
|
|
125
|
+
["l", ""],
|
|
126
|
+
["", "m"],
|
|
127
|
+
["", "b"],
|
|
128
|
+
["p", "s"],
|
|
129
|
+
["", "s"],
|
|
130
|
+
["", "ss"],
|
|
131
|
+
["ng", ""],
|
|
132
|
+
["", "j"],
|
|
133
|
+
["", "ch"],
|
|
134
|
+
["", "k"],
|
|
135
|
+
["", "t"],
|
|
136
|
+
["", "p"],
|
|
137
|
+
["", ""]
|
|
138
|
+
];
|
|
139
|
+
var COMPAT_JAMO = {
|
|
140
|
+
ㄱ: "g",
|
|
141
|
+
ㄲ: "kk",
|
|
142
|
+
ㄴ: "n",
|
|
143
|
+
ㄷ: "d",
|
|
144
|
+
ㄸ: "tt",
|
|
145
|
+
ㄹ: "r",
|
|
146
|
+
ㅁ: "m",
|
|
147
|
+
ㅂ: "b",
|
|
148
|
+
ㅃ: "pp",
|
|
149
|
+
ㅅ: "s",
|
|
150
|
+
ㅆ: "ss",
|
|
151
|
+
ㅇ: "ng",
|
|
152
|
+
ㅈ: "j",
|
|
153
|
+
ㅉ: "jj",
|
|
154
|
+
ㅊ: "ch",
|
|
155
|
+
ㅋ: "k",
|
|
156
|
+
ㅌ: "t",
|
|
157
|
+
ㅍ: "p",
|
|
158
|
+
ㅎ: "h",
|
|
159
|
+
ㅏ: "a",
|
|
160
|
+
ㅐ: "ae",
|
|
161
|
+
ㅑ: "ya",
|
|
162
|
+
ㅒ: "yae",
|
|
163
|
+
ㅓ: "eo",
|
|
164
|
+
ㅔ: "e",
|
|
165
|
+
ㅕ: "yeo",
|
|
166
|
+
ㅖ: "ye",
|
|
167
|
+
ㅗ: "o",
|
|
168
|
+
ㅘ: "wa",
|
|
169
|
+
ㅙ: "wae",
|
|
170
|
+
ㅚ: "oe",
|
|
171
|
+
ㅛ: "yo",
|
|
172
|
+
ㅜ: "u",
|
|
173
|
+
ㅝ: "wo",
|
|
174
|
+
ㅞ: "we",
|
|
175
|
+
ㅟ: "wi",
|
|
176
|
+
ㅠ: "yu",
|
|
177
|
+
ㅡ: "eu",
|
|
178
|
+
ㅢ: "ui",
|
|
179
|
+
ㅣ: "i"
|
|
180
|
+
};
|
|
181
|
+
function decomposeHangul(codePoint) {
|
|
182
|
+
if (codePoint < HANGUL_SYLLABLE_BASE || codePoint > HANGUL_SYLLABLE_END) return null;
|
|
183
|
+
const index = codePoint - HANGUL_SYLLABLE_BASE;
|
|
184
|
+
return {
|
|
185
|
+
initial: Math.floor(index / (VOWEL_COUNT * FINAL_COUNT)),
|
|
186
|
+
vowel: Math.floor(index / FINAL_COUNT) % VOWEL_COUNT,
|
|
187
|
+
final: index % FINAL_COUNT
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Transliterates Korean hangul to Latin using Revised Romanization.
|
|
192
|
+
*
|
|
193
|
+
* Handles precomposed syllables with basic liaison (a final consonant before
|
|
194
|
+
* a silent-ㅇ syllable carries over as its initial) and standalone
|
|
195
|
+
* compatibility jamo. Assimilation sound rules (e.g. 종로 → "Jongno") are
|
|
196
|
+
* intentionally skipped — slugs need recognizable output, not phonetic
|
|
197
|
+
* perfection. Non-hangul characters pass through unchanged.
|
|
198
|
+
*
|
|
199
|
+
* @param text - Input text, possibly mixing hangul with other characters
|
|
200
|
+
* @returns Text with hangul replaced by its romanization
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* hangulToLatin("안녕하세요"); // "annyeonghaseyo"
|
|
205
|
+
* hangulToLatin("한국어"); // "hangugeo" (liaison)
|
|
206
|
+
* hangulToLatin("개발 일지"); // "gaebal ilji"
|
|
207
|
+
* ```
|
|
208
|
+
*/ function hangulToLatin(text) {
|
|
209
|
+
const chars = [...text];
|
|
210
|
+
let out = "";
|
|
211
|
+
let carried = null;
|
|
212
|
+
for (const [i, char] of chars.entries()) {
|
|
213
|
+
const syllable = decomposeHangul(char.codePointAt(0) ?? 0);
|
|
214
|
+
if (!syllable) {
|
|
215
|
+
carried = null;
|
|
216
|
+
out += COMPAT_JAMO[char] ?? char;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const initial = carried ?? RR_INITIALS[syllable.initial] ?? "";
|
|
220
|
+
carried = null;
|
|
221
|
+
const next = decomposeHangul(chars[i + 1]?.codePointAt(0) ?? 0);
|
|
222
|
+
let final = RR_FINALS[syllable.final] ?? "";
|
|
223
|
+
if (syllable.final !== 0 && next && RR_INITIALS[next.initial] === "") {
|
|
224
|
+
const [remains, carry] = RR_LIAISON[syllable.final] ?? ["", ""];
|
|
225
|
+
final = remains;
|
|
226
|
+
carried = carry;
|
|
227
|
+
}
|
|
228
|
+
out += initial + (RR_VOWELS[syllable.vowel] ?? "") + final;
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
var KANA_BASE = {
|
|
233
|
+
あ: "a",
|
|
234
|
+
い: "i",
|
|
235
|
+
う: "u",
|
|
236
|
+
え: "e",
|
|
237
|
+
お: "o",
|
|
238
|
+
か: "ka",
|
|
239
|
+
き: "ki",
|
|
240
|
+
く: "ku",
|
|
241
|
+
け: "ke",
|
|
242
|
+
こ: "ko",
|
|
243
|
+
さ: "sa",
|
|
244
|
+
し: "shi",
|
|
245
|
+
す: "su",
|
|
246
|
+
せ: "se",
|
|
247
|
+
そ: "so",
|
|
248
|
+
た: "ta",
|
|
249
|
+
ち: "chi",
|
|
250
|
+
つ: "tsu",
|
|
251
|
+
て: "te",
|
|
252
|
+
と: "to",
|
|
253
|
+
な: "na",
|
|
254
|
+
に: "ni",
|
|
255
|
+
ぬ: "nu",
|
|
256
|
+
ね: "ne",
|
|
257
|
+
の: "no",
|
|
258
|
+
は: "ha",
|
|
259
|
+
ひ: "hi",
|
|
260
|
+
ふ: "fu",
|
|
261
|
+
へ: "he",
|
|
262
|
+
ほ: "ho",
|
|
263
|
+
ま: "ma",
|
|
264
|
+
み: "mi",
|
|
265
|
+
む: "mu",
|
|
266
|
+
め: "me",
|
|
267
|
+
も: "mo",
|
|
268
|
+
や: "ya",
|
|
269
|
+
ゆ: "yu",
|
|
270
|
+
よ: "yo",
|
|
271
|
+
ら: "ra",
|
|
272
|
+
り: "ri",
|
|
273
|
+
る: "ru",
|
|
274
|
+
れ: "re",
|
|
275
|
+
ろ: "ro",
|
|
276
|
+
わ: "wa",
|
|
277
|
+
ゐ: "i",
|
|
278
|
+
ゑ: "e",
|
|
279
|
+
を: "o",
|
|
280
|
+
ん: "n",
|
|
281
|
+
が: "ga",
|
|
282
|
+
ぎ: "gi",
|
|
283
|
+
ぐ: "gu",
|
|
284
|
+
げ: "ge",
|
|
285
|
+
ご: "go",
|
|
286
|
+
ざ: "za",
|
|
287
|
+
じ: "ji",
|
|
288
|
+
ず: "zu",
|
|
289
|
+
ぜ: "ze",
|
|
290
|
+
ぞ: "zo",
|
|
291
|
+
だ: "da",
|
|
292
|
+
ぢ: "ji",
|
|
293
|
+
づ: "zu",
|
|
294
|
+
で: "de",
|
|
295
|
+
ど: "do",
|
|
296
|
+
ば: "ba",
|
|
297
|
+
び: "bi",
|
|
298
|
+
ぶ: "bu",
|
|
299
|
+
べ: "be",
|
|
300
|
+
ぼ: "bo",
|
|
301
|
+
ぱ: "pa",
|
|
302
|
+
ぴ: "pi",
|
|
303
|
+
ぷ: "pu",
|
|
304
|
+
ぺ: "pe",
|
|
305
|
+
ぽ: "po",
|
|
306
|
+
ゔ: "vu",
|
|
307
|
+
ぁ: "a",
|
|
308
|
+
ぃ: "i",
|
|
309
|
+
ぅ: "u",
|
|
310
|
+
ぇ: "e",
|
|
311
|
+
ぉ: "o",
|
|
312
|
+
ゃ: "ya",
|
|
313
|
+
ゅ: "yu",
|
|
314
|
+
ょ: "yo",
|
|
315
|
+
ゎ: "wa"
|
|
316
|
+
};
|
|
317
|
+
var KANA_COMBO = {
|
|
318
|
+
きゃ: "kya",
|
|
319
|
+
きゅ: "kyu",
|
|
320
|
+
きょ: "kyo",
|
|
321
|
+
ぎゃ: "gya",
|
|
322
|
+
ぎゅ: "gyu",
|
|
323
|
+
ぎょ: "gyo",
|
|
324
|
+
しゃ: "sha",
|
|
325
|
+
しゅ: "shu",
|
|
326
|
+
しょ: "sho",
|
|
327
|
+
じゃ: "ja",
|
|
328
|
+
じゅ: "ju",
|
|
329
|
+
じょ: "jo",
|
|
330
|
+
ちゃ: "cha",
|
|
331
|
+
ちゅ: "chu",
|
|
332
|
+
ちょ: "cho",
|
|
333
|
+
ぢゃ: "ja",
|
|
334
|
+
ぢゅ: "ju",
|
|
335
|
+
ぢょ: "jo",
|
|
336
|
+
にゃ: "nya",
|
|
337
|
+
にゅ: "nyu",
|
|
338
|
+
にょ: "nyo",
|
|
339
|
+
ひゃ: "hya",
|
|
340
|
+
ひゅ: "hyu",
|
|
341
|
+
ひょ: "hyo",
|
|
342
|
+
びゃ: "bya",
|
|
343
|
+
びゅ: "byu",
|
|
344
|
+
びょ: "byo",
|
|
345
|
+
ぴゃ: "pya",
|
|
346
|
+
ぴゅ: "pyu",
|
|
347
|
+
ぴょ: "pyo",
|
|
348
|
+
みゃ: "mya",
|
|
349
|
+
みゅ: "myu",
|
|
350
|
+
みょ: "myo",
|
|
351
|
+
りゃ: "rya",
|
|
352
|
+
りゅ: "ryu",
|
|
353
|
+
りょ: "ryo",
|
|
354
|
+
ふぁ: "fa",
|
|
355
|
+
ふぃ: "fi",
|
|
356
|
+
ふぇ: "fe",
|
|
357
|
+
ふぉ: "fo",
|
|
358
|
+
てぃ: "ti",
|
|
359
|
+
でぃ: "di",
|
|
360
|
+
とぅ: "tu",
|
|
361
|
+
どぅ: "du",
|
|
362
|
+
うぃ: "wi",
|
|
363
|
+
うぇ: "we",
|
|
364
|
+
うぉ: "wo",
|
|
365
|
+
ゔぁ: "va",
|
|
366
|
+
ゔぃ: "vi",
|
|
367
|
+
ゔぇ: "ve",
|
|
368
|
+
ゔぉ: "vo",
|
|
369
|
+
ちぇ: "che",
|
|
370
|
+
しぇ: "she",
|
|
371
|
+
じぇ: "je",
|
|
372
|
+
つぁ: "tsa",
|
|
373
|
+
つぃ: "tsi",
|
|
374
|
+
つぇ: "tse",
|
|
375
|
+
つぉ: "tso",
|
|
376
|
+
いぇ: "ye"
|
|
377
|
+
};
|
|
378
|
+
var KATAKANA_START = 12449;
|
|
379
|
+
var KATAKANA_END = 12534;
|
|
380
|
+
var KATAKANA_TO_HIRAGANA_SHIFT = 96;
|
|
381
|
+
var SOKUON = "っ";
|
|
382
|
+
var CHOONPU = "ー";
|
|
383
|
+
function normalizeToHiragana(char) {
|
|
384
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
385
|
+
if (codePoint >= KATAKANA_START && codePoint <= KATAKANA_END) return String.fromCodePoint(codePoint - KATAKANA_TO_HIRAGANA_SHIFT);
|
|
386
|
+
return char;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Transliterates Japanese kana to macron-less Hepburn romaji.
|
|
390
|
+
*
|
|
391
|
+
* Handles hiragana and katakana, digraphs (きゃ → "kya"), extended katakana
|
|
392
|
+
* loan combos (ファ → "fa"), sokuon gemination (まっちゃ → "matcha"), and
|
|
393
|
+
* drops the chōonpu long-vowel mark (ハンバーガー → "hanbaga"), matching how
|
|
394
|
+
* macrons are conventionally stripped in URLs. Kanji and other non-kana
|
|
395
|
+
* characters pass through unchanged.
|
|
396
|
+
*
|
|
397
|
+
* @param text - Input text, possibly mixing kana with other characters
|
|
398
|
+
* @returns Text with kana replaced by romaji
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* kanaToRomaji("カタカナタイトル"); // "katakanataitoru"
|
|
403
|
+
* kanaToRomaji("ハンバーガー"); // "hanbaga"
|
|
404
|
+
* kanaToRomaji("まっちゃ"); // "matcha"
|
|
405
|
+
* ```
|
|
406
|
+
*/ function kanaToRomaji(text) {
|
|
407
|
+
const chars = [...text].map(normalizeToHiragana);
|
|
408
|
+
let out = "";
|
|
409
|
+
let geminate = false;
|
|
410
|
+
for (let i = 0; i < chars.length; i++) {
|
|
411
|
+
const char = chars[i] ?? "";
|
|
412
|
+
if (char === SOKUON) {
|
|
413
|
+
geminate = true;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (char === CHOONPU) {
|
|
417
|
+
geminate = false;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const combo = KANA_COMBO[char + (chars[i + 1] ?? "")];
|
|
421
|
+
const romaji = combo ?? KANA_BASE[char];
|
|
422
|
+
if (combo) i++;
|
|
423
|
+
if (romaji === void 0) {
|
|
424
|
+
geminate = false;
|
|
425
|
+
out += char;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (geminate) {
|
|
429
|
+
geminate = false;
|
|
430
|
+
if (/^[^aeiou]/.test(romaji)) out += romaji.startsWith("ch") ? "t" : romaji[0] ?? "";
|
|
431
|
+
}
|
|
432
|
+
out += romaji;
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
14
437
|
//#region src/lib/url.ts
|
|
15
438
|
/**
|
|
16
439
|
* URL Utilities
|
|
@@ -26,6 +449,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
26
449
|
normalizePath: () => normalizePath,
|
|
27
450
|
normalizeSitePathPrefix: () => normalizeSitePathPrefix,
|
|
28
451
|
normalizeSiteUrl: () => normalizeSiteUrl,
|
|
452
|
+
sanitizeRichTextHref: () => sanitizeRichTextHref,
|
|
29
453
|
sanitizeUrl: () => sanitizeUrl,
|
|
30
454
|
slugify: () => slugify,
|
|
31
455
|
stripSitePathPrefix: () => stripSitePathPrefix,
|
|
@@ -148,14 +572,54 @@ function normalizeSitePathname(pathname) {
|
|
|
148
572
|
if (parsed.hostname !== reference.hostname) return null;
|
|
149
573
|
return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/";
|
|
150
574
|
}
|
|
575
|
+
var HAN_GLOBAL_RE = /\p{Script=Han}/gu;
|
|
576
|
+
var CONTENT_CHAR_RE = /[\p{L}\p{N}]/u;
|
|
577
|
+
var ASCII_ALNUM_RE = /[a-zA-Z0-9]/;
|
|
578
|
+
/**
|
|
579
|
+
* Minimum fraction of a title's letters/digits the transliteration pipeline
|
|
580
|
+
* must handle for the result to be usable. Below this, too much meaning is
|
|
581
|
+
* lost (e.g. mixed kanji+kana Japanese where the semantic core is kanji, or
|
|
582
|
+
* scripts limax drops entirely) and `slugify` returns "" so callers fall back
|
|
583
|
+
* to their random-ID path instead of shipping a garbled slug.
|
|
584
|
+
*/ var SLUG_COVERAGE_THRESHOLD = .75;
|
|
585
|
+
/** Per-character limax probe cache: does this char transliterate to anything? */ var charCoverageCache = /* @__PURE__ */ new Map();
|
|
586
|
+
function isCharCovered(char) {
|
|
587
|
+
if (ASCII_ALNUM_RE.test(char)) return true;
|
|
588
|
+
let covered = charCoverageCache.get(char);
|
|
589
|
+
if (covered === void 0) {
|
|
590
|
+
covered = limax(char, { tone: false }).length > 0;
|
|
591
|
+
charCoverageCache.set(char, covered);
|
|
592
|
+
}
|
|
593
|
+
return covered;
|
|
594
|
+
}
|
|
595
|
+
function slugCoverage(text, japanese) {
|
|
596
|
+
let content = 0;
|
|
597
|
+
let covered = 0;
|
|
598
|
+
for (const char of text) {
|
|
599
|
+
if (!CONTENT_CHAR_RE.test(char)) continue;
|
|
600
|
+
content++;
|
|
601
|
+
if (KANA_RE.test(char) || HANGUL_RE.test(char)) covered++;
|
|
602
|
+
else if (japanese && HAN_RE.test(char)) {} else if (isCharCovered(char)) covered++;
|
|
603
|
+
}
|
|
604
|
+
return content === 0 ? 0 : covered / content;
|
|
605
|
+
}
|
|
151
606
|
/**
|
|
152
607
|
* Converts text to a URL-friendly slug.
|
|
153
608
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
609
|
+
* Transliterates i18n text to Latin before slugifying: Korean hangul via
|
|
610
|
+
* Revised Romanization, Japanese kana via Hepburn romaji (both in-house, see
|
|
611
|
+
* `translit.ts`), everything else (Han → pinyin, Cyrillic, accents, …) via
|
|
612
|
+
* limax. When the title contains kana it is treated as Japanese and Han
|
|
613
|
+
* characters are dropped rather than misread as Chinese pinyin; Han without
|
|
614
|
+
* kana is treated as Chinese.
|
|
615
|
+
*
|
|
616
|
+
* Returns `""` when transliteration would lose too much of the title (see
|
|
617
|
+
* `SLUG_COVERAGE_THRESHOLD`) — every caller treats an empty base as "no
|
|
618
|
+
* usable title" and falls back to a random ID, which beats publishing a
|
|
619
|
+
* garbled slug.
|
|
156
620
|
*
|
|
157
621
|
* @param text - The text to convert to a slug
|
|
158
|
-
* @returns The slugified string
|
|
622
|
+
* @returns The slugified string, or `""` when no usable slug can be derived
|
|
159
623
|
*
|
|
160
624
|
* @example
|
|
161
625
|
* ```ts
|
|
@@ -164,9 +628,24 @@ function normalizeSitePathname(pathname) {
|
|
|
164
628
|
*
|
|
165
629
|
* slugify("书评");
|
|
166
630
|
* // Returns: "shu-ping"
|
|
631
|
+
*
|
|
632
|
+
* slugify("안녕하세요 세계");
|
|
633
|
+
* // Returns: "annyeonghaseyo-segye"
|
|
634
|
+
*
|
|
635
|
+
* slugify("ハンバーガー");
|
|
636
|
+
* // Returns: "hanbaga"
|
|
637
|
+
*
|
|
638
|
+
* slugify("日本語のタイトルです");
|
|
639
|
+
* // Returns: "" (kanji carries the meaning and can't be transliterated)
|
|
167
640
|
* ```
|
|
168
641
|
*/ function slugify(text) {
|
|
169
|
-
|
|
642
|
+
const japanese = KANA_RE.test(text);
|
|
643
|
+
let pre = text;
|
|
644
|
+
if (japanese) pre = kanaToRomaji(pre.replace(HAN_GLOBAL_RE, " "));
|
|
645
|
+
if (HANGUL_RE.test(pre)) pre = hangulToLatin(pre);
|
|
646
|
+
const base = limax(pre, { tone: false }).replace(/_/g, "-");
|
|
647
|
+
if (!base) return "";
|
|
648
|
+
return slugCoverage(text, japanese) >= SLUG_COVERAGE_THRESHOLD ? base : "";
|
|
170
649
|
}
|
|
171
650
|
/**
|
|
172
651
|
* Extracts a human-friendly domain name from a URL for display purposes.
|
|
@@ -198,6 +677,20 @@ var SAFE_URL_PROTOCOLS = new Set([
|
|
|
198
677
|
"https:",
|
|
199
678
|
"mailto:"
|
|
200
679
|
]);
|
|
680
|
+
var SAFE_RICH_TEXT_HREF_PROTOCOLS = new Set([
|
|
681
|
+
...SAFE_URL_PROTOCOLS,
|
|
682
|
+
"tel:",
|
|
683
|
+
"sms:"
|
|
684
|
+
]);
|
|
685
|
+
function sanitizeUrlWithProtocols(url, protocols) {
|
|
686
|
+
try {
|
|
687
|
+
const parsed = new URL(url, "https://placeholder.invalid");
|
|
688
|
+
if (protocols.has(parsed.protocol)) return url;
|
|
689
|
+
return "";
|
|
690
|
+
} catch {
|
|
691
|
+
return "";
|
|
692
|
+
}
|
|
693
|
+
}
|
|
201
694
|
/**
|
|
202
695
|
* Checks whether a string is a safe absolute URL.
|
|
203
696
|
*
|
|
@@ -241,13 +734,25 @@ var SAFE_URL_PROTOCOLS = new Set([
|
|
|
241
734
|
* sanitizeUrl("data:text/html,<h1>Hi</h1>"); // ""
|
|
242
735
|
* ```
|
|
243
736
|
*/ function sanitizeUrl(url) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
737
|
+
return sanitizeUrlWithProtocols(url, SAFE_URL_PROTOCOLS);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Sanitizes a rich-text link destination.
|
|
741
|
+
*
|
|
742
|
+
* In addition to safe web and email URLs, rich-text links may launch the
|
|
743
|
+
* device's phone or messaging app through `tel:` and `sms:`. Relative links
|
|
744
|
+
* remain valid, while executable and resource-only protocols are rejected.
|
|
745
|
+
*
|
|
746
|
+
* @param url - The link destination to sanitize
|
|
747
|
+
* @returns The original URL if safe for an anchor, or an empty string otherwise
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```ts
|
|
751
|
+
* sanitizeRichTextHref("sms:+15551234567"); // "sms:+15551234567"
|
|
752
|
+
* sanitizeRichTextHref("javascript:alert(1)"); // ""
|
|
753
|
+
* ```
|
|
754
|
+
*/ function sanitizeRichTextHref(url) {
|
|
755
|
+
return sanitizeUrlWithProtocols(url, SAFE_RICH_TEXT_HREF_PROTOCOLS);
|
|
251
756
|
}
|
|
252
757
|
/**
|
|
253
758
|
* Normalize a public site URL by stripping any trailing slash from the path.
|
|
@@ -392,4 +897,4 @@ var SAFE_URL_PROTOCOLS = new Set([
|
|
|
392
897
|
return toAbsoluteSiteUrl(url, siteUrl, sitePathPrefix);
|
|
393
898
|
}
|
|
394
899
|
//#endregion
|
|
395
|
-
export {
|
|
900
|
+
export { toPublicHref as _, getSitePathPrefix as a, url_exports as b, normalizePath as c, sanitizeRichTextHref as d, sanitizeUrl as f, toAbsoluteSiteUrl as g, toAbsoluteAssetUrl as h, getSiteOrigin as i, normalizeSitePathPrefix as l, stripSitePathPrefix as m, extractDisplayDomain as n, isFullUrl as o, slugify as p, extractDomain as r, isSafeInternalRedirect as s, buildSiteUrl as t, normalizeSiteUrl as u, toPublicPath as v, __exportAll as x, toSameSitePath as y };
|
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { parse } from "smol-toml";
|
|
5
|
+
|
|
6
|
+
interface MiseConfig {
|
|
7
|
+
tasks?: Record<
|
|
8
|
+
string,
|
|
9
|
+
{
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("mise tasks", () => {
|
|
16
|
+
it("enables environment proxy support before the Node dev runtime starts", () => {
|
|
17
|
+
const configPath = resolve(import.meta.dirname, "../../../../mise.toml");
|
|
18
|
+
const config = parse(readFileSync(configPath, "utf8")) as MiseConfig;
|
|
19
|
+
|
|
20
|
+
expect(config.tasks?.["dev-node"]?.env?.NODE_USE_ENV_PROXY).toBe("1");
|
|
21
|
+
});
|
|
22
|
+
});
|