// 聚珍(Juzhen)公開入口(架構 §3.1)。 // // const jz = createJuzhen(options); // jz.render(root?); // 套用已啟用之 pass // jz.revert(root?); // 還原 // // 全域(IIFE):window.Juzhen.createJuzhen(...);並掛 v1 相容別名 // globalThis.createCjkAutospace(見 build.mjs footer 與 compat/v1.ts)。 import { Finder } from "./core/finder.js"; import { runPasses, revertPasses } from "./core/pass.js"; import { jiyaAdjacencyPass, jiyaPass } from "./typeset/jiya.js"; import { jinzePass } from "./typeset/jinze.js"; import { gapTrimPass, lineEdgePass } from "./typeset/lineedge.js"; import { longWordPass } from "./typeset/longword.js"; import { orphanPass } from "./typeset/orphan.js"; import { spacingPass } from "./typeset/spacing.js"; import { resolveRuleset } from "./core/unicode.js"; import type { BiasTable, JiyaOptions, JuzhenOptions, LongWordOptions, OrphanOptions, Pass, RenderContext, ResolvedOptions } from "./types.js"; export type { JuzhenOptions, ResolvedOptions, Pass }; export type { CharClassOverride, BdSubclass, Ruleset, Bias, BiasTable } from "./types.js"; export { createCjkAutospace } from "./compat/v1.js"; const DEFAULT_STYLE_INLINES = [ "STRONG", "EM", "A", "B", "I", "U", "S", "MARK", "INS", "DEL", "SUB", "SUP", "SMALL", "ABBR", "CITE", "DFN", "Q", "SPAN", "VAR", "SAMP", "TIME", "BDO", "BDI", "RUBY", "RB", "RT", "RP", "WBR", ]; const DEFAULT_BLOCK_TAGS = [ "BODY", "P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6", "LI", "DT", "DD", "BLOCKQUOTE", "FIGURE", "FIGCAPTION", "TD", "TH", "CAPTION", "SUMMARY", "DETAILS", "HEADER", "FOOTER", "SECTION", "ARTICLE", "ASIDE", "NAV", "MAIN", "ADDRESS", "PRE", "UL", "OL", "DL", "TABLE", "TR", "THEAD", "TBODY", "TFOOT", "FORM", "FIELDSET", "LEGEND", "LABEL", "BR", "HR", ]; const DEFAULT_SKIP_TAGS = [ "CODE", "KBD", "PRE", "SAMP", "TT", "VAR", "SCRIPT", "STYLE", "TEXTAREA", "INPUT", ]; const PASSES: Pass[] = [ jiyaPass, jiyaAdjacencyPass, longWordPass, spacingPass, jinzePass, orphanPass, lineEdgePass, gapTrimPass, ]; function feature(v: unknown, dflt: boolean): boolean { return v === undefined ? dflt : !!v; } function resolveLongWord(v: boolean | LongWordOptions | undefined): { minLength: number; lang: string } { if (v === false) { return { minLength: 6, lang: "en" }; } // 值不用(enabled 為 false) if (v === undefined || v === true || v === null) { return { minLength: 6, lang: "en" }; } const minLength = typeof v.minLength === "number" && v.minLength >= 2 ? v.minLength : 6; const lang = typeof v.lang === "string" && v.lang ? v.lang : "en"; return { minLength, lang }; } function resolveOrphan(v: boolean | OrphanOptions | undefined): { chars: number } { if (v && typeof v === "object") { const chars = typeof v.chars === "number" && v.chars >= 1 ? Math.floor(v.chars) : 2; return { chars }; } return { chars: 2 }; } function resolveJiya( v: boolean | JiyaOptions | undefined, ): { halfWidth: "halt" | "margin"; bias: BiasTable } { if (v && typeof v === "object") { return { halfWidth: v.halfWidth === "margin" ? "margin" : "halt", bias: v.bias || {}, }; } return { halfWidth: "halt", bias: {} }; } /** 正規化公開選項為內部 ResolvedOptions。 */ export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions { const langOpt = opts.lang || {}; const scope = opts.scope || {}; return { locale: { default: langOpt.default || "zh-Hant", ...(langOpt.style ? { style: langOpt.style } : {}), policy: langOpt.policy || {}, }, ruleset: resolveRuleset(opts.charClass), autospaceClass: opts.autospaceClass || "", justifyAtoms: opts.justifyAtoms === false ? false : true, levelText: (opts.level && opts.level.text) || null, scope: { root: scope.root || null, include: scope.include || null, avoid: scope.avoid || null, }, features: { spacing: feature(opts.spacing, true), longWord: opts.longWord === false ? false : feature(opts.longWord, true), jiya: feature(opts.jiya, true), jinze: feature(opts.jinze, true), orphan: opts.orphan === false ? false : feature(opts.orphan, true), hanging: feature(opts.hanging, false), biaodian: feature(opts.biaodian, false), emphasis: feature(opts.emphasis, false), ruby: feature(opts.ruby, false), }, longWord: resolveLongWord(opts.longWord), orphan: resolveOrphan(opts.orphan), // 行端模式恒為 squeeze(行端半形)。**標點懸掛 hang 已全面停用**(§6.5.2/§6.5.6): // 經 headless + 真實瀏覽器實測,純負 margin 機制不可行——句號墨色偏框左下角,安全 // 凸出量(<1em)只把空白半框推出版心、墨色仍在內(視覺不懸掛);凸出滿 1em 又會釋放 // 一字寬、把下一字拉上本行致重疊;且 ResizeObserver 重排時兩階段標記錯亂。原生 // hanging-punctuation 為正解但 Chrome 不支援、且與本庫 inline-block 原子衝突。 // 故此處**唯一關卡**鎖死為 squeeze——即使 opts.hanging 為 true 亦不啟用 hang(無逃逸); // hang 之 CSS/lineedge 程式碼保留(休眠),俟原生支援成熟再議。 lineEnd: "squeeze", jiyaConfig: resolveJiya(opts.jiya), blocks: Array.isArray(opts.blocks) ? opts.blocks.slice() : [], finder: { styleInlines: new Set(opts.styleInlines || DEFAULT_STYLE_INLINES), blockTags: new Set(opts.blockTags || DEFAULT_BLOCK_TAGS), skipTags: new Set(opts.skipTags || DEFAULT_SKIP_TAGS), pillSelector: opts.pillSelector || "code, kbd", skipAttribute: opts.skipAttribute || null, isSkipped: typeof opts.isSkipped === "function" ? opts.isSkipped : null, }, }; } function resolveRoot( arg: Element | string | undefined, options: ResolvedOptions, ): Element | null { const pick = arg !== undefined ? arg : options.scope.root; if (pick && typeof pick !== "string") { return pick; } if (typeof pick === "string" && typeof document !== "undefined") { return document.querySelector(pick); } return typeof document !== "undefined" ? document.body : null; } export interface Juzhen { render(root?: Element | string): void; revert(root?: Element | string): void; readonly options: ResolvedOptions; } /** 建立一個聚珍實例。 */ export function createJuzhen(opts: JuzhenOptions = {}): Juzhen { const options = normalizeOptions(opts); function makeCtx(root: Element): RenderContext { return { root, options, finder: new Finder(options) }; } return { options, render(root?: Element | string): void { const el = resolveRoot(root, options); if (!el) { return; } runPasses(PASSES, makeCtx(el)); // margin 後備模式以 root 屬性作 CSS 開關(見 css/_jiya.css);halt 模式不設。 if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") { el.setAttribute("data-jz-halfwidth", "margin"); } // justifyAtoms:false → jz-char/jz-jinze 改 inline(分頁器相容,見 css)。 if (!options.justifyAtoms) { el.setAttribute("data-jz-atoms", "inline"); } }, revert(root?: Element | string): void { const el = resolveRoot(root, options); if (!el) { return; } revertPasses(PASSES, makeCtx(el)); el.removeAttribute("data-jz-halfwidth"); el.removeAttribute("data-jz-atoms"); }, }; }