75 lines
3.5 KiB
TypeScript
75 lines
3.5 KiB
TypeScript
/**
|
|
* دوزبانهسازی خودکار متن مقاله — قاعده: متن لاتین همیشه «۲ پیکسل کوچکتر» از متن
|
|
* فارسیِ همان بلوک است (تیتر۱ ۱۸→۱۶، تیتر۲ ۱۶→۱۴، تیتر۳ و پاراگراف ۱۴→۱۲) و در
|
|
* مقاله انگلیسی برعکس، واژههای فارسی ۲ پیکسل بزرگتر میشوند.
|
|
*
|
|
* اجرای غیرغالب متن با یک span جهتدار جدا میشود تا نه جهت متن بههم بریزد
|
|
* (unicode-bidi: isolate) و نه خط پایه جابهجا شود؛ اندازه در CSS با
|
|
* calc(1em - 2px) نسبت به همان بلوک حساب میشود.
|
|
*/
|
|
|
|
/** اجرای متن لاتین — حروف/ارقام لاتین بههمراه فاصله و نشانههای داخلی */
|
|
const LATIN_PATTERN =
|
|
"[A-Za-z0-9\\u00c0-\\u024f]+(?:[ \\t.,:;'\"()&/+%\\u2013\\u2014-]+[A-Za-z0-9\\u00c0-\\u024f]+)*"
|
|
|
|
/** اجرای متن فارسی/عربی — با نیمفاصله و ارقام فارسی */
|
|
const RTL_PATTERN = '[\\u0590-\\u08ff\\u200c]+(?:[ \\t\\u200c]+[\\u0590-\\u08ff\\u200c]+)*'
|
|
|
|
/** کلاس span متن لاتین و متن فارسی — همینها در CSS اندازه ۲ پیکسل تفاضلی میگیرند */
|
|
export const LATIN_CLASS = 'latin'
|
|
export const RTL_CLASS = 'persian'
|
|
|
|
/** عناصری که متنشان دستنخورده میماند (کد، مارکهای قدیمی و اجرای بستهبندیشده) */
|
|
const SKIP_SELECTOR = 'code, pre, kbd, samp, script, style, .english-text, [data-small], .latin, .persian'
|
|
|
|
/** همه اجراهای غیرغالب یک متن — با بازه نسبت به ابتدای همان متن */
|
|
export function findScriptRuns(text: string, dir: 'rtl' | 'ltr'): { from: number; to: number }[] {
|
|
const scan = new RegExp(dir === 'rtl' ? LATIN_PATTERN : RTL_PATTERN, 'g')
|
|
const runs: { from: number; to: number }[] = []
|
|
|
|
for (const match of text.matchAll(scan)) {
|
|
const from = match.index ?? 0
|
|
runs.push({ from, to: from + match[0].length })
|
|
}
|
|
|
|
return runs
|
|
}
|
|
|
|
/**
|
|
* بستهبندی اجرای غیرغالب همه گرههای متنی داخل یک ریشه HTML.
|
|
* در مقاله فارسی اجرای لاتین و در مقاله انگلیسی اجرای فارسی جدا میشود.
|
|
*/
|
|
export function wrapScriptRuns(root: HTMLElement, dir: 'rtl' | 'ltr'): void {
|
|
const document = root.ownerDocument
|
|
const className = dir === 'rtl' ? LATIN_CLASS : RTL_CLASS
|
|
const runDir = dir === 'rtl' ? 'ltr' : 'rtl'
|
|
|
|
const texts: Text[] = []
|
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
const text = node as Text
|
|
if (!text.parentElement || text.parentElement.closest(SKIP_SELECTOR)) continue
|
|
if (!text.data.trim()) continue
|
|
if (findScriptRuns(text.data, dir).length) texts.push(text)
|
|
}
|
|
|
|
for (const text of texts) {
|
|
const value = text.data
|
|
const fragment = document.createDocumentFragment()
|
|
let cursor = 0
|
|
|
|
for (const run of findScriptRuns(value, dir)) {
|
|
if (run.from > cursor) fragment.append(value.slice(cursor, run.from))
|
|
const span = document.createElement('span')
|
|
span.className = className
|
|
span.setAttribute('dir', runDir)
|
|
span.textContent = value.slice(run.from, run.to)
|
|
fragment.append(span)
|
|
cursor = run.to
|
|
}
|
|
|
|
if (cursor < value.length) fragment.append(value.slice(cursor))
|
|
text.replaceWith(fragment)
|
|
}
|
|
}
|