dsh-visualkit 0.0.0-stage → 0.0.1
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/CHANGELOG.md +60 -0
- package/LICENSE +26 -0
- package/README.md +427 -2
- package/THIRD_PARTY.md +55 -0
- package/cordis.patch.yml +29 -0
- package/data/assets/README.md +38 -0
- package/data/assets/icons/brands/bilibili.svg +1 -0
- package/data/assets/icons/brands/discord.svg +1 -0
- package/data/assets/icons/brands/neteasecloudmusic.svg +1 -0
- package/data/assets/icons/brands/qq.svg +1 -0
- package/data/assets/icons/brands/sinaweibo.svg +1 -0
- package/data/assets/icons/brands/steam.svg +1 -0
- package/data/assets/icons/brands/telegram.svg +1 -0
- package/data/assets/icons/brands/tiktok.svg +1 -0
- package/data/assets/icons/brands/twitch.svg +1 -0
- package/data/assets/icons/brands/wechat.svg +1 -0
- package/data/assets/icons/brands/x.svg +1 -0
- package/data/assets/icons/brands/youtube.svg +1 -0
- package/data/assets/icons/lucide/bell.svg +1 -0
- package/data/assets/icons/lucide/calendar.svg +1 -0
- package/data/assets/icons/lucide/clock.svg +1 -0
- package/data/assets/icons/lucide/gamepad-2.svg +1 -0
- package/data/assets/icons/lucide/gift.svg +1 -0
- package/data/assets/icons/lucide/heart.svg +1 -0
- package/data/assets/icons/lucide/mic.svg +1 -0
- package/data/assets/icons/lucide/play.svg +1 -0
- package/data/assets/icons/lucide/star.svg +1 -0
- package/data/assets/icons/lucide/video.svg +1 -0
- package/data/assets/icons/phosphor/LICENSE +21 -0
- package/data/assets/icons/phosphor/atom-duotone.svg +1 -0
- package/data/assets/icons/phosphor/binary-duotone.svg +1 -0
- package/data/assets/icons/phosphor/blueprint-duotone.svg +1 -0
- package/data/assets/icons/phosphor/circuitry-duotone.svg +1 -0
- package/data/assets/icons/phosphor/cloud-arrow-up-duotone.svg +1 -0
- package/data/assets/icons/phosphor/code-duotone.svg +1 -0
- package/data/assets/icons/phosphor/compass-duotone.svg +1 -0
- package/data/assets/icons/phosphor/cpu-duotone.svg +1 -0
- package/data/assets/icons/phosphor/cube-duotone.svg +1 -0
- package/data/assets/icons/phosphor/database-duotone.svg +1 -0
- package/data/assets/icons/phosphor/flask-duotone.svg +1 -0
- package/data/assets/icons/phosphor/frame-corners-duotone.svg +1 -0
- package/data/assets/icons/phosphor/function-duotone.svg +1 -0
- package/data/assets/icons/phosphor/hard-drives-duotone.svg +1 -0
- package/data/assets/icons/phosphor/hexagon-duotone.svg +1 -0
- package/data/assets/icons/phosphor/infinity-duotone.svg +1 -0
- package/data/assets/icons/phosphor/lightning-duotone.svg +1 -0
- package/data/assets/icons/phosphor/magic-wand-duotone.svg +1 -0
- package/data/assets/icons/phosphor/moon-stars-duotone.svg +1 -0
- package/data/assets/icons/phosphor/paint-brush-duotone.svg +1 -0
- package/data/assets/icons/phosphor/pen-nib-duotone.svg +1 -0
- package/data/assets/icons/phosphor/planet-duotone.svg +1 -0
- package/data/assets/icons/phosphor/rocket-launch-duotone.svg +1 -0
- package/data/assets/icons/phosphor/ruler-duotone.svg +1 -0
- package/data/assets/icons/phosphor/selection-plus-duotone.svg +1 -0
- package/data/assets/icons/phosphor/shapes-duotone.svg +1 -0
- package/data/assets/icons/phosphor/sparkle-duotone.svg +1 -0
- package/data/assets/icons/phosphor/sun-horizon-duotone.svg +1 -0
- package/data/assets/icons/phosphor/terminal-window-duotone.svg +1 -0
- package/data/assets/icons/phosphor/triangle-duotone.svg +1 -0
- package/data/assets/icons/phosphor/waveform-duotone.svg +1 -0
- package/data/assets/icons/tabler/alarm.svg +1 -0
- package/data/assets/icons/tabler/badge-ad.svg +1 -0
- package/data/assets/icons/tabler/bell.svg +1 -0
- package/data/assets/icons/tabler/bolt.svg +1 -0
- package/data/assets/icons/tabler/brand-bilibili.svg +1 -0
- package/data/assets/icons/tabler/brand-discord.svg +1 -0
- package/data/assets/icons/tabler/brand-qq.svg +1 -0
- package/data/assets/icons/tabler/brand-twitch.svg +1 -0
- package/data/assets/icons/tabler/brand-wechat.svg +1 -0
- package/data/assets/icons/tabler/brand-weibo.svg +1 -0
- package/data/assets/icons/tabler/brand-youtube.svg +1 -0
- package/data/assets/icons/tabler/broadcast.svg +1 -0
- package/data/assets/icons/tabler/calendar-event.svg +1 -0
- package/data/assets/icons/tabler/calendar-week.svg +1 -0
- package/data/assets/icons/tabler/clock-hour-3.svg +1 -0
- package/data/assets/icons/tabler/coin.svg +1 -0
- package/data/assets/icons/tabler/crown.svg +1 -0
- package/data/assets/icons/tabler/currency-yen.svg +1 -0
- package/data/assets/icons/tabler/device-gamepad-2.svg +1 -0
- package/data/assets/icons/tabler/diamond.svg +1 -0
- package/data/assets/icons/tabler/flame.svg +1 -0
- package/data/assets/icons/tabler/gift.svg +1 -0
- package/data/assets/icons/tabler/headphones.svg +1 -0
- package/data/assets/icons/tabler/heart-filled.svg +1 -0
- package/data/assets/icons/tabler/heart.svg +1 -0
- package/data/assets/icons/tabler/live-view.svg +1 -0
- package/data/assets/icons/tabler/microphone.svg +1 -0
- package/data/assets/icons/tabler/mood-smile.svg +1 -0
- package/data/assets/icons/tabler/mood-tongue.svg +1 -0
- package/data/assets/icons/tabler/movie.svg +1 -0
- package/data/assets/icons/tabler/player-play.svg +1 -0
- package/data/assets/icons/tabler/playlist.svg +1 -0
- package/data/assets/icons/tabler/rocket.svg +1 -0
- package/data/assets/icons/tabler/sparkles.svg +1 -0
- package/data/assets/icons/tabler/star-filled.svg +1 -0
- package/data/assets/icons/tabler/star.svg +1 -0
- package/data/assets/icons/tabler/tag.svg +1 -0
- package/data/assets/icons/tabler/thumb-up.svg +1 -0
- package/data/assets/icons/tabler/ticket.svg +1 -0
- package/data/assets/icons/tabler/video.svg +1 -0
- package/data/assets/illustrations/openpeeps/LICENSE +8 -0
- package/data/assets/illustrations/openpeeps/peep-10.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-100.svg +32 -0
- package/data/assets/illustrations/openpeeps/peep-101.svg +25 -0
- package/data/assets/illustrations/openpeeps/peep-102.svg +28 -0
- package/data/assets/illustrations/openpeeps/peep-103.svg +32 -0
- package/data/assets/illustrations/openpeeps/peep-105.svg +25 -0
- package/data/assets/illustrations/openpeeps/peep-12.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-13.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-16.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-17.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-22.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-25.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-26.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-27.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-30.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-31.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-34.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-38.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-39.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-4.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-41.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-44.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-47.svg +26 -0
- package/data/assets/illustrations/openpeeps/peep-48.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-50.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-51.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-53.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-54.svg +26 -0
- package/data/assets/illustrations/openpeeps/peep-55.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-56.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-58.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-61.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-64.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-68.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-69.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-7.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-72.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-76.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-77.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-80.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-81.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-83.svg +29 -0
- package/data/assets/manifest.json +1 -0
- package/data/assets/parts//342/206/223/346/212/225/345/275/261.png +0 -0
- package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2711.png +0 -0
- package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2712.png +0 -0
- package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2713.png +0 -0
- package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2714.png +0 -0
- package/data/assets/parts//345/217/214/351/251/254/345/260/276.png +0 -0
- package/data/assets/parts//345/217/214/351/251/254/345/260/2761.png +0 -0
- package/data/assets/parts//345/217/2211.png +0 -0
- package/data/assets/parts//345/217/2212.png +0 -0
- package/data/assets/parts//345/217/2213.png +0 -0
- package/data/assets/parts//345/217/221/345/234/210.png +0 -0
- package/data/assets/parts//345/217/221/345/244/2711.png +0 -0
- package/data/assets/parts//345/217/221/345/244/2712.png +0 -0
- package/data/assets/parts//345/217/243/346/260/264/345/267/276/345/211/215.png +0 -0
- package/data/assets/parts//345/217/243/346/260/264/345/267/276/345/220/216.png +0 -0
- package/data/assets/parts//345/217/263/344/270/212/347/234/274/347/235/221.png +0 -0
- package/data/assets/parts//345/217/263/346/211/213.png +0 -0
- package/data/assets/parts//345/217/263/346/211/2131.png +0 -0
- package/data/assets/parts//345/217/263/347/234/274/347/220/203.png +0 -0
- package/data/assets/parts//345/217/263/347/234/274/347/231/275.png +0 -0
- package/data/assets/parts//345/217/263/350/205/277.png +0 -0
- package/data/assets/parts//345/221/206/346/257/233.png +0 -0
- package/data/assets/parts//345/230/264/345/267/264.png +0 -0
- package/data/assets/parts//345/260/276/345/267/264.png +0 -0
- package/data/assets/parts//345/267/246/344/270/212/347/234/274/347/235/221.png +0 -0
- package/data/assets/parts//345/267/246/345/217/214/347/234/274/347/232/256.png +0 -0
- package/data/assets/parts//345/267/246/346/211/213.png +0 -0
- package/data/assets/parts//345/267/246/346/211/2131.png +0 -0
- package/data/assets/parts//345/267/246/347/234/274/347/220/203.png +0 -0
- package/data/assets/parts//345/267/246/347/234/274/347/231/275.png +0 -0
- package/data/assets/parts//345/267/246/350/205/277.png +0 -0
- package/data/assets/parts//346/212/225/345/275/261.png +0 -0
- package/data/assets/parts//347/234/211/346/257/233.png +0 -0
- package/data/assets/parts//347/234/274/347/275/251.png +0 -0
- package/data/assets/parts//350/200/263/346/234/2651.png +0 -0
- package/data/assets/parts//350/200/263/346/234/2652.png +0 -0
- package/data/assets/parts//350/204/221/345/243/263.png +0 -0
- package/data/assets/parts//350/241/243/346/234/215/345/211/215.png +0 -0
- package/data/assets/parts//350/241/243/346/234/215/345/220/216.png +0 -0
- package/data/assets/parts//350/241/243/346/234/215/351/273/221.png +0 -0
- package/data/assets/parts//350/241/243/346/234/215/351/273/2211.png +0 -0
- package/data/assets/parts//350/272/253/344/275/223.png +0 -0
- package/data/assets/parts//351/223/201/346/235/277.png +0 -0
- package/data/assets/parts//351/273/221/347/234/274/345/234/210.png +0 -0
- package/data/assets/parts//351/273/221/347/234/274/345/234/2102.png +0 -0
- package/data/assets/patterns/checker.svg +1 -0
- package/data/assets/patterns/circles-overlap.svg +1 -0
- package/data/assets/patterns/crosshatch.svg +1 -0
- package/data/assets/patterns/diagonal.svg +1 -0
- package/data/assets/patterns/dots.svg +1 -0
- package/data/assets/patterns/grid.svg +1 -0
- package/data/assets/patterns/halftone.svg +1 -0
- package/data/assets/patterns/plus.svg +1 -0
- package/data/assets/patterns/scales.svg +1 -0
- package/data/assets/patterns/triangles.svg +1 -0
- package/data/assets/patterns/waves.svg +1 -0
- package/data/assets/patterns/zigzag.svg +1 -0
- package/data/assets/stickers/openmoji/1F375.svg +12 -0
- package/data/assets/stickers/openmoji/1F9E8.svg +21 -0
- package/data/assets/stickers/twemoji/1f389.svg +1 -0
- package/data/assets/stickers/twemoji/1f3a4.svg +1 -0
- package/data/assets/stickers/twemoji/1f3a7.svg +1 -0
- package/data/assets/stickers/twemoji/1f3ae.svg +1 -0
- package/data/assets/stickers/twemoji/1f44b.svg +1 -0
- package/data/assets/stickers/twemoji/1f44d.svg +1 -0
- package/data/assets/stickers/twemoji/1f494.svg +1 -0
- package/data/assets/stickers/twemoji/1f4a2.svg +1 -0
- package/data/assets/stickers/twemoji/1f4a8.svg +1 -0
- package/data/assets/stickers/twemoji/1f4b5.svg +1 -0
- package/data/assets/stickers/twemoji/1f4d6.svg +1 -0
- package/data/assets/stickers/twemoji/1f525.svg +1 -0
- package/data/assets/stickers/twemoji/1f600.svg +1 -0
- package/data/assets/stickers/twemoji/1f602.svg +1 -0
- package/data/assets/stickers/twemoji/1f60d.svg +1 -0
- package/data/assets/stickers/twemoji/1f62d.svg +1 -0
- package/data/assets/stickers/twemoji/1f680.svg +1 -0
- package/data/assets/stickers/twemoji/1f979.svg +1 -0
- package/data/assets/stickers/twemoji/1f97a.svg +1 -0
- package/data/assets/stickers/twemoji/26a1.svg +1 -0
- package/data/assets/stickers/twemoji/2705.svg +1 -0
- package/data/assets/stickers/twemoji/274c.svg +1 -0
- package/data/assets/stickers/twemoji/2764.svg +1 -0
- package/data/assets/stickers/twemoji/2b50.svg +1 -0
- package/data/assets/stickers/twemoji_1f525.svg +1 -0
- package/data/assets/stickers/twemoji_1f600.svg +1 -0
- package/data/assets/stickers/twemoji_1f979.svg +1 -0
- package/data/assets/stickers/twemoji_2764.svg +1 -0
- package/data/assets/textures/grain-dark.png +0 -0
- package/data/assets/textures/grain-light.png +0 -0
- package/data/assets/textures/grain-warm.png +0 -0
- package/data/lucide-icons.json +1 -0
- package/data/packs/tabler.icons.json +1 -0
- package/icon.svg +26 -0
- package/lib/assets.js +228 -0
- package/lib/audit.js +284 -0
- package/lib/contrast-render.js +213 -0
- package/lib/css-rewrite.js +379 -0
- package/lib/detect.js +701 -0
- package/lib/diff.js +153 -0
- package/lib/emit.js +557 -0
- package/lib/html-context.js +315 -0
- package/lib/icon-render.js +107 -0
- package/lib/icondb.js +402 -0
- package/lib/image-normalize.js +140 -0
- package/lib/index.js +1289 -0
- package/lib/layouts/common.js +48 -0
- package/lib/layouts/image.js +1010 -0
- package/lib/layouts/index.js +10 -0
- package/lib/layouts/intent.js +66 -0
- package/lib/layouts/metrics-text.js +91 -0
- package/lib/layouts/png.js +75 -0
- package/lib/layouts/rules-contrast.js +575 -0
- package/lib/layouts/rules-punct.js +486 -0
- package/lib/layouts/rules-structure.js +182 -0
- package/lib/layouts/source.js +1961 -0
- package/lib/layouts/svg.js +1204 -0
- package/lib/morphcore.js +139 -0
- package/lib/preview.js +225 -0
- package/lib/render-html.js +255 -0
- package/lib/render.js +486 -0
- package/lib/rules.js +267 -0
- package/lib/shadow.js +83 -0
- package/lib/skip.js +56 -0
- package/lib/style.js +688 -0
- package/lib/svg.js +729 -0
- package/lib/taste-image.js +323 -0
- package/lib/taste.js +219 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +92 -4
- package/vendor/morphicons-adapters.iife.js +903 -0
- package/vendor/morphicons.iife.js +2441 -0
- package/vendor/resvg/LICENSE +373 -0
- package/vendor/resvg/index.d.ts +98 -0
- package/vendor/resvg/index.js +23 -0
- package/vendor/resvg/js-binding.d.ts +52 -0
- package/vendor/resvg/js-binding.js +1 -0
- package/vendor/resvg/package.json +138 -0
- package/vendor/resvg/resvgjs.win32-x64-msvc.node +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* html-context.js — 判断「某个位置在 HTML 里是什么」。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:机械替换(emoji→SVG、直引号→弯引号、术语替换)最容易翻车的地方
|
|
5
|
+
* 不是替换规则,而是**替换发生在哪**:
|
|
6
|
+
* · 文本节点里可以放 `<svg>`,浏览器会渲染它;
|
|
7
|
+
* · 属性值里放 `<svg>`,浏览器会把它当**文本**显示出来(实测:搜索框右侧溢出
|
|
8
|
+
* 一段 `<svg class="ico" …>` 原始标记);
|
|
9
|
+
* · 把属性值的**结束引号**换掉(`placeholder="…"` → `placeholder="…“`),
|
|
10
|
+
* 属性会被撑破,后面半页内容全部错位;
|
|
11
|
+
* · `<script>` / `<style>` / `<!-- -->` 里根本不该动。
|
|
12
|
+
* 所以先把位置分类,再决定怎么改。
|
|
13
|
+
*
|
|
14
|
+
* 只做一件事:扫描成「带 kind 的区段」,不解释标签语义、不做 DOM 树。
|
|
15
|
+
* 无依赖、可在任何 HTML 片段上跑(含 Vue/Svelte 模板;`{{ }}` 归到文本节点,
|
|
16
|
+
* 由调用方决定要不要进一步排除)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 哨兵:pair 命中但映射为空(区别于“显式删除”的 null)。 */
|
|
20
|
+
const UNMAPPED = Symbol("unmapped");
|
|
21
|
+
|
|
22
|
+
/** 区段种类。 */
|
|
23
|
+
export const KIND = {
|
|
24
|
+
TEXT: "text",
|
|
25
|
+
TAG: "tag",
|
|
26
|
+
ATTR_NAME: "attrName",
|
|
27
|
+
ATTR_VALUE: "attrValue",
|
|
28
|
+
SCRIPT: "script",
|
|
29
|
+
STYLE: "style",
|
|
30
|
+
COMMENT: "comment",
|
|
31
|
+
DOCTYPE: "doctype",
|
|
32
|
+
RAW: "raw"
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 把源码切成区段。
|
|
37
|
+
* @param {string} source
|
|
38
|
+
* @returns {{kind:string, start:number, end:number, attr?:string, quote?:string}[]}
|
|
39
|
+
*/
|
|
40
|
+
export function scanHtml(source) {
|
|
41
|
+
const s = String(source);
|
|
42
|
+
const out = [];
|
|
43
|
+
let i = 0;
|
|
44
|
+
const push = (kind, start, end, extra) => {
|
|
45
|
+
if (end > start) out.push({ kind, start, end, ...(extra ?? {}) });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
while (i < s.length) {
|
|
49
|
+
const lt = s.indexOf("<", i);
|
|
50
|
+
if (lt === -1) {
|
|
51
|
+
push(KIND.TEXT, i, s.length);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (lt > i) push(KIND.TEXT, i, lt);
|
|
55
|
+
|
|
56
|
+
// 注释 / doctype / CDATA
|
|
57
|
+
if (s.startsWith("<!--", lt)) {
|
|
58
|
+
const close = s.indexOf("-->", lt + 4);
|
|
59
|
+
const end = close === -1 ? s.length : close + 3;
|
|
60
|
+
push(KIND.COMMENT, lt, end);
|
|
61
|
+
i = end;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (s.startsWith("<!", lt) || s.startsWith("<?", lt)) {
|
|
65
|
+
const close = s.indexOf(">", lt);
|
|
66
|
+
const end = close === -1 ? s.length : close + 1;
|
|
67
|
+
push(KIND.DOCTYPE, lt, end);
|
|
68
|
+
i = end;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// script / style:整段吃掉(里面的 < > 不当标签)
|
|
73
|
+
const rawTag = /^<(script|style|textarea|pre)\b/i.exec(s.slice(lt, lt + 12));
|
|
74
|
+
if (rawTag !== null) {
|
|
75
|
+
const tagName = rawTag[1].toLowerCase();
|
|
76
|
+
const openEnd = s.indexOf(">", lt);
|
|
77
|
+
if (openEnd === -1) {
|
|
78
|
+
push(KIND.RAW, lt, s.length);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const closeRe = new RegExp(`</${tagName}\\s*>`, "i");
|
|
82
|
+
const rest = s.slice(openEnd + 1);
|
|
83
|
+
const m = closeRe.exec(rest);
|
|
84
|
+
const end = m === null ? s.length : openEnd + 1 + m.index + m[0].length;
|
|
85
|
+
// 开标签本身仍然要解析属性(`<script src="…">`)
|
|
86
|
+
parseTag(s, lt, openEnd + 1, out, push);
|
|
87
|
+
push(tagName === "script" ? KIND.SCRIPT : KIND.STYLE, openEnd + 1, end - (m === null ? 0 : m[0].length));
|
|
88
|
+
if (m !== null) parseTag(s, end - m[0].length, end, out, push);
|
|
89
|
+
i = end;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 普通标签(含自闭合):属性值里的 `>` 不能截断,必须尊重引号
|
|
94
|
+
const tagEnd = findTagEnd(s, lt);
|
|
95
|
+
parseTag(s, lt, tagEnd, out, push);
|
|
96
|
+
i = tagEnd;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 找标签的结束位置:跳过引号内的 `>`。 */
|
|
102
|
+
function findTagEnd(s, from) {
|
|
103
|
+
let quote = null;
|
|
104
|
+
for (let i = from + 1; i < s.length; i += 1) {
|
|
105
|
+
const ch = s[i];
|
|
106
|
+
if (quote !== null) {
|
|
107
|
+
if (ch === quote) quote = null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (ch === '"' || ch === "'") {
|
|
111
|
+
quote = ch;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (ch === ">") return i + 1;
|
|
115
|
+
}
|
|
116
|
+
return s.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 解析一个标签区间,产出 TAG / ATTR_NAME / ATTR_VALUE 区段。 */
|
|
120
|
+
function parseTag(s, start, end, out, push) {
|
|
121
|
+
push(KIND.TAG, start, end);
|
|
122
|
+
const inner = s.slice(start, end);
|
|
123
|
+
// 属性:name、name=value、name="value"、name='value'(引号内的内容单独成段)
|
|
124
|
+
const re = /([:@\w-]+)(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))?/g;
|
|
125
|
+
// 跳过标签名本身
|
|
126
|
+
const nameEnd = /^<\/?[a-zA-Z][\w:-]*/.exec(inner);
|
|
127
|
+
const from = nameEnd === null ? 0 : nameEnd[0].length;
|
|
128
|
+
re.lastIndex = from;
|
|
129
|
+
let m;
|
|
130
|
+
while ((m = re.exec(inner)) !== null) {
|
|
131
|
+
const valuePart = m[3] ?? m[4] ?? m[5];
|
|
132
|
+
if (valuePart === undefined) continue;
|
|
133
|
+
const valueOffset = m.index + m[1].length + m[2].length + (m[3] !== undefined || m[4] !== undefined ? 1 : 0);
|
|
134
|
+
push(KIND.ATTR_VALUE, start + valueOffset, start + valueOffset + valuePart.length, { attr: m[1] });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 查某个偏移属于哪个区段(线性足够:区段数在千级、单次改写也只查命中的几处)。
|
|
140
|
+
* @param {{kind:string,start:number,end:number,attr?:string}[]} segments
|
|
141
|
+
* @param {number} index
|
|
142
|
+
*/
|
|
143
|
+
export function kindAt(segments, index) {
|
|
144
|
+
// 要取**最具体**的那一段:标签区间(TAG)包住了属性值区间(ATTR_VALUE),
|
|
145
|
+
// 先碰到 TAG 就返回会把 `title="搜索 🔍 内容"` 里的 emoji 误判成“标签结构”,
|
|
146
|
+
// 于是本该“改不得”的属性值被当成整体拒绝,或被错误地归到 text。
|
|
147
|
+
let best = null;
|
|
148
|
+
for (const seg of segments) {
|
|
149
|
+
if (index < seg.start || index >= seg.end) continue;
|
|
150
|
+
if (best === null || seg.end - seg.start < best.end - best.start) best = seg;
|
|
151
|
+
}
|
|
152
|
+
return best ?? { kind: KIND.TEXT, start: index, end: index + 1 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 按上下文约束做字面量替换。
|
|
157
|
+
*
|
|
158
|
+
* @param {object} opts
|
|
159
|
+
* @param {string} opts.source 原文
|
|
160
|
+
* @param {string|RegExp} opts.find 要替换的内容(字符串按字面量处理)
|
|
161
|
+
* @param {string|((m:string)=>string)} opts.replace 替换内容
|
|
162
|
+
* @param {"text"|"text+attr"|"all"} [opts.scope] 允许改动的上下文,默认 text
|
|
163
|
+
* @param {boolean} [opts.allowMarkupInAttr] 允许把含 `<` 的内容写进属性(默认拒绝)
|
|
164
|
+
* @returns {{output:string, changes:{kind:string,attr?:string,index:number,from:string,to:string}[], refused:{kind:string,attr?:string,index:number,reason:string}[]}}
|
|
165
|
+
*/
|
|
166
|
+
export function rewriteByContext(opts) {
|
|
167
|
+
const source = String(opts.source ?? "");
|
|
168
|
+
const scope = opts.scope ?? "text";
|
|
169
|
+
// 两种用法:单条 find/replace,或批量 pairs[{find,replace}]。
|
|
170
|
+
// 批量**一次性匹配**(合成一个正则),不是逐条跑——逐条跑时前一条插入的内容
|
|
171
|
+
// 会被后一条再匹配到,也更容易把偏移搞乱。
|
|
172
|
+
const pairs = Array.isArray(opts.pairs) && opts.pairs.length > 0 ? opts.pairs : null;
|
|
173
|
+
if (pairs === null && (opts.find === undefined || opts.find === "")) throw new Error("rewriteByContext 需要 find 或 pairs");
|
|
174
|
+
// strict(默认开):**映射为空**不等于“删掉”。
|
|
175
|
+
// 实测教训(2026-11-08,现代化改造 v1):映射表里漏了几个 emoji,脚本把“查不到图标”
|
|
176
|
+
// 当成空串传进来,结果那几行图标被默默删掉,渲染出来只剩文字——不报错、不提示,最难查。
|
|
177
|
+
// 所以空映射默认**拒绝**;确实要删就显式写 `replace: null`(工具层写作 `原文=>(del)`)。
|
|
178
|
+
const strict = opts.strict !== false;
|
|
179
|
+
const segments = scanHtml(source);
|
|
180
|
+
const changes = [];
|
|
181
|
+
const refused = [];
|
|
182
|
+
const unmapped = [];
|
|
183
|
+
|
|
184
|
+
const allowed = (kind) => {
|
|
185
|
+
if (scope === "all") return kind !== KIND.COMMENT;
|
|
186
|
+
if (kind === KIND.TEXT) return true;
|
|
187
|
+
if (kind === KIND.ATTR_VALUE) return scope === "text+attr";
|
|
188
|
+
return false; // script / style / comment / tag / doctype
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
let re;
|
|
192
|
+
let lookup = null;
|
|
193
|
+
if (pairs === null) {
|
|
194
|
+
const find = opts.find;
|
|
195
|
+
re = find instanceof RegExp ? new RegExp(find.source, find.flags.includes("g") ? find.flags : `${find.flags}g`) : new RegExp(escapeRegExp(find), "g");
|
|
196
|
+
} else {
|
|
197
|
+
// 长字符串优先,避免 "ab" 抢在 "abc" 前面
|
|
198
|
+
const sorted = [...pairs].filter((p) => typeof p.find === "string" && p.find !== "").sort((a, b) => b.find.length - a.find.length);
|
|
199
|
+
lookup = new Map();
|
|
200
|
+
for (const pair of sorted) {
|
|
201
|
+
const empty = pair.replace === "" || pair.replace === undefined;
|
|
202
|
+
if (empty && strict) {
|
|
203
|
+
// 不放进 lookup:命中时会被当成“没配映射”拒绝,而不是静默删除
|
|
204
|
+
unmapped.push(pair.find);
|
|
205
|
+
lookup.set(pair.find, UNMAPPED);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
lookup.set(pair.find, pair.replace === undefined ? null : pair.replace);
|
|
209
|
+
}
|
|
210
|
+
re = new RegExp(sorted.map((p) => escapeRegExp(p.find)).join("|"), "g");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let output = source;
|
|
214
|
+
// 从后往前替换,避免偏移漂移
|
|
215
|
+
const hits = [];
|
|
216
|
+
for (const m of source.matchAll(re)) hits.push({ index: m.index ?? 0, text: m[0] });
|
|
217
|
+
for (let k = hits.length - 1; k >= 0; k -= 1) {
|
|
218
|
+
const hit = hits[k];
|
|
219
|
+
const seg = kindAt(segments, hit.index);
|
|
220
|
+
const raw = lookup === null ? opts.replace : lookup.get(hit.text);
|
|
221
|
+
if (raw === UNMAPPED) {
|
|
222
|
+
refused.push({ kind: seg.kind, attr: seg.attr, index: hit.index, reason: `「${hit.text}」映射为空——这是有意删除就写 \`${hit.text}=>(del)\`,否则补上要换成的图标/文本`, from: hit.text });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const next = typeof raw === "function" ? raw(hit.text) : String(raw ?? "");
|
|
226
|
+
if (!allowed(seg.kind)) {
|
|
227
|
+
refused.push({ kind: seg.kind, attr: seg.attr, index: hit.index, reason: reasonOf(seg.kind, scope) });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// 属性值里注入标记 = 一定会被当文本显示出来
|
|
231
|
+
if (seg.kind === KIND.ATTR_VALUE && /[<>]/.test(next) && opts.allowMarkupInAttr !== true) {
|
|
232
|
+
refused.push({ kind: seg.kind, attr: seg.attr, index: hit.index, reason: "替换内容含 < 或 >,写进属性会被当成文本显示" });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
output = output.slice(0, hit.index) + next + output.slice(hit.index + hit.text.length);
|
|
236
|
+
changes.push({ kind: seg.kind, attr: seg.attr, index: hit.index, from: hit.text, to: next });
|
|
237
|
+
}
|
|
238
|
+
changes.reverse();
|
|
239
|
+
refused.reverse();
|
|
240
|
+
return { output, changes, refused, unmapped };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function reasonOf(kind, scope) {
|
|
244
|
+
if (kind === KIND.COMMENT) return "注释里不渲染,不需要改";
|
|
245
|
+
if (kind === KIND.SCRIPT || kind === KIND.STYLE) return "脚本/样式里是代码,不是文案";
|
|
246
|
+
if (kind === KIND.ATTR_VALUE) return `scope=${scope} 不含属性值`;
|
|
247
|
+
if (kind === KIND.TAG) return "标签结构,不按文案替换";
|
|
248
|
+
return `scope=${scope} 不含 ${kind}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function escapeRegExp(text) {
|
|
252
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 字符类:“这个文件里有哪些 emoji / 直引号 / 全角标点”这类问题,
|
|
257
|
+
* 不该靠人一个个试。`probe` 只能查已知字符串,`class` 先扫描分类。
|
|
258
|
+
*/
|
|
259
|
+
export const CLASSES = {
|
|
260
|
+
// emoji:与 taste_check 同口径(主平面 emoji + 符号块,⚠⭐ 也算)
|
|
261
|
+
emoji: /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B50}]/gu,
|
|
262
|
+
// 直引号:中文行文里该用弯引号的那种
|
|
263
|
+
"straight-quote": /["']/g,
|
|
264
|
+
// 全角(中文)标点:检查中英混排该用半角/该不该加空格时用
|
|
265
|
+
"cjk-punct": /[,。、;:!?()【】「」『』《》〈〉…—]/g
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 按字符类扫描,并按「字符 × 上下文」分组。
|
|
270
|
+
*
|
|
271
|
+
* 为什么要分组:同一个 emoji 在文本节点里可以换成 `<svg>`;在属性/脚本里换成标记
|
|
272
|
+
* 就会被当文本显示——所以“有多少个”不如“哪些能改”有用。
|
|
273
|
+
*/
|
|
274
|
+
export function locateClass(source, className) {
|
|
275
|
+
const re = CLASSES[className];
|
|
276
|
+
if (re === undefined) throw new Error(`未知的 class "${className}"(可用:${Object.keys(CLASSES).join(", ")})`);
|
|
277
|
+
const segments = scanHtml(source);
|
|
278
|
+
const byChar = new Map();
|
|
279
|
+
for (const m of source.matchAll(new RegExp(re.source, re.flags))) {
|
|
280
|
+
const ch = m[0];
|
|
281
|
+
const seg = kindAt(segments, m.index ?? 0);
|
|
282
|
+
const entry = byChar.get(ch) ?? { char: ch, total: 0, counts: {} };
|
|
283
|
+
const key = seg.attr === undefined ? seg.kind : `${seg.kind}:${seg.attr}`;
|
|
284
|
+
entry.total += 1;
|
|
285
|
+
entry.counts[key] = (entry.counts[key] ?? 0) + 1;
|
|
286
|
+
byChar.set(ch, entry);
|
|
287
|
+
}
|
|
288
|
+
const chars = [...byChar.values()].sort((a, b) => b.total - a.total);
|
|
289
|
+
const total = chars.reduce((sum, c) => sum + c.total, 0);
|
|
290
|
+
// 只出现在属性/脚本/注释里的字符:换成标记没用,只能拿掉或保留
|
|
291
|
+
const unconvertible = chars.filter((c) => c.counts.text === undefined);
|
|
292
|
+
return { className, total, chars, unconvertible };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 汇总:按上下文统计出现次数(scan 用)。 */
|
|
296
|
+
export function locate(source, probe) {
|
|
297
|
+
const segments = scanHtml(source);
|
|
298
|
+
const found = [];
|
|
299
|
+
if (typeof probe === "string" && probe !== "") {
|
|
300
|
+
let from = 0;
|
|
301
|
+
for (;;) {
|
|
302
|
+
const index = source.indexOf(probe, from);
|
|
303
|
+
if (index === -1) break;
|
|
304
|
+
const seg = kindAt(segments, index);
|
|
305
|
+
found.push({ index, kind: seg.kind, attr: seg.attr, line: source.slice(0, index).split("\n").length });
|
|
306
|
+
from = index + probe.length;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const counts = {};
|
|
310
|
+
for (const item of found) {
|
|
311
|
+
const key = item.attr === undefined ? item.kind : `${item.kind}:${item.attr}`;
|
|
312
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
313
|
+
}
|
|
314
|
+
return { found, counts, total: found.length };
|
|
315
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* icon-render.js — 图标视觉语言的**正交维度合成器**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不靠 style 枚举(旧架构的局限):
|
|
5
|
+
* 9 个 style 是 9 条写死的配方,彼此互斥(badge 就不能同时是 gradient)。
|
|
6
|
+
* 那是模板,不是工具——用户想要「圆角方容器 + 细描边 + 双色渐变字形 + 平头线端」时,
|
|
7
|
+
* 没有任何一个 style 能表达。
|
|
8
|
+
*
|
|
9
|
+
* 现在把视觉语言拆成 6 个正交维,任意组合:
|
|
10
|
+
*
|
|
11
|
+
* ① paint stroke | fill | both 绘法(填充+描边可共存)
|
|
12
|
+
* ② weight 0–64 描边粗细
|
|
13
|
+
* ③ caps round | butt | square 线端(自动配对 join)
|
|
14
|
+
* ④ container none | rounded | circle | squircle
|
|
15
|
+
* + radius 0–12 容器形状与圆角(连续可调)
|
|
16
|
+
* + inset 0–0.45 字形在容器里的留白
|
|
17
|
+
* ⑤ gradient none | linear | radial
|
|
18
|
+
* + gradientAngle 0–360 渐变类型与方向
|
|
19
|
+
* ⑥ duo none | 0–1 双色底层不透明度(需要填充孪生体)
|
|
20
|
+
*
|
|
21
|
+
* style 仍然保留,但退化为「一组维度值的快捷命名」(预设)。
|
|
22
|
+
* 显式传入的维度参数会逐个覆盖预设——所以总是能调到底。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** 9 个预设 → 维度值。仅作为默认起点,任何一项都能被显式参数覆盖。 */
|
|
26
|
+
export const PRESETS = {
|
|
27
|
+
outline: { paint: "stroke", weight: 2 },
|
|
28
|
+
hairline: { paint: "stroke", weight: 1 },
|
|
29
|
+
bold: { paint: "stroke", weight: 2.8 },
|
|
30
|
+
sharp: { paint: "stroke", weight: 2, caps: "butt" },
|
|
31
|
+
solid: { paint: "fill", needsTwin: true },
|
|
32
|
+
duotone: { paint: "stroke", weight: 2, duo: 0.2, needsTwin: true },
|
|
33
|
+
badge: { paint: "stroke", weight: 1.8, container: "rounded", radius: 7.2, inset: 0.24 },
|
|
34
|
+
knockout: { paint: "stroke", weight: 1.8, container: "circle", inset: 0.24 },
|
|
35
|
+
gradient: { paint: "stroke", weight: 2.2, gradient: "linear", gradientAngle: 45 }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** caps → [linecap, linejoin]。平头线端配斜接转角才不会在转折处出小缺口。 */
|
|
39
|
+
export const CAP_TO_JOIN = {
|
|
40
|
+
round: ["round", "round"],
|
|
41
|
+
butt: ["butt", "miter"],
|
|
42
|
+
square: ["square", "miter"]
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** 容器型:把字形装进底色里。none 返回 null。 */
|
|
46
|
+
export const CONTAINERS = ["none", "rounded", "circle", "squircle"];
|
|
47
|
+
|
|
48
|
+
export const DIMENSION_LIMITS = {
|
|
49
|
+
weight: [0, 64],
|
|
50
|
+
radius: [0, 12],
|
|
51
|
+
inset: [0, 0.45],
|
|
52
|
+
gradientAngle: [0, 360],
|
|
53
|
+
duo: [0, 1]
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 解析渐变停靠点:`color` 或 `color@offset`(offset 为 0–1,对齐 SVG 的 offset)。
|
|
58
|
+
*
|
|
59
|
+
* 为什么必须是**可扩展的数组而不是内置几种渐变**:
|
|
60
|
+
* 彩虹、多段金属、双色环、条纹…全都只是「停下来描多个颜色」;
|
|
61
|
+
* 内置 `rainbow`/`metal` 这种名字就把用户锁死了。
|
|
62
|
+
*
|
|
63
|
+
* 未写 offset 的停靠点均匀分布(首尾固定 0 与 1)。
|
|
64
|
+
* 颜色只做**注入安全校验**(不允许引号/尖括号),不强制 hex——
|
|
65
|
+
* hex 只在「不传 stops、由 color2 自动推末色」时才必需。
|
|
66
|
+
*/
|
|
67
|
+
const COLOR_SAFE_RE = /^[#a-zA-Z0-9(),.%\s/+-]+$/;
|
|
68
|
+
|
|
69
|
+
export function parseGradientStops(spec, fallback = []) {
|
|
70
|
+
if (!Array.isArray(spec) || spec.length === 0) return null;
|
|
71
|
+
const parsed = [];
|
|
72
|
+
for (const raw of spec) {
|
|
73
|
+
if (typeof raw !== "string") return null;
|
|
74
|
+
const text = raw.trim();
|
|
75
|
+
if (text === "") return null;
|
|
76
|
+
const at = text.lastIndexOf("@");
|
|
77
|
+
let color = text;
|
|
78
|
+
let offset = null;
|
|
79
|
+
if (at !== -1) {
|
|
80
|
+
const num = Number(text.slice(at + 1));
|
|
81
|
+
if (!Number.isFinite(num) || num < 0 || num > 1) return null;
|
|
82
|
+
offset = num;
|
|
83
|
+
color = text.slice(0, at).trim();
|
|
84
|
+
}
|
|
85
|
+
if (color === "" || !COLOR_SAFE_RE.test(color)) return null;
|
|
86
|
+
parsed.push({ color, offset });
|
|
87
|
+
}
|
|
88
|
+
if (parsed.length === 1) parsed.push({ color: parsed[0].color, offset: 1 });
|
|
89
|
+
// 未写的 offset 按位置均匀分布;已写的保留(允许渐变锐变)
|
|
90
|
+
const n = parsed.length;
|
|
91
|
+
return parsed.map((stop, i) => ({
|
|
92
|
+
color: stop.color,
|
|
93
|
+
offset: stop.offset !== null ? stop.offset : i === 0 ? 0 : i === n - 1 ? 1 : Math.round((i / (n - 1)) * 1000) / 1000
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 曾经的 SPREADS(spreadMethod: repeat/reflect)已移除:
|
|
98
|
+
// 实测 resvg 里它**零效果**——我们的渐变向量总是铺满整个 24 网格盒子,
|
|
99
|
+
// 一次循环就覆盖全部,pad/repeat/reflect 的采样值完全相同。
|
|
100
|
+
// 条纹用「多停靠点 + 同偏移锐变」实现更精确(可控条纹数与位置)。
|
|
101
|
+
|
|
102
|
+
/** 把可能越界的数值夹到合法区间;非数字返回 undefined(让调用方回落到预设)。 */
|
|
103
|
+
export function clampDim(name, value) {
|
|
104
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
105
|
+
const [lo, hi] = DIMENSION_LIMITS[name] ?? [-Infinity, Infinity];
|
|
106
|
+
return Math.min(hi, Math.max(lo, value));
|
|
107
|
+
}
|