"use strict"; var TabLocalFiles; (function (TabLocalFiles) { TabLocalFiles.RUNTIME_VERSION = "0.1.7"; })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var TextCodec; (function (TextCodec) { const BOM = ""; const UTF8_BOM_BYTES = [0xef, 0xbb, 0xbf]; function detectLineEnding(text) { let crlf = 0; let lf = 0; let cr = 0; for (let index = 0; index < text.length; index += 1) { const code = text.charCodeAt(index); if (code === 13) { if (text.charCodeAt(index + 1) === 10) { crlf += 1; index += 1; } else { cr += 1; } } else if (code === 10) { lf += 1; } } if (crlf === 0 && lf === 0 && cr === 0) { return "unknown"; } if (crlf >= lf && crlf >= cr) { return "crlf"; } return lf >= cr ? "lf" : "cr"; } TextCodec.detectLineEnding = detectLineEnding; function normalizeLineBreaks(text) { return text.replace(/\r\n?/g, "\n"); } TextCodec.normalizeLineBreaks = normalizeLineBreaks; function ingest(text, encoding, hasBomHint = false) { const hasBom = hasBomHint || text.startsWith(BOM); const withoutBom = text.startsWith(BOM) ? text.slice(BOM.length) : text; return { text: normalizeLineBreaks(withoutBom), profile: { encoding: normalizeEncodingLabel(encoding), hasBom, lineEnding: detectLineEnding(withoutBom) } }; } TextCodec.ingest = ingest; function emit(text, profile) { const normalized = normalizeLineBreaks(text); switch (profile?.lineEnding) { case "crlf": return normalized.replace(/\n/g, "\r\n"); case "cr": return normalized.replace(/\n/g, "\r"); default: return normalized; } } TextCodec.emit = emit; function encode(text, profile) { const encoding = profile?.encoding ?? "utf-8"; const hasBom = profile?.hasBom ?? false; if (encoding === "utf-8") { return { bytes: encodeUtf8(text, hasBom), encoding, lossy: false }; } if (encoding === "utf-16le" || encoding === "utf-16be") { return { bytes: encodeUtf16(text, encoding === "utf-16le", hasBom), encoding, lossy: false }; } const legacy = encodeLegacy(text, encoding); if (legacy) { return { bytes: legacy, encoding, lossy: false }; } return { bytes: encodeUtf8(text, false), encoding: "utf-8", lossy: true }; } TextCodec.encode = encode; const profiles = new Map(); function rememberProfile(path, profile) { profiles.set(path, profile); } TextCodec.rememberProfile = rememberProfile; function profileFor(path) { return profiles.get(path); } TextCodec.profileFor = profileFor; function forgetProfile(path) { profiles.delete(path); } TextCodec.forgetProfile = forgetProfile; function normalizeEncodingLabel(label) { const normalized = label?.trim().toLowerCase(); if (!normalized || normalized === "utf8" || normalized === "unicode-1-1-utf-8") { return "utf-8"; } if (normalized === "gbk" || normalized === "gb2312" || normalized === "chinese") { return "gb18030"; } return normalized; } function encodeUtf8(text, withBom) { const body = new TextEncoder().encode(text); if (!withBom) { return body; } const bytes = new Uint8Array(UTF8_BOM_BYTES.length + body.length); bytes.set(UTF8_BOM_BYTES, 0); bytes.set(body, UTF8_BOM_BYTES.length); return bytes; } function encodeUtf16(text, littleEndian, withBom) { const units = withBom ? text.length + 1 : text.length; const bytes = new Uint8Array(units * 2); const view = new DataView(bytes.buffer); let offset = 0; if (withBom) { view.setUint16(0, 0xfeff, littleEndian); offset = 2; } for (let index = 0; index < text.length; index += 1) { view.setUint16(offset, text.charCodeAt(index), littleEndian); offset += 2; } return bytes; } const reverseTables = new Map(); function fatalDecoder(encoding) { try { return new TextDecoder(encoding, { fatal: true }); } catch { return undefined; } } function decodeSingleCodePoint(decoder, bytes) { try { const decoded = decoder.decode(bytes); const codePoint = decoded.codePointAt(0); if (codePoint === undefined || codePoint === 0xfffd || decoded.length !== String.fromCodePoint(codePoint).length) { return undefined; } return codePoint; } catch { return undefined; } } function reverseTableFor(encoding) { const cached = reverseTables.get(encoding); if (cached) { return cached; } const decoder = fatalDecoder(encoding); if (!decoder) { return undefined; } const table = { single: new Map(), double: new Map() }; const one = new Uint8Array(1); for (let byte = 0; byte < 0x100; byte += 1) { one[0] = byte; const codePoint = decodeSingleCodePoint(decoder, one); if (codePoint !== undefined && !table.single.has(codePoint)) { table.single.set(codePoint, byte); } } const two = new Uint8Array(2); for (let lead = 0x81; lead <= 0xfe; lead += 1) { for (let trail = 0x40; trail <= 0xfe; trail += 1) { two[0] = lead; two[1] = trail; const codePoint = decodeSingleCodePoint(decoder, two); if (codePoint !== undefined && !table.single.has(codePoint) && !table.double.has(codePoint)) { table.double.set(codePoint, (lead << 8) | trail); } } } reverseTables.set(encoding, table); return table; } function gb18030QuadTable(table) { if (table.quad) { return table.quad; } const quad = new Map(); const decoder = fatalDecoder("gb18030"); if (decoder) { const four = new Uint8Array(4); for (let b1 = 0x81; b1 <= 0x84; b1 += 1) { for (let b2 = 0x30; b2 <= 0x39; b2 += 1) { for (let b3 = 0x81; b3 <= 0xfe; b3 += 1) { for (let b4 = 0x30; b4 <= 0x39; b4 += 1) { four[0] = b1; four[1] = b2; four[2] = b3; four[3] = b4; const codePoint = decodeSingleCodePoint(decoder, four); if (codePoint !== undefined && !quad.has(codePoint)) { quad.set(codePoint, ((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) >>> 0); } } } } } } table.quad = quad; return quad; } function encodeLegacy(text, encoding) { const table = reverseTableFor(encoding); if (!table) { return undefined; } const output = []; for (const character of text) { const codePoint = character.codePointAt(0); const single = table.single.get(codePoint); if (single !== undefined) { output.push(single); continue; } const double = table.double.get(codePoint); if (double !== undefined) { output.push(double >> 8, double & 0xff); continue; } if (encoding === "gb18030") { if (codePoint >= 0x10000) { const linear = codePoint - 0x10000; output.push(0x90 + Math.floor(linear / 12600), 0x30 + Math.floor((linear % 12600) / 1260), 0x81 + Math.floor((linear % 1260) / 10), 0x30 + (linear % 10)); continue; } const quad = gb18030QuadTable(table).get(codePoint); if (quad !== undefined) { output.push((quad >>> 24) & 0xff, (quad >>> 16) & 0xff, (quad >>> 8) & 0xff, quad & 0xff); continue; } } return undefined; } return Uint8Array.from(output); } })(TextCodec = TabLocalFiles.TextCodec || (TabLocalFiles.TextCodec = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ExternalChanges; (function (ExternalChanges) { const LCS_CELL_BUDGET = 40000; const MAX_ENTRIES = 200; ExternalChanges.STORAGE_KEY = "tab-local-files:external-change-marks"; function resolveEnabled(stored) { return stored !== "false"; } ExternalChanges.resolveEnabled = resolveEnabled; function readStoredEnabled() { try { return resolveEnabled(window.localStorage.getItem(ExternalChanges.STORAGE_KEY)); } catch { return true; } } ExternalChanges.readStoredEnabled = readStoredEnabled; function diffLineHunks(oldText, newText) { if (oldText === newText) { return []; } const oldLines = oldText.split("\n"); const newLines = newText.split("\n"); const maxPrefix = Math.min(oldLines.length, newLines.length); let prefix = 0; while (prefix < maxPrefix && oldLines[prefix] === newLines[prefix]) { prefix += 1; } let suffix = 0; while (suffix < maxPrefix - prefix && oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]) { suffix += 1; } const oldEnd = oldLines.length - suffix; const newEnd = newLines.length - suffix; const oldMiddle = oldLines.slice(prefix, oldEnd); const newMiddle = newLines.slice(prefix, newEnd); if (!oldMiddle.length && !newMiddle.length) { return []; } if (oldMiddle.length * newMiddle.length > LCS_CELL_BUDGET) { return [{ oldStart: prefix, oldEnd, newStart: prefix, newEnd }]; } return lcsHunks(oldMiddle, newMiddle).map((hunk) => ({ oldStart: hunk.oldStart + prefix, oldEnd: hunk.oldEnd + prefix, newStart: hunk.newStart + prefix, newEnd: hunk.newEnd + prefix })); } ExternalChanges.diffLineHunks = diffLineHunks; function lcsHunks(oldLines, newLines) { const rows = oldLines.length; const columns = newLines.length; const dp = Array.from({ length: rows + 1 }, () => new Array(columns + 1).fill(0)); for (let i = rows - 1; i >= 0; i -= 1) { for (let j = columns - 1; j >= 0; j -= 1) { dp[i][j] = oldLines[i] === newLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]); } } const hunks = []; let current; const open = (i, j) => { if (!current) { current = { oldStart: i, oldEnd: i, newStart: j, newEnd: j }; } }; const close = () => { if (current) { hunks.push(current); current = undefined; } }; let i = 0; let j = 0; while (i < rows || j < columns) { if (i < rows && j < columns && oldLines[i] === newLines[j]) { close(); i += 1; j += 1; } else if (j >= columns || (i < rows && dp[i + 1][j] >= dp[i][j + 1])) { open(i, j); i += 1; current.oldEnd = i; } else { open(i, j); j += 1; current.newEnd = j; } } close(); return hunks; } function mapLine(hunks, line, bias) { let delta = 0; for (const hunk of hunks) { if (bias > 0 ? line <= hunk.oldStart : line < hunk.oldStart) { break; } if (line < hunk.oldEnd) { return bias < 0 ? hunk.newStart : hunk.newEnd; } delta += hunk.newEnd - hunk.newStart - (hunk.oldEnd - hunk.oldStart); } return line + delta; } ExternalChanges.mapLine = mapLine; function touches(entry, hunk) { if (entry.kind === "del") { return hunk.oldStart <= entry.start && entry.start <= hunk.oldEnd; } if (hunk.oldStart === hunk.oldEnd) { return hunk.oldStart > entry.start && hunk.oldStart < entry.end; } return entry.start < hunk.oldEnd && entry.end > hunk.oldStart; } function shifted(entry, hunks) { const start = mapLine(hunks, entry.start, -1); if (entry.kind === "del") { return { ...entry, start, end: start }; } return { ...entry, start, end: Math.max(start, mapLine(hunks, entry.end, 1)) }; } function createLedger() { let entries = []; let nextId = 1; const listeners = new Set(); const notify = () => { listeners.forEach((listener) => { try { listener(); } catch (error) { console.warn("[Previewer] External change listener failed.", error); } }); }; const commit = (next) => { next.sort((a, b) => a.start - b.start || a.id - b.id); entries = next.length > MAX_ENTRIES ? next.slice(next.length - MAX_ENTRIES) : next; notify(); }; return { entries: () => entries.slice(), size: () => entries.length, applyExternal: (oldText, newText, now = Date.now()) => { const hunks = diffLineHunks(oldText, newText); if (!hunks.length) { return []; } const kept = []; const created = []; const consumed = new Set(); for (const hunk of hunks) { const touched = entries.filter((entry) => !consumed.has(entry) && touches(entry, hunk)); touched.forEach((entry) => consumed.add(entry)); const oldLength = hunk.oldEnd - hunk.oldStart; const newLength = hunk.newEnd - hunk.newStart; if (newLength === 0) { created.push({ id: nextId++, kind: "del", start: hunk.newStart, end: hunk.newStart, at: now }); continue; } const blocks = touched.filter((entry) => entry.kind !== "del"); const coveredByAdds = oldLength === 0 || (blocks.every((entry) => entry.kind === "add") && blocks.some((entry) => entry.start <= hunk.oldStart && entry.end >= hunk.oldEnd)); let start = hunk.newStart; let end = hunk.newEnd; for (const entry of blocks) { start = Math.min(start, mapLine(hunks, entry.start, -1)); end = Math.max(end, mapLine(hunks, entry.end, 1)); } created.push({ id: nextId++, kind: coveredByAdds ? "add" : "mod", start, end, at: now }); } for (const entry of entries) { if (!consumed.has(entry)) { kept.push(shifted(entry, hunks)); } } commit(kept.concat(created)); return created.slice(); }, applyLocal: (oldText, newText) => { if (!entries.length) { return false; } const hunks = diffLineHunks(oldText, newText); if (!hunks.length) { return false; } const next = entries .filter((entry) => !hunks.some((hunk) => touches(entry, hunk))) .map((entry) => shifted(entry, hunks)) .filter((entry) => entry.kind === "del" || entry.end > entry.start); commit(next); return true; }, markRead: (ids) => { const drop = new Set(ids); const next = entries.filter((entry) => !drop.has(entry.id)); if (next.length === entries.length) { return false; } commit(next); return true; }, markAllRead: () => { if (!entries.length) { return false; } commit([]); return true; }, onChange: (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; } }; } ExternalChanges.createLedger = createLedger; })(ExternalChanges = TabLocalFiles.ExternalChanges || (TabLocalFiles.ExternalChanges = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ChatEditDocument; (function (ChatEditDocument) { const MAX_TEXT = 512 * 1024; const MAX_HISTORY = 256; const MAX_TARGETS = 64; const MAX_OPERATIONS = 128; const fail = (error) => ({ success: false, error }); function textChange(before, after) { if (before === after) return []; let start = 0; while (start < before.length && start < after.length && before[start] === after[start]) start++; let end = before.length; let nextEnd = after.length; while (end > start && nextEnd > start && before[end - 1] === after[nextEnd - 1]) { end--; nextEnd--; } return [{ start, end, replacement: after.slice(start, nextEnd) }]; } ChatEditDocument.textChange = textChange; function escapesRange(before, after, allowed) { const head = before.slice(0, allowed.start); const tail = before.slice(allowed.end); return after.length < head.length + tail.length || !after.startsWith(head) || !after.endsWith(tail); } ChatEditDocument.escapesRange = escapesRange; function replaceRanges(text, edits) { for (let i = edits.length - 1; i >= 0; i--) { const edit = edits[i]; text = text.slice(0, edit.start) + edit.replacement + text.slice(edit.end); } return text; } ChatEditDocument.replaceRanges = replaceRanges; function mapRange(range, edits) { let shift = 0; for (const edit of edits) { if (range.start === range.end && edit.start <= range.start && edit.end >= range.end) return undefined; const insertion = edit.start === edit.end; if (insertion ? edit.start > range.start && edit.start < range.end : edit.start < range.end && edit.end > range.start) { return undefined; } if (edit.end <= range.start) shift += edit.replacement.length - (edit.end - edit.start); } return { ...range, start: range.start + shift, end: range.end + shift }; } function inverseEdits(text, edits) { let shift = 0; return edits.map((edit) => { const start = edit.start + shift; shift += edit.replacement.length - (edit.end - edit.start); return { start, end: start + edit.replacement.length, replacement: text.slice(edit.start, edit.end) }; }); } function create(initialText) { let text = initialText; let revision = 0; let disposed = false; let committing = false; const history = []; const targets = new Map(); const operations = new Map(); const commit = (next, edits = textChange(text, next)) => { if (next === text) return; text = next; history.push({ revision: ++revision, edits }); if (history.length > MAX_HISTORY) history.shift(); }; const mapFrom = (ranges, from) => { if (from > revision || from < (history[0]?.revision ?? revision + 1) - 1) return undefined; for (const change of history) { if (change.revision <= from) continue; const mapped = ranges.map((range) => mapRange(range, change.edits)); if (mapped.some((range) => !range)) return undefined; ranges = mapped; } return ranges; }; const hull = (edits) => ({ start: Math.min(...edits.map((edit) => edit.start)), end: Math.max(...edits.map((edit) => edit.end)) }); const applySurface = (edits, allowed, applyToSurface) => { const before = text; const desired = replaceRanges(before, edits); committing = true; let actual; try { if (escapesRange(before, applyToSurface(edits, desired, true), allowed)) throw new Error("OUT_OF_SCOPE_EDIT"); actual = applyToSurface(edits, desired, false); } finally { committing = false; } const actualEdits = actual === desired ? edits : textChange(before, actual); const inverse = inverseEdits(before, actualEdits); commit(actual, actualEdits); if (escapesRange(before, actual, allowed)) throw new Error("OUT_OF_SCOPE_EDIT"); return inverse; }; const currentScope = (target) => { if (target.context.scope === "document") return { start: 0, end: text.length, replacement: "" }; const original = target.snapshots.get(target.context.revision); return mapFrom([{ start: original.start, end: original.end, replacement: "" }], target.context.revision)?.[0]; }; const advanceTarget = (target, range, before, edits) => { if (!range) return; const changes = replaceRanges(before, edits) === text ? edits : textChange(before, text); if (changes.some((edit) => edit.start < range.start || edit.end > range.end)) return; const end = range.end + text.length - before.length; const content = text.slice(range.start, end); target.context = { ...target.context, revision, text: content }; target.snapshots.set(revision, { start: range.start, end, text: content }); }; return { getText: () => text, observe: (next) => { if (!disposed && !committing) commit(next); }, capture: (start, end, surface, scope) => { if (disposed) throw new Error("TARGET_UNAVAILABLE"); if (text.length > MAX_TEXT) throw new Error("DOCUMENT_TOO_LARGE"); if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > text.length) { throw new Error("INVALID_RANGE"); } if (scope === "document") { start = 0; end = text.length; } for (const target of targets.values()) { const snapshot = target.snapshots.get(revision); if (target.context.revision === revision && target.context.surface === surface && target.context.scope === scope && snapshot?.start === start && snapshot.end === end) return { ...target.context }; } const snapshot = { start, end, text: text.slice(start, end) }; const context = { contextId: crypto.randomUUID(), revision, surface, scope, text: snapshot.text }; targets.set(context.contextId, { context, snapshots: new Map([[revision, snapshot]]) }); if (targets.size > MAX_TARGETS) targets.delete(targets.keys().next().value); return { ...context }; }, read: (contextId) => { const target = targets.get(contextId); if (disposed || !target) return fail("TARGET_UNAVAILABLE"); let start = 0; let end = text.length; if (target.context.scope === "selection") { const original = target.snapshots.get(target.context.revision); const mapped = mapFrom([{ start: original.start, end: original.end, replacement: "" }], target.context.revision); if (!mapped) return fail("EDIT_CONFLICT"); ({ start, end } = mapped[0]); } const content = text.slice(start, end); target.snapshots.set(revision, { start, end, text: content }); return { success: true, text: content, revision }; }, apply: (request, applyToSurface) => { if (disposed) return fail("TARGET_UNAVAILABLE"); if (!request || typeof request.operationId !== "string" || !request.operationId || request.operationId.length > 160 || !Number.isInteger(request.baseRevision) || !Array.isArray(request.edits) || !request.edits.length || request.edits.length > 32) { return fail("INVALID_EDIT"); } const signature = JSON.stringify([request.contextId, request.baseRevision, request.edits]); const previous = operations.get(request.operationId); if (previous) return previous.signature === signature ? { ...previous.result } : fail("OPERATION_ID_REUSED"); const target = targets.get(request.contextId); if (!target) return fail("TARGET_UNAVAILABLE"); const base = target.snapshots.get(request.baseRevision); if (!base) return fail("STALE_REVISION"); const edits = []; for (const edit of request.edits) { if (!edit || typeof edit.find !== "string" || typeof edit.replace !== "string" || edit.replace.length > MAX_TEXT) return fail("INVALID_EDIT"); if (!edit.find && base.text) return fail("INVALID_EDIT"); const index = base.text.indexOf(edit.find); if (index < 0) return fail("MATCH_NOT_FOUND"); if (edit.find && base.text.indexOf(edit.find, index + 1) >= 0) return fail("AMBIGUOUS_MATCH"); edits.push({ start: base.start + index, end: base.start + index + edit.find.length, replacement: edit.replace }); } edits.sort((a, b) => a.start - b.start); if (edits.some((edit, i) => i > 0 && edit.start <= edits[i - 1].end)) return fail("OVERLAPPING_EDITS"); const mapped = mapFrom(edits, request.baseRevision); if (!mapped) return fail("EDIT_CONFLICT"); if (replaceRanges(text, mapped).length > MAX_TEXT) return fail("DOCUMENT_TOO_LARGE"); try { const before = text; const scope = currentScope(target); const inverse = applySurface(mapped, scope ?? hull(mapped), applyToSurface); advanceTarget(target, scope, before, mapped); const result = { success: true, status: before === text ? "unchanged" : "applied", revision, operationId: request.operationId }; operations.set(request.operationId, { contextId: request.contextId, signature, result, inverse, revision }); if (operations.size > MAX_OPERATIONS) operations.delete(operations.keys().next().value); return { ...result }; } catch (error) { return fail(error instanceof Error ? error.message : String(error)); } }, undo: (contextId, operationId, applyToSurface) => { if (disposed) return fail("TARGET_UNAVAILABLE"); const operation = operations.get(operationId); if (!operation || operation.contextId !== contextId) return fail("OPERATION_UNAVAILABLE"); if (operation.result.status === "undone") return { ...operation.result }; const mapped = mapFrom(operation.inverse, operation.revision); if (!mapped) return fail("EDIT_CONFLICT"); try { const target = targets.get(contextId); const scope = target && currentScope(target); const before = text; applySurface(mapped, hull(mapped), applyToSurface); if (target) advanceTarget(target, scope, before, mapped); operation.result = { success: true, status: "undone", operationId, revision }; return { ...operation.result }; } catch (error) { return fail(error instanceof Error ? error.message : String(error)); } }, dispose: () => { disposed = true; targets.clear(); operations.clear(); history.length = 0; } }; } ChatEditDocument.create = create; })(ChatEditDocument = TabLocalFiles.ChatEditDocument || (TabLocalFiles.ChatEditDocument = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ChatSource; (function (ChatSource) { function project(source, tokens) { const result = { text: "", spans: [], wrappers: [] }; const append = (text, start, end) => { for (let i = 0; i < text.length; i++) { result.spans.push({ start: end === undefined ? start + i : start, end: end ?? start + i + 1 }); } result.text += text; }; const contentOffset = (token) => { switch (token.type) { case "paragraph": return 0; case "heading": return /^ {0,3}#{1,6}(?=[ \t\n]|$)[ \t]*/.exec(token.raw)?.[0].length ?? 0; case "strong": return 2; case "em": return 1; case "del": return token.raw.startsWith("~~") ? 2 : 1; case "link": return token.raw[0] === "[" || token.raw[0] === "<" ? 1 : 0; default: return undefined; } }; const walk = (items, raw, base, first = 0) => { let cursor = first; for (const token of items) { if (!raw.startsWith(token.raw, cursor)) return false; const at = cursor; cursor = at + token.raw.length; const start = base + at; if (token.type === "space") continue; if (["paragraph", "heading", "strong", "em", "del", "link"].includes(token.type) && token.tokens) { const from = result.text.length; const offset = contentOffset(token); if (offset === undefined || !walk(token.tokens, token.raw, start, offset)) return false; if (["strong", "em", "del", "link"].includes(token.type)) { result.wrappers.push({ start, end: start + token.raw.length, from, to: result.text.length }); } } else if (token.type === "text" || token.type === "escape") { if (token.tokens) { if (!walk(token.tokens, token.raw, start)) return false; continue; } const value = token.type === "escape" ? token.text ?? "" : token.raw; if (token.type === "escape") { append(value, start, start + token.raw.length); continue; } let offset = 0; const entities = /&(?:amp|lt|gt|quot|apos|#\d+|#x[\da-f]+);/gi; for (const match of value.matchAll(entities)) { const index = match.index; append(value.slice(offset, index), start + offset); const body = match[0].slice(1, -1).toLowerCase(); const number = body.startsWith("#x") ? parseInt(body.slice(2), 16) : body.startsWith("#") ? parseInt(body.slice(1), 10) : undefined; const decoded = number !== undefined && number > 0 && number <= 0x10ffff ? String.fromCodePoint(number) : { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }[body] ?? match[0]; append(decoded, start + index, start + index + match[0].length); offset = index + match[0].length; } append(value.slice(offset), start + offset); } else if (token.type === "codespan" && token.text) { const ticks = /^`+/.exec(token.raw)[0].length; const inner = token.raw.slice(ticks, token.raw.length - ticks); const padding = inner === token.text ? 0 : inner === ` ${token.text} ` ? 1 : -1; if (padding < 0) return false; const from = result.text.length; append(token.text, start + ticks + padding); result.wrappers.push({ start, end: start + token.raw.length, from, to: result.text.length }); } else { return false; } } return true; }; return walk(tokens, source, 0) ? result : undefined; } ChatSource.project = project; function sourceRange(projection, from, to) { if (from < 0 || to <= from || to > projection.text.length) return undefined; let start = projection.spans[from].start; let end = projection.spans[to - 1].end; for (const wrapper of projection.wrappers) { if (to <= wrapper.from || from >= wrapper.to) continue; if (from >= wrapper.from && to <= wrapper.to) continue; if (from > wrapper.from || to < wrapper.to) return undefined; start = Math.min(start, wrapper.start); end = Math.max(end, wrapper.end); } return { start, end }; } ChatSource.sourceRange = sourceRange; })(ChatSource = TabLocalFiles.ChatSource || (TabLocalFiles.ChatSource = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { const DEFAULT_LOCALE = "zh-CN"; const I18N_MESSAGES = { "zh-CN": { "app.description": "本地文件渲染", "viewer.openingTitle": "正在打开文件", "viewer.openingDescription": "Previewer 正在准备文件预览。", "common.fileName": "文件名", "common.fileSize": "大小", "common.fileCreated": "创建时间", "common.fileModified": "修改时间", "common.today": "今天", "common.yesterday": "昨天", "common.filePath": "文件路径", "common.pathEllipsis": "...", "common.showCollapsedPath": "显示折叠的中间路径", "common.collapsedPathSegments": "折叠的中间路径", "common.fileActions": "文件操作", "common.fileInfo": "文件信息", "common.moreActions": "更多操作", "common.renameFile": "重命名文件", "common.expandExplorer": "展开资源管理器", "common.collapseExplorer": "收起资源管理器", "common.expandHeader": "展开文件头", "common.collapseHeader": "收起文件头", "common.rawFile": "原始文件", "common.openInNewTab": "在新标签页打开", "common.save": "保存", "common.saveAs": "另存为……", "common.downloadRemoteFile": "下载到本地", "common.print": "打印", "print.preparing": "正在准备打印内容…", "print.failed": "无法准备打印内容", "print.notReady": "内容尚未加载完成,请稍后再试", "print.pageAlt": "打印第 {page} 页", "print.namedPageAlt": "{fileName} 第 {page} 页", "print.preparingPage": "正在准备第 {page}/{total} 页…", "print.preparingSlide": "正在准备第 {slide}/{total} 张幻灯片…", "print.namedSlideAlt": "{fileName} 第 {slide} 张幻灯片", "common.edit": "编辑", "common.preview": "预览", "common.copy": "复制", "common.copyAllText": "复制全部", "common.export": "导出", "common.revealInFileManager": "在文件管理器中显示", "common.revealInFinder": "在 Finder 中显示", "common.revealInExplorer": "在文件资源管理器中显示", "common.revealUnavailableRemote": "在线页面无法显示", "common.editUnavailableRemote": "在线页面无法编辑", "common.fileUnavailable": "该文件可能已被移至别处、修改或删除。", "common.unknown": "未知", "common.unknownError": "未知错误", "common.untitled": "Untitled", "common.localFile": "Local file", "common.loading": "正在加载...", "common.notModified": "未修改", "common.unsavedChanges": "有未保存的修改", "common.unexportedChanges": "有未导出的修改", "common.copiedToClipboard": "已复制到剪贴板", "common.copiedSelectedText": "已复制选中文本", "common.copyFailed": "复制失败", "common.saving": "正在保存...", "common.exporting": "正在导出...", "common.savingAs": "正在另存为...", "common.downloadingRemoteFile": "正在下载...", "common.savedToOriginal": "已保存到原文件", "common.saved": "已保存", "common.savedAs": "已另存为", "common.downloadedRemoteFile": "已下载到本地", "common.exportedToDownloads": "已导出到下载文件夹", "common.cancelledExport": "已取消导出", "common.cancelledSaveAs": "已取消另存为", "common.cancelledDownload": "已取消下载", "common.cancel": "取消", "common.saveFailedWorkspace": "保存失败:无法写入当前工作区文件", "common.exportFailed": "导出失败", "common.saveAsFailed": "另存失败", "common.downloadRemoteFailed": "下载失败:无法读取在线文件", "common.downloadUnavailable": "当前文件无法下载", "common.fileContent": "文件内容", "common.textFile": "Text file", "common.filePickerLocalFile": "Local file", "editor.clean": "已同步", "editor.external": "外部更新中", "editor.externalHint": "其他程序正在修改此文件,改动已自动同步到这里", "changes.unreadAbove": "上方 {count} 处未读改动", "changes.unreadBelow": "下方 {count} 处未读改动", "changes.markAllRead": "全部标为已读", "changes.jump": "跳到这处改动", "changes.added": "外部新增", "changes.modified": "外部修改", "changes.deleted": "外部删除了这里的内容", "editor.readonly": "只读", "editor.dirty": "未保存", "editor.saving": "保存中", "editor.saved": "已保存", "editor.autoSavePending": "待保存", "editor.autoSaved": "已自动保存", "editor.savedAsUtf8": "文件原为 {encoding} 编码,内容无法用该编码表示,已改为 UTF-8 保存", "editor.error": "保存失败", "editor.conflict": "文件冲突", "editor.draftRestored": "已恢复未保存草稿", "editor.draftOutOfSpace": "浏览器存储空间不足,本文档的未保存草稿无法留存。文字一个字也没被删减,建议尽快保存到文件。", "editor.unsavedTitle": "保存修改?", "editor.unsavedDescription": "“{fileName}”有未保存的修改。", "editor.saveBeforeLeave": "离开前保存“{fileName}”的修改?", "editor.discardBeforeLeave": "放弃“{fileName}”的未保存修改?", "editor.discard": "放弃修改", "editor.beforeUnload": "当前文件有未保存的修改。", "editor.revertTitle": "还原到打开文件时的内容?", "editor.revertDescription": "将丢弃“{fileName}”自本次打开以来的全部修改,包括已经自动保存到磁盘的部分。", "editor.revertConfirm": "还原", "editor.conflictTitle": "文件已在其他位置修改", "editor.conflictDescription": "“{fileName}”的磁盘内容已变化。为避免覆盖他人的修改,只有明确确认后才会继续保存。也可以取消并使用“另存为”。", "editor.overwrite": "仍然覆盖", "markdown.bold": "粗体", "markdown.italic": "斜体", "markdown.insertLink": "插入链接", "markdown.insertImage": "插入图片", "markdown.widthDecrease": "减小正文宽度", "markdown.contentWidth": "正文宽度", "markdown.widthIncrease": "增加正文宽度", "markdown.taskToggle": "切换任务状态", "markdown.taskReadonlyRemote": "在线 Markdown 只读,请下载到本地后编辑", "large.previewTitle": "大文件安全预览", "large.previewDescription": "已先加载前 {previewSize},滚动到底部继续按需加载;编辑和保存已禁用。", "large.previewAria": "{kind} 大文件预览片段", "large.previewMeta": "{kind} · 大文件预览 · {size}", "large.blockedTitle": "文件过大,已停止自动预览", "large.blockedDescription": "{kind} 文件大小 {size}。为避免浏览器崩溃,Previewer 没有读取完整内容。可以打开原始文件或在文件管理器中定位。", "large.blockedMeta": "{kind} · 文件过大 · {size}", "large.emptyPreview": "无法读取安全预览片段。", "large.virtualStatus": "正在显示 {start} - {end} / {total}", "large.virtualLoading": "正在加载下一段...", "large.virtualError": "加载失败,滚动后将重试。", "large.directoryLimited": "仅显示前 {count} 项,文件夹过大时不会一次渲染全部内容。", "toolbar.reveal": "在文件管理器中显示", "theme.followingBrowser": "跟随浏览器{theme}主题,点击可临时固定{nextTheme}", "theme.fixed": "已固定{theme}主题,点击恢复跟随浏览器", "theme.dark": "深色", "theme.light": "浅色", "rename.inProgress": "正在重命名...", "rename.success": "已重命名", "rename.failed": "重命名失败:{error}", "rename.invalidName": "文件名包含非法字符", "rename.duplicateName": "当前文件夹已存在同名文件", "rename.extensionNone": "无扩展名", "rename.extensionChangeTitle": "确认变更扩展名", "rename.extensionChangeDescription": "「{fileName}」的扩展名即将变更为「{extension}」,这会改变文件的打开方式。确认重命名吗?", "rename.extensionChangeConfirm": "确认重命名", "common.panelMode": "预览 / 编辑模式", "common.previewModeHint": "预览模式(⌘E 切换)", "common.editModeHint": "编辑模式(⌘E 切换)", "common.editUnsupportedType": "该格式不支持编辑", "common.copyAbsolutePath": "复制绝对路径", "common.copiedPath": "已复制绝对路径", "common.duplicateFile": "创建副本", "common.reloadFromDisk": "从磁盘刷新", "common.resizePanes": "拖动调整两栏宽度(双击恢复默认)", "common.reloadedFromDisk": "已从磁盘重新加载", "common.reloadFailed": "读取文件失败", "common.revert": "还原", "common.revertToOpened": "还原到打开文件时的内容", "common.revertUnavailable": "当前内容与打开文件时一致", "common.reverted": "已还原到打开文件时的内容", "common.saveNothingToSave": "没有需要保存的修改", "common.saveHandledByAutoSave": "自动保存已接管,无需手动保存", "common.moreTools": "更多工具", "common.undo": "撤销", "common.quoteFileToChat": "引用文件到 Chat", "common.undoChatEdit": "撤销 AI 修改", "common.chatEditConflict": "内容已被修改,无法安全撤销。", "common.redo": "重做", "common.fit": "适应", "common.zoomPercent": "缩放比例,可输入百分比", "duplicate.readFailed": "读取文件内容失败,无法创建副本", "duplicate.failed": "创建副本失败:{error}", "duplicate.success": "已创建副本 {fileName}", "duplicate.namePattern": "{name}(副本)", "duplicate.namePatternIndexed": "{name}(副本 {index})", "document.translate": "翻译文档并保存为新文件或覆盖原文件", "document.translateShort": "译文…", "document.translateTitle": "导出译文", "document.translateModeLabel": "输出内容", "document.translateModeTranslationFirst": "双语(译文在上)", "document.translateModeOriginalFirst": "双语(译文在下)", "document.translateModeReplace": "仅译文", "document.translateLanguageLabel": "目标语言", "document.translateOutputLabel": "保存方式", "document.translateOutputNew": "导出为新文件", "document.translateOutputOverwrite": "覆盖当前文件", "document.translateSummaryExport": "将翻译 {count} 个段落,结果导出为 {fileName},保存到下载目录。代码块、表格、链接地址保持原样。翻译会消耗账户用量。", "document.translateSummaryOverwrite": "将翻译 {count} 个段落并覆盖当前文件。代码块、表格、链接地址保持原样。翻译会消耗账户用量。", "document.translateNothing": "没有需要翻译的正文段落", "document.translateUnavailable": "翻译服务不可用,请确认翻译扩展已启用", "document.translateStart": "开始翻译", "document.translateCancel": "取消", "document.translateRunning": "正在翻译 {completed}/{total}", "document.translateCancelled": "已取消翻译", "document.translateFailed": "翻译失败:{error}", "document.translateSourceChanged": "文档在翻译期间被修改,已放弃覆盖保存", "document.translateDoneOverwrite": "已翻译 {count} 个段落并保存", "document.translateDoneExported": "已翻译 {count} 个段落,已导出 {fileName} 到下载目录", "document.translateKeptOriginal": ";{count} 个段落未能翻译,保留原文", "document.translateAllRejected": "译文会破坏文档格式,未写入任何内容", "markdown.toc": "目录", "markdown.tocResizeAria": "调整目录宽度", "markdown.tocResizeTitle": "拖拽调整目录宽度(双击恢复默认)", "markdown.readingWidth": "阅读宽度", "markdown.widthFixed": "默认", "markdown.widthFull": "全宽", "markdown.fontSize": "字号", "markdown.fontDecrease": "减小字号", "markdown.fontIncrease": "增大字号", "markdown.fontMedium": "中", "markdown.fontLarge": "大", "markdown.fontXLarge": "超大", "markdown.heading1": "一级标题", "markdown.heading2": "二级标题", "markdown.heading3": "三级标题", "markdown.heading4": "四级标题", "markdown.heading5": "五级标题", "markdown.strikethrough": "删除线", "markdown.inlineCode": "行内代码", "markdown.unorderedList": "无序列表", "markdown.orderedList": "有序列表", "markdown.taskList": "任务列表", "markdown.blockquote": "引用", "markdown.codeBlock": "代码块", "markdown.insertTable": "插入表格", "markdown.horizontalRule": "分割线", "markdown.clearFormat": "清除格式", "common.shortcutHint": "{label}({shortcut})", "markdown.tableHeader": "列", "markdown.tableCell": "内容", "markdown.editModeSwitch": "编辑方式", "markdown.editSource": "源码", "markdown.editSplit": "分屏", "markdown.editInline": "即时编辑", "markdown.inlineLoading": "正在加载即时编辑器...", "markdown.codeCopied": "已复制", "markdown.statLines": "行数", "markdown.statWords": "词数", "markdown.exportHtml": "HTML", "markdown.exportHtmlDone": "已导出 HTML", "markdown.exportHtmlFailed": "导出 HTML 失败", "markdown.exportPdf": "PDF", "markdown.exportPdfWorking": "正在生成 PDF…", "markdown.exportPdfDone": "已导出 PDF", "markdown.exportPdfFailed": "导出 PDF 失败", "markdown.exportImage": "长图 (PNG)", "markdown.exportImageWorking": "正在生成长图…", "markdown.exportImageDone": "已导出长图", "markdown.exportImageFailed": "导出长图失败", "markdown.exportImageTooTall": "文档太长,无法生成单张长图", "markdown.exportUnavailable": "当前环境不支持此导出方式", "markdown.inlineLoadFailed": "即时编辑器加载失败,已切回源码视图", "inline.placeholder": "开始输入…", "inline.codeSearchLanguage": "搜索语言", "inline.codeNoResult": "无结果", "inline.codePreview": "预览", "inline.codePreviewLoading": "正在渲染图表...", "inline.codeHideCode": "隐藏代码", "inline.linkPlaceholder": "粘贴链接...", "inline.imageUrlPlaceholder": "粘贴图片链接...", "markdown.searchPlaceholder": "在文档中搜索", "markdown.searchPrev": "上一个匹配", "markdown.searchNext": "下一个匹配", "markdown.searchClose": "关闭搜索", "markdown.imagePasteFailed": "图片保存失败", "markdown.formatTables": "格式化表格", "markdown.frontmatter": "文档属性", "markdown.frontmatterKey": "属性名", "markdown.frontmatterValue": "属性值", "markdown.frontmatterEmptyValue": "没有值", "markdown.frontmatterAdd": "添加文档属性", "markdown.frontmatterRemove": "移除该属性", "markdown.frontmatterNewKey": "新属性", "markdown.largeSourceOnly": "大文件仅支持源码编辑", "markdown.splitTooNarrow": "窗口太窄,放不下分屏的两栏", "markdown.footnotes": "脚注", "markdown.footnoteBackref": "返回引用处", "markdown.alertNote": "说明", "markdown.alertTip": "提示", "markdown.alertImportant": "重要", "markdown.alertWarning": "警告", "markdown.alertCaution": "危险", "markdown.alertAbstract": "摘要", "markdown.alertInfo": "信息", "markdown.alertTodo": "待办", "markdown.alertSuccess": "成功", "markdown.alertQuestion": "问题", "markdown.alertFailure": "失败", "markdown.alertDanger": "危险", "markdown.alertBug": "缺陷", "markdown.alertExample": "示例", "markdown.alertQuote": "引用", "markdown.searchToggleReplace": "显示替换", "markdown.searchCaseSensitive": "区分大小写", "markdown.searchReplacePlaceholder": "替换为", "markdown.searchReplaceOne": "替换", "markdown.searchReplaceAll": "全部替换", "svg.sourceEditorAria": "SVG 源码编辑器", "code.lineNumbers": "行号", "code.wordWrap": "自动换行", "code.highlightLanguage": "语法高亮", "code.jsonRawStyle": "JSON 显示方式", "code.jsonStyleOriginal": "原始", "code.jsonStylePretty": "格式化", "code.jsonStyleTree": "树形", "code.dataViewMode": "数据视图", "code.dataTreeLoading": "解析中…", "code.dataTreeParseError": "解析失败", "code.jsonStyleCompact": "压缩", "code.jsonFormatPretty2": "格式化(2 空格)", "code.jsonFormatPretty4": "格式化(4 空格)", "code.jsonFormatCompact": "压缩", "code.invalidJson": "JSON 无效,无法执行该操作", "code.compactApplied": "已压缩", "code.jsonValid": "JSON 有效", "code.jsonInvalid": "JSON 无效", "code.jsonExpandNode": "展开", "code.jsonCollapseNode": "折叠", "code.jsonTreeRoot": "根节点", "code.jsonTreeAria": "JSON 结构树", "code.jsonNodeKeys": "{count} 个键", "code.jsonNodeItems": "{count} 项", "code.jsonFoldLimited": "结构超过 {lines} 行,已关闭折叠", "code.lineNumbersUnavailable": "当前显示方式没有行号", "code.csvViewMode": "表格显示方式", "code.csvViewTable": "表格", "code.csvViewRaw": "原始文本", "code.csvDimensions": "{rows} 行 × {columns} 列", "code.jsonlViewMode": "JSON Lines 显示方式", "code.jsonlRecords": "{count} 条记录", "code.jsonlValid": "每一行都是有效 JSON", "code.jsonlInvalidSummary": "{count} 行不是有效 JSON,首个在第 {line} 行", "code.jsonlInvalidLine": "第 {line} 行不是有效 JSON:{text}", "code.jsonlInvalid": "不是有效 JSON", "code.jsonlLineLabel": "第 {line} 行", "code.jsonlLineColumn": "行", "code.jsonlValueColumn": "值", "code.jsonlTreeLimited": "仅显示前 {count} 行;编辑态保留完整原始文本。", "code.errorLinesOnly": "仅显示错误行", "code.errorLinesMatched": "仅显示错误行:{count} 行命中", "code.noErrorLines": "(无匹配的错误行)", "office.richPreviewReadOnly": "当前排版预览暂不支持编辑", "directory.sortBy": "排序方式", "directory.sortByName": "名称", "directory.sortByKind": "类型", "directory.sortDirection": "排序方向", "directory.sortAscending": "升序", "directory.sortDescending": "降序", "directory.viewMode": "视图", "directory.viewList": "列表", "directory.viewGrid": "网格", "directory.kindFolder": "文件夹", "create.inProgress": "正在新建 {fileName}...", "create.success": "已新建", "create.failed": "新建失败:{error}", "create.missingDirectory": "无法确定当前文件夹,不能新建文件。", "create.workspaceUnavailable": "当前环境未开放 chrome.tabLocalWorkspace,无法新建文件。", "create.directoryNotWritable": "当前文件夹未授权写入,无法新建文件。", "create.new": "新建", "create.newFileType": "新建文件类型", "outline.empty": "没有可展示的目录", "outline.aria": "文档大纲", "outline.title": "文档大纲", "outline.collapseAll": "全部折叠", "outline.expandAll": "全部展开", "outline.toggleAria": "折叠或展开子标题", "directory.aria": "文件夹树", "directory.backToRoot": "回到根目录", "directory.backToParent": ".. 上级目录", "directory.empty": "空文件夹", "directory.loading": "正在加载...", "directory.expandFailed": "无法展开文件夹", "directory.readFailed": "无法读取文件夹", "directory.previewDescription": "文件夹内的文件和子文件夹如下所示,也会同步显示在左侧目录树中。", "directory.previewItemCount": "包含条目", "directory.previewPath": "路径", "directory.resizeAria": "调整侧栏宽度", "directory.resizeTitle": "拖拽调整侧栏宽度", "directory.settings": "文件夹选项", "directory.showHiddenFiles": "显示隐藏项目", "directory.foldersFirst": "文件夹优先", "directory.rememberSidebarWidth": "记住侧栏宽度", "directory.openSettingsPage": "设置项", "directory.newFolder": "新建文件夹", "directory.newFolderName": "未命名文件夹", "directory.newFolderFailed": "新建文件夹失败:{error}", "directory.rename": "重命名", "directory.renameAria": "新名称", "directory.moved": "已移动到「{folder}」", "directory.moveFailed": "移动失败:{error}", "directory.moveExists": "「{folder}」中已有同名项目", "directory.extensionLocked": "这类文件不能修改扩展名", "directory.newFolderItem": "文件夹", "directory.favoritesMenu": "收藏的文件夹", "directory.favoritesEmpty": "还没有收藏的文件夹。在文件夹的「⋯」菜单里,或点文件头的星标即可收藏。", "trash.placeTrash": "废纸篓", "trash.placeRecycleBin": "回收站", "trash.action": "移到{place}", "trash.confirmTitle": "移到{place}?", "trash.confirmFile": "「{name}」将被移到{place},之后可以从{place}中恢复。", "trash.confirmFolder": "文件夹「{name}」及其中的全部内容将被移到{place},之后可以从{place}中恢复。", "trash.done": "已移到{place}", "trash.failed": "移到{place}失败:{error}", "favorites.add": "收藏", "favorites.remove": "取消收藏", "favorites.added": "已收藏", "favorites.removed": "已取消收藏", "popup.settings": "设置", "popup.favorites": "收藏", "popup.recents": "最近打开", "popup.favoritesEmpty": "在文件头点击星标,即可把文件或文件夹收藏到这里。", "popup.recentsEmpty": "最近打开的文件和文件夹会显示在这里。", "settings.title": "Previewer 设置", "settings.general": "通用", "settings.showHiddenFilesDescription": "在文件夹树与文件夹概览中显示以点开头的隐藏文件。", "settings.rememberSidebarWidthDescription": "下次打开文件夹时恢复上次调整的侧栏宽度。", "settings.documentTitle": "用文档标题命名标签页", "settings.documentTitleDescription": "预览 Markdown、MDX、HTML 时,用文档自己的标题(frontmatter 标题、一级标题、)命名标签页;文档没有标题时仍用文件名。", "settings.lineNumbers": "显示行号", "settings.lineNumbersDescription": "在代码与文本文件的预览和编辑器中显示行号。", "settings.wordWrap": "自动换行", "settings.wordWrapDescription": "超出宽度的长行自动折行显示,关闭后横向滚动查看。", "settings.syntax": "支持的 Markdown 语法", "settings.syntaxDescription": "Previewer 能识别的 Markdown 写法。基础部分遵循 GitHub Flavored Markdown,另外支持若干常见方言与两种图表围栏。", "settings.syntaxGroupBasic": "基础", "settings.syntaxGroupBlocks": "块级", "settings.syntaxGroupExtended": "扩展", "settings.syntaxHeading": "一到六级标题,自动生成大纲", "settings.syntaxEmphasis": "行内强调与代码", "settings.syntaxLinkImage": "链接与图片,相对路径按文档位置解析", "settings.syntaxList": "无序、有序与可勾选的任务列表", "settings.syntaxQuote": "引用块,可嵌套", "settings.syntaxRule": "水平分隔线", "settings.syntaxCodeFence": "围栏代码块,按语言高亮", "settings.syntaxTable": "表格,支持对齐冒号", "settings.syntaxDefinitionList": "定义列表", "settings.syntaxFootnote": "脚注引用与文末脚注区", "settings.syntaxAlert": "GFM 提示块(NOTE/TIP/IMPORTANT/WARNING/CAUTION)与 Obsidian Callout([!tip] 标题、+/- 折叠)", "settings.syntaxFrontmatter": "YAML frontmatter,可在顶部折叠面板中查看与编辑", "settings.syntaxHighlight": "高亮与插入标记", "settings.syntaxSubSup": "下标与上标", "settings.syntaxMath": "KaTeX 数学公式,也支持 \\( \\) 与 \\[ \\] 分隔符", "settings.syntaxMermaid": "Mermaid 图表;mindmap 语法渲染为脑图", "settings.syntaxSvg": "SVG,经白名单消毒后渲染", "settings.syntaxHtml": "静态展示类标签;脚本、事件属性与危险协议会被移除", "settings.shortcuts": "快捷键", "settings.shortcutsDescription": "编辑 Markdown 时可用的快捷键。按键随平台显示,macOS 上 Ctrl 显示为 ⌘。", "settings.shortcutGroupFormat": "行内格式", "settings.shortcutGroupBlocks": "块级格式", "settings.shortcutGroupDocument": "文档操作", "settings.fileTypes": "文件关联", "settings.fileTypesDescription": "Previewer 支持的文件类型。可查看每个类型当前的系统打开方式,将 Tabbit 设为系统默认,或选择在浏览器内使用 Previewer 还是浏览器自带方式打开。", "settings.currentHandler": "当前系统默认应用", "settings.currentHandlerSelf": "Tabbit(当前浏览器)", "settings.currentHandlerUnknown": "未检测到默认应用", "settings.detecting": "检测中...", "settings.detectUnavailable": "无法检测", "settings.systemDefault": "设为系统默认", "settings.systemDefaultActive": "已是系统默认打开方式", "settings.setAsDefault": "设为默认", "settings.setAllAsDefault": "全部设为默认", "settings.setAllNothingPending": "该分类目前没有可批量设为默认的项", "settings.unsetDefaultHint": "如需取消默认,请在系统设置中修改", "settings.openWith": "Tabbit 内打开方式", "settings.openWithPreviewer": "Tabbit Previewer", "settings.openWithNative": "浏览器自带", "settings.openWithNativeUnavailable": "该格式无法用浏览器自带方式正常显示", "settings.setDefaultRequested": "已请求将 Tabbit 设为 {extension} 的默认打开方式", "settings.openedSystemSettings": "已打开系统设置,请选择 Tabbit 作为 {extension} 的默认应用", "settings.setDefaultUnsupported": "当前系统暂不支持设置 {extension} 的默认应用", "settings.setDefaultFailed": "设置 {extension} 默认打开方式失败", "settings.type.markdown": "Markdown", "settings.type.svg": "SVG", "settings.type.image": "图片", "settings.type.media": "音频与视频", "settings.type.pdf": "PDF", "settings.type.office": "Office 文档", "settings.type.html": "HTML", "settings.type.codeText": "代码与文本", "settings.type.subtitle": "字幕", "settings.type.archive": "压缩包", "editor.sourceLabel": "文件内容", "markdown.contentLabel": "Markdown 内容", "code.contentLabel": "代码内容", "code.delimitedContentLabel": "表格原始文本", "code.tablePreviewAria": "{fileName} 表格预览", "code.unnamedColumn": "列 {index}", "code.tablePreviewLimited": "仅预览前 {rows} 行、前 {columns} 列;编辑态保留完整原始文本。", "code.noFormattingNeeded": "当前内容无需格式化", "code.formatApplied": "已应用格式化", "code.sourceReloaded": "{fileName} 源文件已重新读取", "code.formatJson": "格式化 JSON", "code.expandCode": "展开代码", "code.subtitleViewMode": "字幕视图", "code.subtitleTimeline": "时间轴", "code.subtitleSource": "源码", "code.subtitleInfoFormat": "格式", "code.subtitleInfoCues": "条数", "code.subtitleInfoDuration": "时长", "code.subtitleInfoLanguage": "语言", "code.subtitleInfoEncoding": "编码", "code.subtitleTimelineTruncated": "字幕过多,时间轴仅显示前 {shown} 条(共 {total} 条),完整内容请切换到源码视图", "code.subtitleSearchPlaceholder": "搜索台词,或输入 12:30 跳转", "code.subtitleCopyCue": "复制这条字幕", "code.subtitleTextView": "纯文本", "code.subtitleTextTruncated": "字幕过多,纯文本视图仅显示前 {shown} 条(共 {total} 条);「复制全文」仍包含全部内容", "code.subtitleBilingual": "双语", "code.subtitleEncodingTooltip": "检测到的文件编码", "code.subtitleTranscode": "转存为 UTF-8", "code.subtitleTranscoded": "已转存为 UTF-8,旧播放器不再乱码", "code.subtitleTranscodeFailed": "转存失败,请重试", "code.subtitleBinaryNotice": "这是二进制的位图字幕文件(VobSub 图形字幕),无法作为文本预览或编辑。请将它与同名 .idx 文件放在一起,在视频播放器中加载使用。", "code.subtitleExportMenu": "导出", "code.subtitleExportSrt": "SRT", "code.subtitleExportVtt": "VTT", "code.subtitleExportTxt": "纯文本 (TXT)", "code.subtitleExportUnparseable": "无法解析字幕内容,未导出", "code.subtitleExported": "已导出 {fileName}", "code.subtitleExportFailed": "导出失败,请重试", "code.searchPlaceholder": "查找", "code.searchPrev": "上一个", "code.searchNext": "下一个", "code.searchClose": "关闭", "code.replacePlaceholder": "替换为", "code.replaceOne": "替换", "code.replaceAll": "全部替换", "code.replacedCount": "已替换 {count} 处", "code.subtitleShiftMenu": "时轴平移", "code.subtitleShiftLabel": "偏移量(毫秒)", "code.subtitleShiftHint": "正值延后、负值提前,应用后直接保存。仅修正整体偏移,无法修复帧率漂移。", "code.subtitleShiftApply": "应用并保存", "code.subtitleShiftInvalid": "请输入非零的毫秒偏移量", "code.subtitleShifted": "已平移 {count} 条时间轴并保存", "code.subtitleShiftedClamped": "已平移并保存,部分时间被截止到 00:00", "code.subtitleShiftFailed": "平移保存失败,请重试", "code.subtitleShiftExported": "该格式不支持原位平移,已导出平移后的 {fileName}", "code.subtitleCleanMenu": "清理", "code.subtitleCleanTags": "去除样式标签", "code.subtitleCleanAnnotations": "去除括注说明(SDH)", "code.subtitleCleanMusic": "去除音乐符号行", "code.subtitleCleanDuplicates": "合并重复与空白条目", "code.subtitleCleaned": "已清理 {count} 处并保存", "code.subtitleCleanNothing": "没有需要清理的内容", "code.subtitleCleanFailed": "清理失败,请重试", "code.subtitleCleanExported": "该格式不支持原位清理,已导出清理后的 {fileName}", "code.subtitleTranslate": "翻译字幕并保存为新文件", "code.subtitleTranslateTitle": "翻译字幕", "code.subtitleTranslateModeLabel": "输出内容", "code.subtitleTranslateModeBilingual": "双语(译文在上)", "code.subtitleTranslateModeReplace": "仅译文", "code.subtitleTranslateLanguageLabel": "目标语言", "code.subtitleTranslateOutputLabel": "保存方式", "code.subtitleTranslateOutputNew": "另存为新文件", "code.subtitleTranslateOutputOverwrite": "覆盖当前文件", "code.subtitleTranslateOverwriteUnavailable": "仅本地 SRT 文件支持覆盖保存,当前文件将另存为新文件", "code.subtitleTranslateSummary": "将翻译 {count} 条字幕(约 {characters} 字符),已包含目标语言的条目会自动跳过。翻译会消耗账户用量。", "code.subtitleTranslateNothing": "没有需要翻译的条目(可能已包含目标语言)", "code.subtitleTranslateStart": "开始翻译", "code.subtitleTranslateCancel": "取消", "code.subtitleTranslateRunning": "正在翻译 {completed}/{total} 条…", "code.subtitleTranslateCancelled": "已取消,未修改文件", "code.subtitleTranslateUnavailable": "翻译服务不可用,请确认浏览器版本", "code.subtitleTranslateSourceChanged": "字幕内容已变化,请重新开始翻译", "code.subtitleTranslateFailed": "翻译失败:{error}", "code.subtitleTranslateDoneOverwrite": "已翻译 {count} 条并保存到当前文件", "code.subtitleTranslateDoneExported": "已翻译 {count} 条,导出为 {fileName}", "code.subtitleLangChinese": "中文", "code.subtitleLangJapanese": "日文", "code.subtitleLangKorean": "韩文", "code.subtitleLangLatin": "英文", "code.subtitleLangCyrillic": "西里尔文", "code.subtitleLangArabic": "阿拉伯文", "subtitle.binaryUnknown": "二进制 SUB", "subtitle.vobsubDescription": "检测到 MPEG Program Stream / DVD subpicture 数据;不会按文本解码或写回。", "subtitle.binaryDescription": "该 SUB 文件包含二进制数据,已停止文本解码以避免乱码或误写。", "subtitle.vobsubCompanionHint": "VobSub 画面、语言和时间索引通常由同名 .idx 文件描述;请同时打开配套 IDX 查看时间戳。", "subtitle.scannedBytes": "已检查头部", "subtitle.packHeaders": "MPEG pack 头", "subtitle.privateStreams": "字幕私有流包", "html.sourceLoading": "正在读取源码", "html.sourceEditorAria": "HTML source editor", "html.previewAria": "HTML preview", "html.previewFrameTitle": "{fileName} preview", "html.printFrameTitle": "{fileName} 打印预览", "html.preparing": "正在准备源码与预览", "html.viewMode": "HTML view mode", "html.modeSource": "源码", "html.modeSplit": "分屏", "html.modePreview": "预览", "html.reload": "重新读取", "html.sourceReloaded": "已重新读取源码", "html.sourceRead": "已读取 HTML 源码", "html.readFailed": "读取失败:无法读取 HTML 源文件", "html.readRequiresWorkspace": "读取失败:需要本地写回服务读取 HTML 源码", "html.previewUnavailable": "该文件可能已被移至别处、修改或删除。", "html.previewUnavailableInArchive": "压缩包内的 HTML 暂不支持预览。可在左侧查看源码,解压后打开即可正常预览。", "html.previewUnavailableInArchiveStatus": "暂不支持预览", "image.stageAria": "Image preview stage", "image.loadingTitle": "正在加载图片", "image.loadingMeta": "{extension} · 正在读取图片", "image.readyStatus": "可缩放、拖动、旋转和翻转", "image.zoomOut": "缩小", "image.zoomIn": "放大", "image.fit": "适应窗口", "image.actualSize": "100% 原始尺寸", "image.rotateLeft": "左转", "image.rotateRight": "右转", "image.flipHorizontal": "水平翻转", "image.flipVertical": "垂直翻转", "image.reset": "重置", "image.exportPng": "PNG", "image.loadFailedTitle": "图片加载失败", "image.loadFailedMeta": "{extension} · 加载失败", "image.loadFailedStatus": "无法读取当前图片,请检查链接是否仍然有效", "image.zoomedOut": "已缩小视图", "image.zoomedIn": "已放大视图,可拖动查看细节", "image.fitApplied": "已适应窗口", "image.actualSizeApplied": "已切换到 100% 原始尺寸", "image.resetApplied": "已重置变换并适应窗口", "image.wheelZoomed": "滚轮缩放视图,可拖动查看细节", "image.transformed": "有图片变换", "image.panned": "已移动视图", "image.notLoaded": "图片尚未加载完成", "image.exportedPng": "已导出 PNG", "image.exportFailed": "导出失败,浏览器可能限制了当前本地图片的画布读取", "media.kind.audio": "音频", "media.kind.video": "视频", "media.loading": "正在读取媒体信息", "media.playbackRate": "播放速度", "media.playbackRateValue": "播放速度:{rate}", "media.pictureInPicture": "画中画", "media.pictureInPictureActive": "已进入画中画模式", "media.pictureInPictureFailed": "无法进入画中画模式", "media.fullscreen": "全屏", "media.fullscreenActive": "已进入全屏模式", "media.fullscreenFailed": "无法进入全屏模式", "media.loadFailed": "媒体加载失败", "media.loadFailedDetail": "无法播放此媒体;容器或内部编码可能不受支持", "svg.stageAria": "SVG 预览区域,放大后可使用方向键移动视图", "pdf.loadingMeta": "PDF · 正在读取文件", "pdf.initialStatus": "使用 PDF.js 渲染,原始文件可作为浏览器兜底", "pdf.toggleNavigation": "显示或隐藏 PDF 导航", "pdf.navLabel": "导航", "pdf.hideNavigation": "隐藏 PDF 导航", "pdf.showNavigation": "显示 PDF 导航", "pdf.previousPage": "上一页", "pdf.nextPage": "下一页", "pdf.pageJumpTitle": "跳转到指定页码", "pdf.currentPage": "当前页码", "pdf.pageLabelTitle": "第 {page} 页 · 页码标签 {label}", "pdf.searchTitle": "搜索 PDF 文本", "pdf.searchPlaceholder": "搜索 PDF", "pdf.previousSearchResult": "上一个搜索结果", "pdf.nextSearchResult": "下一个搜索结果", "pdf.fitWidth": "适应宽度", "pdf.fitPage": "适应整页", "pdf.handTool": "拖拽平移", "pdf.handToolOn": "已开启拖拽平移:按住鼠标左键拖动页面", "pdf.handToolOff": "已关闭拖拽平移:拖动可选择文本", "pdf.rotateRight": "右转", "pdf.viewMode": "PDF 视图模式", "pdf.viewContinuous": "连续", "pdf.viewSingle": "单页", "pdf.viewTwoPage": "双页", "pdf.readAloud": "朗读当前页或所选文本", "pdf.readAloudLabel": "朗读", "pdf.rerender": "重新渲染", "pdf.navAria": "PDF 导航", "pdf.navTabsAria": "PDF 导航面板", "pdf.navTitle": "阅读导航", "pdf.tabPages": "页面", "pdf.tabOutline": "目录", "pdf.tabNotes": "备注", "pdf.infoLoading": "正在读取文档信息", "pdf.infoEmpty": "这个 PDF 没有可读取的文档信息", "pdf.propTitle": "标题", "pdf.propAuthor": "作者", "pdf.propSubject": "主题", "pdf.propKeywords": "关键词", "pdf.propCreator": "创建工具", "pdf.propProducer": "PDF 生成器", "pdf.propCreated": "创建时间", "pdf.propVersion": "PDF 版本", "pdf.infoAttachments": "附件", "pdf.attachmentSave": "另存附件 {name}", "pdf.attachmentSaving": "正在保存附件 {name}", "pdf.attachmentSaved": "已保存附件 {name}", "pdf.attachmentSaveFailed": "无法保存附件 {name}", "pdf.infoLayers": "图层", "pdf.layerUntitled": "未命名图层", "pdf.layerShown": "已显示图层「{name}」", "pdf.layerHidden": "已隐藏图层「{name}」", "pdf.infoSignature": "数字签名", "pdf.infoSignatureN": "数字签名 {index}", "pdf.metaSigned": "已签名", "pdf.signatureSigner": "签名人", "pdf.signatureTime": "签名时间", "pdf.signatureReason": "原因", "pdf.signatureLocation": "地点", "pdf.signatureContact": "联系方式", "pdf.signatureField": "签名域", "pdf.permission.print": "打印", "pdf.permission.copy": "复制文本", "pdf.permission.modify": "修改内容", "pdf.permission.annotate": "添加批注", "pdf.permission.forms": "填写表单", "pdf.permissionAllowed": "允许", "pdf.permissionRestricted": "作者已限制", "pdf.printRestricted": "这个 PDF 的作者限制了打印,打印结果可能受限", "pdf.copyRestricted": "这个 PDF 的作者限制了复制文本", "pdf.navLoading": "正在准备导航", "pdf.notePlaceholder": "为当前页记录备注", "pdf.noteHint": "备注只保存在这台设备,不会写入 PDF 原文件。", "pdf.noteAria": "当前 PDF 页面备注", "pdf.stageAria": "{fileName} PDF preview", "pdf.loadingTitle": "正在加载 PDF", "pdf.loadingDescription": "解析文件并准备页面渲染。", "pdf.zoomedOut": "已缩小视图", "pdf.zoomedIn": "已放大视图", "pdf.fitWidthApplied": "已适应宽度", "pdf.fitPageApplied": "已适应整页", "pdf.actualSizeApplied": "已切换到 100% 原始尺寸", "pdf.rotatedRight": "已右转 PDF 视图", "pdf.viewModeApplied": "已切换到 {mode} 视图", "pdf.rerenderingVisible": "正在重新渲染可见页面", "pdf.wheelZoomed": "滚轮缩放 PDF 视图", "pdf.readFailed": "无法读取 PDF 文件。可以尝试打开原始文件。", "pdf.parsing": "正在解析 PDF", "pdf.renderedPages": "PDF.js 渲染 · 共 {pages}", "pdf.pageAria": "第 {page} 页", "pdf.thumbnailTitle": "跳转到第 {page} 页", "pdf.pageHasNote": "这一页有备注", "pdf.waitingRender": "等待渲染", "pdf.rendering": "正在渲染", "pdf.renderFailed": "渲染失败", "pdf.noTranslatableText": "此页未检测到可翻译文本;扫描件需要 OCR 文字层。", "pdf.translationPageAria": "第 {page} 页翻译", "pdf.translatingPage": "正在翻译第 {page} 页…", "pdf.translationProgress": "已完成 {completed}/{total} 段", "pdf.translationFailed": "此页翻译失败", "pdf.translationRetry": "翻译失败,点击重试", "pdf.translationBudgetExceeded": "此页文字过多,已超过翻译上限(64 段或 48K 字符)。", "pdf.ocrTextTooLarge": "此页识别文字超过 64KiB,无法安全翻译。", "pdf.readingProgress": "正在读取 PDF · {percent}%", "pdf.searching": "正在搜索 PDF 文本", "pdf.searchFound": "找到 {count} 个匹配项", "pdf.searchNoResults": "没有找到匹配项", "pdf.searchCleared": "已清除搜索", "pdf.notePage": "第 {page} 页备注", "pdf.noteUnsaved": "备注未保存", "pdf.noteSaved": "备注已保存到本机", "pdf.marksTitle": "本页标注", "pdf.marksHint": "选中正文后可高亮或下划线。标注只保存在本机,不写入 PDF。", "pdf.marksEmpty": "这一页还没有标注", "pdf.markHighlight": "高亮", "pdf.markUnderline": "下划线", "pdf.markCopy": "复制", "pdf.toolHighlight": "高亮", "pdf.toolInk": "画笔", "pdf.toolFreeText": "文字", "pdf.toolStamp": "图片", "pdf.editUnavailable": "当前 PDF.js 版本不支持标注编辑", "pdf.editRestricted": "文档作者已限制添加批注", "pdf.exportAnnotated": "导出一份带标注的副本,原文件不变", "pdf.exportAnnotatedLabel": "带标注的 PDF 副本", "pdf.exportAnnotatedNeedsEdit": "进入编辑模式并添加标注后,可导出带标注的副本", "pdf.exportAnnotatedDone": "已导出 {name}", "pdf.exportAnnotatedFailed": "导出带标注副本失败", "pdf.annotatedCopySuffix": "标注", "pdf.saveAnnotations": "把标注写入原 PDF 文件", "pdf.saveAnnotationsDone": "标注已写入原文件", "pdf.saveAnnotationsFailed": "标注写入原文件失败", "pdf.saveAnnotationsConfirmTitle": "写入原 PDF 文件?", "pdf.saveAnnotationsConfirmDescription": "将把本次编辑的标注写入「{fileName}」本体,覆盖磁盘上的原文件。预览模式的高亮与下划线只存在本机,不会一起写入。", "pdf.saveAnnotationsConfirm": "写入原文件", "pdf.annotationsEmpty": "还没有可保存的标注", "pdf.markAdded": "已添加{kind},只保存在本机", "pdf.markRemoved": "已删除标注", "pdf.markDelete": "删除标注", "pdf.markJump": "定位到这条{kind}", "pdf.copied": "已复制所选文本", "pdf.copyFailed": "复制失败", "pdf.selectionToolbarAria": "所选文本操作", "pdf.outlineLoading": "正在读取目录", "pdf.outlineUnavailable": "当前 PDF 没有可读取目录", "pdf.outlineEmpty": "这个 PDF 没有目录", "pdf.outlineUntitled": "未命名目录项", "pdf.outlineGoTo": "跳转到第 {page} 页", "pdf.outlineExternal": "打开外部链接", "pdf.linkInternal": "跳转到文档内的链接目标", "pdf.linkUnresolved": "这个链接的目标页面无法定位", "pdf.annotationAria": "批注:{summary}", "pdf.readAloudStopped": "已停止朗读", "pdf.readAloudNoText": "当前页没有可朗读文本", "pdf.readAloudFinished": "朗读结束", "pdf.readAloudFailed": "朗读失败", "pdf.readAloudSelection": "正在朗读所选文本", "pdf.readAloudPage": "正在朗读第 {page} 页", "pdf.previewFailedTitle": "PDF 预览失败", "pdf.passwordRequired": "这个 PDF 需要密码,当前预览暂不处理加密 PDF。", "pdf.renderErrorDetail": "PDF.js 无法渲染此文件:{detail}", "pdf.renderError": "PDF.js 无法渲染此文件。", "pdf.canvasUnavailable": "当前浏览器无法创建 PDF canvas。", "office.loadingTitle": "正在读取文件", "office.loadingDescription": "会尽量提取可预览的内容。", "office.kindStatus": "{kind} 文档", "office.parsing": "正在解析文档内容", "office.readFailed": "无法读取当前本地文件。", "office.unsupportedExtension": "暂不支持该 Office 扩展名。", "office.parseFailed": "文档解析失败,可能是加密文件、损坏文件或浏览器不支持 ZIP 解压。", "office.noDocxBody": "没有找到 DOCX 正文内容。", "office.docxMeta": "DOCX · {paragraphs} · 只读文本预览,复杂样式以原文件为准", "office.docxBodyLabel": "DOCX 正文", "office.docxReady": "已读取 DOCX,当前为只读文本预览", "office.docxRendering": "正在渲染 DOCX 预览", "office.docxRendered": "已渲染 DOCX 预览", "office.docxFitWidth": "适应宽度", "office.docxZoomedOut": "已缩小 DOCX 预览", "office.docxZoomedIn": "已放大 DOCX 预览", "office.docxFitWidthApplied": "已适应 DOCX 页面宽度", "office.docxActualSizeApplied": "已切换到 DOCX 100% 原始尺寸", "office.noXlsxSheets": "没有找到 XLSX 工作表。", "office.sheet": "工作表", "office.sheetCount": "共 {count}", "office.currentSheetTsv": "当前工作表 TSV", "office.sheetSwitched": "已切换工作表", "office.noPptxSlides": "没有找到 PPTX 幻灯片。", "office.pptxMissingViewer": "PptxViewJS 未加载,已切换到文本大纲预览", "office.pptxRendering": "正在使用 PptxViewJS 渲染 PPTX", "office.pptxReady": "已读取 PPTX,当前为只读文本大纲预览", "office.pptxTextLabel": "PPTX 幻灯片文本", "office.pptxOutlineTitle": "文本大纲", "office.pptxOutlineDescription": "查看幻灯片文本", "office.pptxBackToSlides": "返回幻灯片预览", "office.pptxViewerLoading": "正在加载 {count}", "office.pptxPreviousPage": "上一页", "office.pptxNextPage": "下一页", "office.pptxZoomOut": "缩小", "office.pptxZoomIn": "放大", "office.pptxFit": "适应", "office.pptxViewerMountFailed": "PPTX 预览器挂载失败,已切换到文本大纲预览", "office.pptxRenderingPage": "正在渲染第 {page} 页", "office.pptxViewerReady": "PptxViewJS 预览", "office.pptxViewerReadyStatus": "已使用 PptxViewJS 渲染 PPTX", "office.pptxRenderFailed": "PptxViewJS 渲染失败,已切换到文本大纲预览", "office.legacyUnsupportedStatus": "旧版 Office 二进制格式暂不支持浏览器内解析", "office.legacyUnsupportedTitle": "{extension} 暂不支持预览", "office.legacyUnsupportedDescription": "轻量方案先支持 .docx / .xlsx / .pptx。旧版格式建议另存为新版 OOXML 后再打开。", "office.parseErrorTitle": "无法解析文档", "office.emptyDocxTitle": "没有提取到正文", "office.emptyDocxDescription": "该文档可能主要由图片、图表或受保护内容组成。", "office.emptyXlsxTitle": "没有可展示的表格内容", "office.emptyXlsxDescription": "可以切换到编辑态填写 TSV 内容后保存。", "office.chart": "图表", "office.chartSeries": "系列 {index}", "office.chartUnresolved": "图表结构已识别,但引用的数据范围暂时无法解析。", "office.embeddedChart": "嵌入图表", "office.emptyPptxTitle": "没有提取到幻灯片文本", "office.emptyPptxDescription": "该演示文稿可能主要由图片或图形组成。", "office.blankSlide": "空白幻灯片", "office.xlsxMeta": "XLSX · {sheetName} · {sheetCount} · 保存当前显示值,复杂公式/样式以轻量兼容为准", "office.pptxMeta": "PPTX · {slides} · {images} · 轻量编辑会保存文本,图片/版式保留原文件结构", "unsupported.displayName": "不支持的文件", "unsupported.description": "当前文件类型无法直接预览,可以在文件管理器中定位。", "unsupported.type": "类型", "unsupported.path": "路径", "unsupported.size": "大小", "unsupported.created": "创建时间", "unsupported.modified": "修改时间", "unsupported.reading": "正在读取...", "unsupported.revealRequested": "已请求文件管理器定位", "unsupported.revealFailed": "无法定位文件", "unsupported.unreadable": "无法读取", "zip.displayName": "压缩包", "zip.kicker": "Archive", "zip.folderKicker": "Archive folder", "zip.entryKicker": "Archive entry", "zip.loading": "正在读取压缩包...", "zip.ready": "已读取压缩包索引", "zip.refresh": "从磁盘重新读取", "zip.description": "在左侧目录树中展开即可浏览压缩包内的文件,无需解压。", "zip.emptyArchive": "这个压缩包里没有文件。", "zip.entries": "文件条目", "zip.childEntries": "当前层级", "zip.uncompressedSize": "原始大小", "zip.compressedSize": "压缩后大小", "zip.ratio": "压缩率", "zip.archiveSize": "压缩包大小", "zip.archive": "所属压缩包", "zip.entryPath": "包内路径", "zip.readOnly": "压缩包内的文件是只读的,无法修改或重命名。", "zip.readFailed": "无法读取这个压缩包。", "zip.tooLarge": "压缩包超过 {size} 的预览上限,未读取内容。", "zip.corrupt": "文件已损坏,或者不是有效的 ZIP 压缩包。", "zip.skippedUnsafe": "已跳过 {count} 个路径越界的条目。", "zip.truncated": "条目过多,仅索引前 {count} 项。", "zip.encryptedEntries": "其中 {count} 个条目已加密,无法预览。", "zip.entryEncrypted": "这个条目已加密,需要密码才能解压,无法预览。", "zip.entryUnsupportedMethod": "这个条目使用了浏览器不支持的压缩算法,无法预览。", "zip.entryTooLarge": "这个条目解压后超过单条目预览上限,未展开。", "zip.entryRatio": "这个条目的压缩比异常,出于安全考虑没有解压。", "zip.entryCorrupt": "这个条目已损坏,无法解压。", "zip.entryNotFound": "压缩包中找不到这个条目。", "fileType.directory": "文件夹", "fileType.markdown": "Markdown", "fileType.svg": "SVG", "fileType.image": "图片", "fileType.pdf": "PDF", "fileType.office": "Office", "fileType.html": "HTML", "fileType.code-text": "代码 / 文本", "fileType.unsupported": "不支持的文件" }, en: { "app.description": "Local file rendering", "viewer.openingTitle": "Opening file", "viewer.openingDescription": "Previewer is preparing the file preview.", "common.fileName": "File Name", "common.fileSize": "Size", "common.fileCreated": "Created", "common.fileModified": "Modified", "common.today": "Today", "common.yesterday": "Yesterday", "common.filePath": "File Path", "common.pathEllipsis": "...", "common.showCollapsedPath": "Show collapsed middle path", "common.collapsedPathSegments": "Collapsed middle path", "common.fileActions": "File Actions", "common.fileInfo": "File Info", "common.moreActions": "More Actions", "common.renameFile": "Rename File", "common.expandExplorer": "Expand Explorer", "common.collapseExplorer": "Collapse Explorer", "common.expandHeader": "Expand File Header", "common.collapseHeader": "Collapse File Header", "common.rawFile": "Raw File", "common.openInNewTab": "Open in New Tab", "common.save": "Save", "common.saveAs": "Save As...", "common.downloadRemoteFile": "Download to Local Device", "common.print": "Print", "print.preparing": "Preparing content for printing…", "print.failed": "Unable to prepare content for printing", "print.notReady": "The content is not ready yet. Please try again shortly.", "print.pageAlt": "Print page {page}", "print.namedPageAlt": "{fileName}, page {page}", "print.preparingPage": "Preparing page {page} of {total}…", "print.preparingSlide": "Preparing slide {slide} of {total}…", "print.namedSlideAlt": "{fileName}, slide {slide}", "common.edit": "Edit", "common.preview": "Preview", "common.copy": "Copy", "common.copyAllText": "Copy All", "common.export": "Export", "common.revealInFileManager": "Show in File Manager", "common.revealInFinder": "Show in Finder", "common.revealInExplorer": "Show in Explorer", "common.revealUnavailableRemote": "Online pages cannot be shown", "common.editUnavailableRemote": "Online pages cannot be edited", "common.fileUnavailable": "This file may have been moved, changed, or deleted.", "common.unknown": "Unknown", "common.unknownError": "Unknown error", "common.untitled": "Untitled", "common.localFile": "Local file", "common.loading": "Loading...", "common.notModified": "Not modified", "common.unsavedChanges": "Unsaved changes", "common.unexportedChanges": "Unexported changes", "common.copiedToClipboard": "Copied to clipboard", "common.copiedSelectedText": "Copied selected text", "common.copyFailed": "Copy failed", "common.saving": "Saving...", "common.exporting": "Exporting...", "common.savingAs": "Saving as...", "common.downloadingRemoteFile": "Downloading...", "common.savedToOriginal": "Saved to original file", "common.saved": "Saved", "common.savedAs": "Saved as", "common.downloadedRemoteFile": "Downloaded to local device", "common.exportedToDownloads": "Exported to Downloads", "common.cancelledExport": "Export canceled", "common.cancelledSaveAs": "Save as canceled", "common.cancelledDownload": "Download canceled", "common.cancel": "Cancel", "common.saveFailedWorkspace": "Save failed: unable to write the current workspace file", "common.exportFailed": "Export failed", "common.saveAsFailed": "Save as failed", "common.downloadRemoteFailed": "Download failed: unable to read the online file", "common.downloadUnavailable": "This file cannot be downloaded", "common.fileContent": "File content", "common.textFile": "Text file", "common.filePickerLocalFile": "Local file", "editor.clean": "Synced", "editor.external": "External edit", "editor.externalHint": "Another program is editing this file; its changes are synced here automatically", "changes.unreadAbove": "{count} unread above", "changes.unreadBelow": "{count} unread below", "changes.markAllRead": "Mark all as read", "changes.jump": "Jump to this change", "changes.added": "Added externally", "changes.modified": "Modified externally", "changes.deleted": "Lines removed externally", "editor.readonly": "Read only", "editor.dirty": "Unsaved", "editor.saving": "Saving", "editor.saved": "Saved", "editor.autoSavePending": "Autosaving", "editor.autoSaved": "Autosaved", "editor.savedAsUtf8": "The file was {encoding}-encoded and cannot hold this content; it was saved as UTF-8", "editor.error": "Save failed", "editor.conflict": "File conflict", "editor.draftRestored": "Recovered unsaved draft", "editor.draftOutOfSpace": "Browser storage is full, so this document has no recovery draft. Nothing was cut from your text — save it to the file soon.", "editor.unsavedTitle": "Save changes?", "editor.unsavedDescription": "“{fileName}” has unsaved changes.", "editor.saveBeforeLeave": "Save changes to “{fileName}” before leaving?", "editor.discardBeforeLeave": "Discard unsaved changes to “{fileName}”?", "editor.discard": "Discard changes", "editor.beforeUnload": "The current file has unsaved changes.", "editor.revertTitle": "Revert to the opened content?", "editor.revertDescription": "This discards every change made to “{fileName}” since it was opened, including changes already autosaved to disk.", "editor.revertConfirm": "Revert", "editor.conflictTitle": "File changed elsewhere", "editor.conflictDescription": "The copy of “{fileName}” on disk has changed. To avoid silently overwriting someone else's work, saving can continue only after explicit confirmation. You can also cancel and use Save As.", "editor.overwrite": "Overwrite anyway", "markdown.bold": "Bold", "markdown.italic": "Italic", "markdown.insertLink": "Insert Link", "markdown.insertImage": "Insert Image", "markdown.widthDecrease": "Decrease Content Width", "markdown.contentWidth": "Content Width", "markdown.widthIncrease": "Increase Content Width", "markdown.taskToggle": "Toggle task status", "markdown.taskReadonlyRemote": "Online Markdown is read-only. Download it locally to edit tasks.", "large.previewTitle": "Large-file safety preview", "large.previewDescription": "Loaded the first {previewSize}. More content loads on demand as you scroll; editing and saving are disabled.", "large.previewAria": "{kind} large-file preview fragment", "large.previewMeta": "{kind} · large-file preview · {size}", "large.blockedTitle": "File too large for automatic preview", "large.blockedDescription": "This {kind} file is {size}. Previewer did not read the full content to avoid crashing the browser. Open the raw file or reveal it in the file manager instead.", "large.blockedMeta": "{kind} · file too large · {size}", "large.emptyPreview": "Unable to read a safe preview fragment.", "large.virtualStatus": "Showing {start} - {end} / {total}", "large.virtualLoading": "Loading the next chunk...", "large.virtualError": "Loading failed. Scroll again to retry.", "large.directoryLimited": "Showing only the first {count} items. Very large folders are not rendered all at once.", "toolbar.reveal": "Show in file manager", "theme.followingBrowser": "Following browser {theme} theme. Click to temporarily pin {nextTheme}.", "theme.fixed": "{theme} theme pinned. Click to follow the browser again.", "theme.dark": "dark", "theme.light": "light", "rename.inProgress": "Renaming...", "rename.success": "Renamed", "rename.failed": "Rename failed: {error}", "rename.invalidName": "File name contains invalid characters", "rename.duplicateName": "A file with this name already exists in the current folder", "rename.extensionNone": "no extension", "rename.extensionChangeTitle": "Confirm extension change", "rename.extensionChangeDescription": "The extension of \"{fileName}\" is about to change to \"{extension}\". This changes how the file opens. Rename anyway?", "rename.extensionChangeConfirm": "Rename", "common.panelMode": "Preview / edit mode", "common.previewModeHint": "Preview mode (⌘E to toggle)", "common.editModeHint": "Edit mode (⌘E to toggle)", "common.editUnsupportedType": "This format does not support editing", "common.copyAbsolutePath": "Copy Absolute Path", "common.copiedPath": "Absolute path copied", "common.duplicateFile": "Create a Copy", "common.reloadFromDisk": "Reload from Disk", "common.resizePanes": "Drag to resize the panes (double-click to reset)", "common.reloadedFromDisk": "Reloaded from disk", "common.reloadFailed": "Failed to read the file", "common.revert": "Revert", "common.revertToOpened": "Revert to the content the file was opened with", "common.revertUnavailable": "Already matches the content the file was opened with", "common.reverted": "Reverted to the content the file was opened with", "common.saveNothingToSave": "No changes to save", "common.saveHandledByAutoSave": "Autosave is handling this file", "common.moreTools": "More Tools", "common.undo": "Undo", "common.quoteFileToChat": "Reference file in Chat", "common.undoChatEdit": "Undo AI edit", "common.chatEditConflict": "The content has changed; this edit cannot be safely undone.", "common.redo": "Redo", "common.fit": "Fit", "common.zoomPercent": "Zoom level, type a percentage", "duplicate.readFailed": "Failed to read the file; cannot create a copy", "duplicate.failed": "Failed to create a copy: {error}", "duplicate.success": "Created copy {fileName}", "duplicate.namePattern": "{name} (copy)", "duplicate.namePatternIndexed": "{name} (copy {index})", "document.translate": "Translate this document into a new file or over the original", "document.translateShort": "Translation…", "document.translateTitle": "Export Translation", "document.translateModeLabel": "Output", "document.translateModeTranslationFirst": "Bilingual (translation first)", "document.translateModeOriginalFirst": "Bilingual (original first)", "document.translateModeReplace": "Translation only", "document.translateLanguageLabel": "Target language", "document.translateOutputLabel": "Save as", "document.translateOutputNew": "Export a new file", "document.translateOutputOverwrite": "Overwrite this file", "document.translateSummaryExport": "Translates {count} blocks and exports {fileName} to your downloads. Code blocks, tables and link targets stay as they are. Translation uses your account quota.", "document.translateSummaryOverwrite": "Translates {count} blocks and overwrites this file. Code blocks, tables and link targets stay as they are. Translation uses your account quota.", "document.translateNothing": "There is no prose to translate", "document.translateUnavailable": "Translation is unavailable; check that the translation extension is enabled", "document.translateStart": "Translate", "document.translateCancel": "Cancel", "document.translateRunning": "Translating {completed}/{total}", "document.translateCancelled": "Translation cancelled", "document.translateFailed": "Translation failed: {error}", "document.translateSourceChanged": "The document changed while translating; the overwrite was abandoned", "document.translateDoneOverwrite": "Translated {count} blocks and saved", "document.translateDoneExported": "Translated {count} blocks and exported {fileName} to your downloads", "document.translateKeptOriginal": "; {count} blocks could not be translated and keep their original text", "document.translateAllRejected": "the translation would have broken the document's syntax, so nothing was written", "markdown.toc": "TOC", "markdown.tocResizeAria": "Resize table of contents width", "markdown.tocResizeTitle": "Drag to resize the table of contents (double-click to reset)", "markdown.readingWidth": "Reading Width", "markdown.widthFixed": "Fixed Width", "markdown.widthFull": "Full Width", "markdown.fontSize": "Font Size", "markdown.fontDecrease": "Decrease Font Size", "markdown.fontIncrease": "Increase Font Size", "markdown.fontMedium": "M", "markdown.fontLarge": "L", "markdown.fontXLarge": "XL", "markdown.heading1": "Heading 1", "markdown.heading2": "Heading 2", "markdown.heading3": "Heading 3", "markdown.heading4": "Heading 4", "markdown.heading5": "Heading 5", "markdown.strikethrough": "Strikethrough", "markdown.inlineCode": "Inline Code", "markdown.unorderedList": "Bulleted List", "markdown.orderedList": "Numbered List", "markdown.taskList": "Task List", "markdown.blockquote": "Blockquote", "markdown.codeBlock": "Code Block", "markdown.insertTable": "Insert Table", "markdown.horizontalRule": "Horizontal Rule", "markdown.clearFormat": "Clear Formatting", "common.shortcutHint": "{label} ({shortcut})", "markdown.tableHeader": "Column", "markdown.tableCell": "Cell", "markdown.editModeSwitch": "Editing Mode", "markdown.editSource": "Source", "markdown.editSplit": "Split", "markdown.editInline": "Inline", "markdown.inlineLoading": "Loading the inline editor...", "markdown.codeCopied": "Copied", "markdown.statLines": "Lines", "markdown.statWords": "Words", "markdown.exportHtml": "HTML", "markdown.exportHtmlDone": "Exported HTML", "markdown.exportHtmlFailed": "HTML export failed", "markdown.exportPdf": "PDF", "markdown.exportPdfWorking": "Generating PDF…", "markdown.exportPdfDone": "Exported PDF", "markdown.exportPdfFailed": "PDF export failed", "markdown.exportImage": "Long image (PNG)", "markdown.exportImageWorking": "Generating long image…", "markdown.exportImageDone": "Exported long image", "markdown.exportImageFailed": "Long image export failed", "markdown.exportImageTooTall": "The document is too long for a single image", "markdown.exportUnavailable": "This export is not available in the current environment", "markdown.inlineLoadFailed": "Failed to load the inline editor; switched back to the source view", "inline.placeholder": "Start writing...", "inline.codeSearchLanguage": "Search language", "inline.codeNoResult": "No result", "inline.codePreview": "Preview", "inline.codePreviewLoading": "Rendering diagram...", "inline.codeHideCode": "Hide code", "inline.linkPlaceholder": "Paste link...", "inline.imageUrlPlaceholder": "Paste image URL...", "markdown.searchPlaceholder": "Search in document", "markdown.searchPrev": "Previous match", "markdown.searchNext": "Next match", "markdown.searchClose": "Close search", "markdown.imagePasteFailed": "Failed to Save Image", "markdown.formatTables": "Format Tables", "markdown.frontmatter": "Frontmatter", "markdown.frontmatterKey": "Property name", "markdown.frontmatterValue": "Value", "markdown.frontmatterEmptyValue": "No value", "markdown.frontmatterAdd": "Add a property", "markdown.frontmatterRemove": "Remove this property", "markdown.frontmatterNewKey": "new-property", "markdown.largeSourceOnly": "Large File: Source Editing Only", "markdown.splitTooNarrow": "Not enough room for two panes", "markdown.footnotes": "Footnotes", "markdown.footnoteBackref": "Back to Reference", "markdown.alertNote": "Note", "markdown.alertTip": "Tip", "markdown.alertImportant": "Important", "markdown.alertWarning": "Warning", "markdown.alertCaution": "Caution", "markdown.alertAbstract": "Abstract", "markdown.alertInfo": "Info", "markdown.alertTodo": "Todo", "markdown.alertSuccess": "Success", "markdown.alertQuestion": "Question", "markdown.alertFailure": "Failure", "markdown.alertDanger": "Danger", "markdown.alertBug": "Bug", "markdown.alertExample": "Example", "markdown.alertQuote": "Quote", "markdown.searchToggleReplace": "Toggle Replace", "markdown.searchCaseSensitive": "Match Case", "markdown.searchReplacePlaceholder": "Replace with", "markdown.searchReplaceOne": "Replace", "markdown.searchReplaceAll": "Replace All", "svg.sourceEditorAria": "SVG source editor", "code.lineNumbers": "Line Numbers", "code.wordWrap": "Word Wrap", "code.highlightLanguage": "Syntax Highlighting", "code.jsonRawStyle": "JSON display style", "code.jsonStyleOriginal": "Original", "code.jsonStylePretty": "Pretty", "code.jsonStyleTree": "Tree", "code.dataViewMode": "Data View", "code.dataTreeLoading": "Parsing…", "code.dataTreeParseError": "Parse Failed", "code.jsonStyleCompact": "Compact", "code.jsonFormatPretty2": "Pretty Format (2 Spaces)", "code.jsonFormatPretty4": "Pretty Format (4 Spaces)", "code.jsonFormatCompact": "Compact Format", "code.invalidJson": "Invalid JSON; cannot run this action", "code.compactApplied": "Compacted", "code.jsonValid": "Valid JSON", "code.jsonInvalid": "Invalid JSON", "code.jsonExpandNode": "Expand", "code.jsonCollapseNode": "Collapse", "code.jsonTreeRoot": "root", "code.jsonTreeAria": "JSON structure tree", "code.jsonNodeKeys": "{count} keys", "code.jsonNodeItems": "{count} items", "code.jsonFoldLimited": "Folding is off above {lines} lines", "code.lineNumbersUnavailable": "This display style has no line numbers", "code.csvViewMode": "Table display mode", "code.csvViewTable": "Table", "code.csvViewRaw": "Raw Text", "code.csvDimensions": "{rows} rows × {columns} columns", "code.jsonlViewMode": "JSON Lines display mode", "code.jsonlRecords": "{count} records", "code.jsonlValid": "Every line is valid JSON", "code.jsonlInvalidSummary": "Lines that are not valid JSON: {count}, first on line {line}", "code.jsonlInvalidLine": "Line {line} is not valid JSON: {text}", "code.jsonlInvalid": "not valid JSON", "code.jsonlLineLabel": "Line {line}", "code.jsonlLineColumn": "Line", "code.jsonlValueColumn": "Value", "code.jsonlTreeLimited": "Showing only the first {count} lines; edit mode keeps the complete source text.", "code.errorLinesOnly": "Error Lines Only", "code.errorLinesMatched": "Error lines only: {count} matched", "code.noErrorLines": "(no matching error lines)", "office.richPreviewReadOnly": "The rich preview does not support editing yet", "directory.sortBy": "Sort By", "directory.sortByName": "Name", "directory.sortByKind": "Type", "directory.sortDirection": "Sort Direction", "directory.sortAscending": "Ascending", "directory.sortDescending": "Descending", "directory.viewMode": "View", "directory.viewList": "List", "directory.viewGrid": "Grid", "directory.kindFolder": "Folder", "create.inProgress": "Creating {fileName}...", "create.success": "Created", "create.failed": "Create failed: {error}", "create.missingDirectory": "Cannot determine the current folder, so a file cannot be created.", "create.workspaceUnavailable": "chrome.tabLocalWorkspace is not available, so a file cannot be created.", "create.directoryNotWritable": "The current folder is not writable, so a file cannot be created.", "create.new": "New", "create.newFileType": "New File Type", "outline.empty": "No TOC available", "outline.aria": "Table of Contents", "outline.title": "Table of Contents", "outline.collapseAll": "Collapse all", "outline.expandAll": "Expand all", "outline.toggleAria": "Collapse or expand subsections", "directory.aria": "Folder tree", "directory.backToRoot": "Back to root folder", "directory.backToParent": ".. Parent directory", "directory.empty": "Empty folder", "directory.loading": "Loading...", "directory.expandFailed": "Unable to expand folder", "directory.readFailed": "Unable to read folder", "directory.previewDescription": "Files and subfolders are listed below and mirrored in the sidebar tree.", "directory.previewItemCount": "Items", "directory.previewPath": "Path", "directory.resizeAria": "Resize sidebar width", "directory.resizeTitle": "Drag to resize sidebar width", "directory.settings": "Folder Options", "directory.showHiddenFiles": "Show Hidden Items", "directory.foldersFirst": "Folders First", "directory.rememberSidebarWidth": "Remember Sidebar Width", "directory.openSettingsPage": "Settings", "directory.newFolder": "New Folder", "directory.newFolderName": "untitled folder", "directory.newFolderFailed": "Couldn't create the folder: {error}", "directory.rename": "Rename", "directory.renameAria": "New name", "directory.moved": "Moved to “{folder}”", "directory.moveFailed": "Couldn't move: {error}", "directory.moveExists": "“{folder}” already has an item with this name", "directory.extensionLocked": "This kind of file can't change its extension", "directory.newFolderItem": "Folder", "directory.favoritesMenu": "Favorite Folders", "directory.favoritesEmpty": "No favorite folders yet. Use a folder's ⋯ menu, or the star in the file header, to add one.", "trash.placeTrash": "Trash", "trash.placeRecycleBin": "Recycle Bin", "trash.action": "Move to {place}", "trash.confirmTitle": "Move to {place}?", "trash.confirmFile": "“{name}” will be moved to the {place}. You can restore it from there.", "trash.confirmFolder": "The folder “{name}” and everything in it will be moved to the {place}. You can restore it from there.", "trash.done": "Moved to the {place}", "trash.failed": "Couldn't move to the {place}: {error}", "favorites.add": "Add to Favorites", "favorites.remove": "Remove from Favorites", "favorites.added": "Added to Favorites", "favorites.removed": "Removed from Favorites", "popup.settings": "Settings", "popup.favorites": "Favorites", "popup.recents": "Recent", "popup.favoritesEmpty": "Star a file or folder from its header to keep it here.", "popup.recentsEmpty": "Files and folders you open will show up here.", "settings.title": "Previewer Settings", "settings.general": "General", "settings.showHiddenFilesDescription": "Show dot-prefixed hidden files in the folder tree and folder overview.", "settings.rememberSidebarWidthDescription": "Restore the last sidebar width the next time a folder opens.", "settings.documentTitle": "Name tabs after the document's own title", "settings.documentTitleDescription": "Title tabs from a Markdown, MDX or HTML document's own title (frontmatter title, first heading, <title>), falling back to the file name when it has none.", "settings.lineNumbers": "Show line numbers", "settings.lineNumbersDescription": "Show the line-number gutter in code and text previews and editors.", "settings.wordWrap": "Word wrap", "settings.wordWrapDescription": "Wrap long lines to the viewport; when off, scroll horizontally instead.", "settings.syntax": "Supported Markdown", "settings.syntaxDescription": "The Markdown this build understands. The basics follow GitHub Flavored Markdown, plus a few common dialects and two diagram fences.", "settings.syntaxGroupBasic": "Basics", "settings.syntaxGroupBlocks": "Blocks", "settings.syntaxGroupExtended": "Extended", "settings.syntaxHeading": "Headings one through six; the outline is built from them", "settings.syntaxEmphasis": "Inline emphasis and code", "settings.syntaxLinkImage": "Links and images; relative paths resolve against the document", "settings.syntaxList": "Bulleted, numbered and checkable task lists", "settings.syntaxQuote": "Blockquotes, nestable", "settings.syntaxRule": "Horizontal rule", "settings.syntaxCodeFence": "Fenced code blocks, highlighted by language", "settings.syntaxTable": "Tables, including alignment colons", "settings.syntaxDefinitionList": "Definition lists", "settings.syntaxFootnote": "Footnote references and a footnote section", "settings.syntaxAlert": "GFM alerts (NOTE/TIP/IMPORTANT/WARNING/CAUTION) and Obsidian callouts ([!tip] Title, +/- folding)", "settings.syntaxFrontmatter": "YAML front matter, viewable in a panel at the top", "settings.syntaxHighlight": "Highlight and insert marks", "settings.syntaxSubSup": "Subscript and superscript", "settings.syntaxMath": "KaTeX math; \\( \\) and \\[ \\] delimiters also work", "settings.syntaxMermaid": "Mermaid diagrams; mindmap syntax renders as a mind map", "settings.syntaxSvg": "SVG, rendered after whitelist sanitising", "settings.syntaxHtml": "Static display tags; scripts, event attributes and unsafe protocols are removed", "settings.shortcuts": "Shortcuts", "settings.shortcutsDescription": "Keyboard shortcuts available while editing Markdown. Keys follow your platform; Ctrl appears as ⌘ on macOS.", "settings.shortcutGroupFormat": "Inline formatting", "settings.shortcutGroupBlocks": "Block formatting", "settings.shortcutGroupDocument": "Document actions", "settings.fileTypes": "File Associations", "settings.fileTypesDescription": "File types supported by Previewer. Check each type's current system handler, make Tabbit the system default, or choose whether the browser opens it with Previewer or its built-in viewer.", "settings.currentHandler": "Current System Default App", "settings.currentHandlerSelf": "Tabbit (this browser)", "settings.currentHandlerUnknown": "No default app detected", "settings.detecting": "Detecting...", "settings.detectUnavailable": "Unavailable", "settings.systemDefault": "Set as System Default", "settings.systemDefaultActive": "Currently the system default", "settings.setAsDefault": "Set as default", "settings.setAllAsDefault": "Set all as default", "settings.setAllNothingPending": "Nothing in this section can be set as default right now", "settings.unsetDefaultHint": "To remove the default, change it in system settings", "settings.openWith": "Open in Tabbit with", "settings.openWithPreviewer": "Tabbit Previewer", "settings.openWithNative": "Browser built‑in", "settings.openWithNativeUnavailable": "This format can't be shown usefully by the browser's built-in viewer", "settings.setDefaultRequested": "Requested to make Tabbit the default app for {extension}", "settings.openedSystemSettings": "System settings opened. Choose Tabbit as the default app for {extension}", "settings.setDefaultUnsupported": "Setting the default app for {extension} is not supported on this system", "settings.setDefaultFailed": "Failed to set the default app for {extension}", "settings.type.markdown": "Markdown", "settings.type.svg": "SVG", "settings.type.image": "Images", "settings.type.media": "Audio & Video", "settings.type.pdf": "PDF", "settings.type.office": "Office Documents", "settings.type.html": "HTML", "settings.type.codeText": "Code & Text", "settings.type.subtitle": "Subtitles", "settings.type.archive": "Archives", "editor.sourceLabel": "File content", "markdown.contentLabel": "Markdown content", "code.contentLabel": "Code content", "code.delimitedContentLabel": "Table source text", "code.tablePreviewAria": "{fileName} table preview", "code.unnamedColumn": "Column {index}", "code.tablePreviewLimited": "Previewing only the first {rows} rows and first {columns} columns; edit mode keeps the complete source text.", "code.noFormattingNeeded": "Current content does not need formatting", "code.formatApplied": "Formatting applied", "code.sourceReloaded": "{fileName} source file reloaded", "code.formatJson": "Format JSON", "code.expandCode": "Expand Code", "code.subtitleViewMode": "Subtitle view", "code.subtitleTimeline": "Timeline", "code.subtitleSource": "Source", "code.subtitleInfoFormat": "Format", "code.subtitleInfoCues": "Cues", "code.subtitleInfoDuration": "Duration", "code.subtitleInfoLanguage": "Language", "code.subtitleInfoEncoding": "Encoding", "code.subtitleTimelineTruncated": "Too many cues: the timeline shows the first {shown} of {total}. Switch to the source view for the full text.", "code.subtitleSearchPlaceholder": "Search dialogue, or a time like 12:30", "code.subtitleCopyCue": "Copy this cue", "code.subtitleTextView": "Plain text", "code.subtitleTextTruncated": "Too many cues: the plain-text view shows the first {shown} of {total}. \"Copy all\" still includes everything.", "code.subtitleBilingual": "Bilingual", "code.subtitleEncodingTooltip": "Detected file encoding", "code.subtitleTranscode": "Re-save as UTF-8", "code.subtitleTranscoded": "Saved as UTF-8", "code.subtitleTranscodeFailed": "Re-save failed, please try again", "code.subtitleBinaryNotice": "This is a binary bitmap subtitle file (VobSub graphic subtitles). It cannot be previewed or edited as text. Keep it next to its matching .idx file and load it in a video player.", "code.subtitleExportMenu": "Export", "code.subtitleExportSrt": "SRT", "code.subtitleExportVtt": "VTT", "code.subtitleExportTxt": "Plain text (TXT)", "code.subtitleExportUnparseable": "Could not parse the subtitles; nothing was exported", "code.subtitleExported": "Exported {fileName}", "code.subtitleExportFailed": "Export failed, please try again", "code.searchPlaceholder": "Find", "code.searchPrev": "Previous", "code.searchNext": "Next", "code.searchClose": "Close", "code.replacePlaceholder": "Replace with", "code.replaceOne": "Replace", "code.replaceAll": "Replace all", "code.replacedCount": "Replaced {count} occurrences", "code.subtitleShiftMenu": "Shift timing", "code.subtitleShiftLabel": "Offset (ms)", "code.subtitleShiftHint": "Positive delays, negative advances; saves immediately. Fixes a constant offset only, not frame-rate drift.", "code.subtitleShiftApply": "Apply and save", "code.subtitleShiftInvalid": "Enter a non-zero offset in milliseconds", "code.subtitleShifted": "Shifted {count} cue timings and saved", "code.subtitleShiftedClamped": "Shifted and saved; some timings were clamped to 00:00", "code.subtitleShiftFailed": "Shift failed, please try again", "code.subtitleShiftExported": "This format cannot be shifted in place; exported the shifted {fileName}", "code.subtitleCleanMenu": "Clean up", "code.subtitleCleanTags": "Remove styling tags", "code.subtitleCleanAnnotations": "Remove bracketed captions (SDH)", "code.subtitleCleanMusic": "Remove music lines", "code.subtitleCleanDuplicates": "Merge duplicate and empty cues", "code.subtitleCleaned": "Cleaned {count} items and saved", "code.subtitleCleanNothing": "Nothing to clean", "code.subtitleCleanFailed": "Clean up failed, please try again", "code.subtitleCleanExported": "This format cannot be cleaned in place; exported the cleaned {fileName}", "code.subtitleTranslate": "Translate the subtitles into a new file", "code.subtitleTranslateTitle": "Translate subtitles", "code.subtitleTranslateModeLabel": "Output", "code.subtitleTranslateModeBilingual": "Bilingual (translation above)", "code.subtitleTranslateModeReplace": "Translation only", "code.subtitleTranslateLanguageLabel": "Target language", "code.subtitleTranslateOutputLabel": "Save as", "code.subtitleTranslateOutputNew": "New file", "code.subtitleTranslateOutputOverwrite": "Overwrite this file", "code.subtitleTranslateOverwriteUnavailable": "Only a writable local SRT can be overwritten; this file will be saved as a new file", "code.subtitleTranslateSummary": "Will translate {count} cues (~{characters} characters); cues already in the target language are skipped. Translation consumes your account quota.", "code.subtitleTranslateNothing": "Nothing to translate (the target language may already be present)", "code.subtitleTranslateStart": "Start translating", "code.subtitleTranslateCancel": "Cancel", "code.subtitleTranslateRunning": "Translating {completed}/{total}…", "code.subtitleTranslateCancelled": "Cancelled; the file was not changed", "code.subtitleTranslateUnavailable": "Translation service unavailable; please make sure the browser is up to date", "code.subtitleTranslateSourceChanged": "Subtitle content changed; please restart translation", "code.subtitleTranslateFailed": "Translation failed: {error}", "code.subtitleTranslateDoneOverwrite": "Translated {count} cues and saved to this file", "code.subtitleTranslateDoneExported": "Translated {count} cues; exported {fileName}", "code.subtitleLangChinese": "Chinese", "code.subtitleLangJapanese": "Japanese", "code.subtitleLangKorean": "Korean", "code.subtitleLangLatin": "English/Latin", "code.subtitleLangCyrillic": "Cyrillic", "code.subtitleLangArabic": "Arabic", "subtitle.binaryUnknown": "Binary SUB", "subtitle.vobsubDescription": "MPEG Program Stream / DVD subpicture data detected; it will not be decoded or written as text.", "subtitle.binaryDescription": "This SUB file contains binary data. Text decoding was stopped to prevent garbled output or accidental writes.", "subtitle.vobsubCompanionHint": "VobSub images, languages, and timing are normally described by a same-name .idx file. Open the companion IDX to inspect timestamps.", "subtitle.scannedBytes": "Header scanned", "subtitle.packHeaders": "MPEG pack headers", "subtitle.privateStreams": "Subtitle private-stream packets", "html.sourceLoading": "Reading source", "html.sourceEditorAria": "HTML source editor", "html.previewAria": "HTML preview", "html.previewFrameTitle": "{fileName} preview", "html.printFrameTitle": "{fileName} print preview", "html.preparing": "Preparing source and preview", "html.viewMode": "HTML view mode", "html.modeSource": "Source", "html.modeSplit": "Split", "html.modePreview": "Preview", "html.reload": "Reload", "html.sourceReloaded": "Source reloaded", "html.sourceRead": "HTML source loaded", "html.readFailed": "Read failed: unable to read the HTML source file", "html.readRequiresWorkspace": "Read failed: local workspace access is required to read HTML source", "html.previewUnavailable": "This file may have been moved, changed, or deleted.", "html.previewUnavailableInArchive": "HTML inside an archive is not supported for preview yet. The source is available on the left; extract the archive to preview it.", "html.previewUnavailableInArchiveStatus": "Preview not supported", "image.stageAria": "Image preview stage", "image.loadingTitle": "Loading image", "image.loadingMeta": "{extension} · Reading image", "image.readyStatus": "Zoom, pan, rotate, and flip", "image.zoomOut": "Zoom Out", "image.zoomIn": "Zoom In", "image.fit": "Fit to Window", "image.actualSize": "100% Actual Size", "image.rotateLeft": "Rotate Left", "image.rotateRight": "Rotate Right", "image.flipHorizontal": "Flip Horizontal", "image.flipVertical": "Flip Vertical", "image.reset": "Reset", "image.exportPng": "PNG", "image.loadFailedTitle": "Image failed to load", "image.loadFailedMeta": "{extension} · Load failed", "image.loadFailedStatus": "Unable to read this image. Check whether the link is still valid.", "image.zoomedOut": "Zoomed out", "image.zoomedIn": "Zoomed in. Drag to inspect details.", "image.fitApplied": "Fit to window", "image.actualSizeApplied": "Switched to 100% actual size", "image.resetApplied": "Reset transforms and fit to window", "image.wheelZoomed": "Wheel zoomed. Drag to inspect details.", "image.transformed": "Image transformed", "image.panned": "View moved", "image.notLoaded": "Image has not finished loading", "image.exportedPng": "PNG exported", "image.exportFailed": "Export failed. The browser may have restricted canvas access for this local image.", "media.kind.audio": "Audio", "media.kind.video": "Video", "media.loading": "Reading media information", "media.playbackRate": "Playback Speed", "media.playbackRateValue": "Playback speed: {rate}", "media.pictureInPicture": "Picture in Picture", "media.pictureInPictureActive": "Picture in Picture active", "media.pictureInPictureFailed": "Unable to enter Picture in Picture", "media.fullscreen": "Full Screen", "media.fullscreenActive": "Full screen active", "media.fullscreenFailed": "Unable to enter full screen", "media.loadFailed": "Media failed to load", "media.loadFailedDetail": "Unable to play this media; its container or codec may not be supported", "svg.stageAria": "SVG preview region. Use the arrow keys to move the zoomed view.", "pdf.loadingMeta": "PDF · Reading file", "pdf.initialStatus": "Rendered with PDF.js. The raw file is available as a browser fallback.", "pdf.toggleNavigation": "Show or hide PDF navigation", "pdf.navLabel": "Navigation", "pdf.hideNavigation": "Hide PDF navigation", "pdf.showNavigation": "Show PDF navigation", "pdf.previousPage": "Previous Page", "pdf.nextPage": "Next Page", "pdf.pageJumpTitle": "Jump to page", "pdf.currentPage": "Current page", "pdf.pageLabelTitle": "Page {page} · label {label}", "pdf.searchTitle": "Search PDF text", "pdf.searchPlaceholder": "Search PDF", "pdf.previousSearchResult": "Previous search result", "pdf.nextSearchResult": "Next search result", "pdf.fitWidth": "Fit Width", "pdf.fitPage": "Fit Page", "pdf.handTool": "Hand tool", "pdf.handToolOn": "Hand tool on: drag with the left button to pan", "pdf.handToolOff": "Hand tool off: drag to select text", "pdf.rotateRight": "Rotate Right", "pdf.viewMode": "PDF view mode", "pdf.viewContinuous": "Continuous", "pdf.viewSingle": "Single", "pdf.viewTwoPage": "Two-Page", "pdf.readAloud": "Read current page or selected text aloud", "pdf.readAloudLabel": "Read Aloud", "pdf.rerender": "Render Again", "pdf.navAria": "PDF navigation", "pdf.navTabsAria": "PDF navigation panels", "pdf.navTitle": "Reading navigation", "pdf.tabPages": "Pages", "pdf.tabOutline": "Outline", "pdf.tabNotes": "Notes", "pdf.infoLoading": "Reading document info", "pdf.infoEmpty": "This PDF carries no readable document info", "pdf.propTitle": "Title", "pdf.propAuthor": "Author", "pdf.propSubject": "Subject", "pdf.propKeywords": "Keywords", "pdf.propCreator": "Application", "pdf.propProducer": "PDF producer", "pdf.propCreated": "Created", "pdf.propVersion": "PDF version", "pdf.infoAttachments": "Attachment(s)", "pdf.attachmentSave": "Save attachment {name}", "pdf.attachmentSaving": "Saving attachment {name}", "pdf.attachmentSaved": "Saved attachment {name}", "pdf.attachmentSaveFailed": "Could not save attachment {name}", "pdf.infoLayers": "Layer(s)", "pdf.layerUntitled": "Untitled layer", "pdf.layerShown": "Layer \u201c{name}\u201d shown", "pdf.layerHidden": "Layer \u201c{name}\u201d hidden", "pdf.infoSignature": "Digital signature(s)", "pdf.infoSignatureN": "Digital signature {index}", "pdf.metaSigned": "Signed", "pdf.signatureSigner": "Signer", "pdf.signatureTime": "Signed at", "pdf.signatureReason": "Reason", "pdf.signatureLocation": "Location", "pdf.signatureContact": "Contact", "pdf.signatureField": "Signature field", "pdf.permission.print": "Print", "pdf.permission.copy": "Copy text", "pdf.permission.modify": "Modify content", "pdf.permission.annotate": "Add annotations", "pdf.permission.forms": "Fill forms", "pdf.permissionAllowed": "Allowed", "pdf.permissionRestricted": "Restricted by author", "pdf.printRestricted": "The author of this PDF restricted printing; the printout may be limited", "pdf.copyRestricted": "The author of this PDF restricted copying text", "pdf.navLoading": "Preparing navigation", "pdf.notePlaceholder": "Write a note for the current page", "pdf.noteHint": "Notes stay on this device and do not modify the PDF file.", "pdf.noteAria": "Current PDF page note", "pdf.stageAria": "{fileName} PDF preview", "pdf.loadingTitle": "Loading PDF", "pdf.loadingDescription": "Parsing the file and preparing page rendering.", "pdf.zoomedOut": "Zoomed out", "pdf.zoomedIn": "Zoomed in", "pdf.fitWidthApplied": "Fit to width", "pdf.fitPageApplied": "Fit to page", "pdf.actualSizeApplied": "Switched to 100% actual size", "pdf.rotatedRight": "Rotated PDF view right", "pdf.viewModeApplied": "Switched to {mode} view", "pdf.rerenderingVisible": "Rendering visible pages again", "pdf.wheelZoomed": "Wheel zoomed PDF view", "pdf.readFailed": "Unable to read the PDF file. Try opening the raw file.", "pdf.parsing": "Parsing PDF", "pdf.renderedPages": "PDF.js rendered · {pages}", "pdf.pageAria": "Page {page}", "pdf.thumbnailTitle": "Jump to page {page}", "pdf.pageHasNote": "This page has a note", "pdf.waitingRender": "Waiting to render", "pdf.rendering": "Rendering", "pdf.renderFailed": "Render failed", "pdf.noTranslatableText": "No translatable text was detected on this page. Scanned pages require an OCR text layer.", "pdf.translationPageAria": "Translation for page {page}", "pdf.translatingPage": "Translating page {page}…", "pdf.translationProgress": "Translated {completed}/{total} blocks", "pdf.translationFailed": "Translation failed for this page", "pdf.translationRetry": "Translation failed. Click to retry", "pdf.translationBudgetExceeded": "This page exceeds the translation limit of 64 blocks or 48K characters.", "pdf.ocrTextTooLarge": "Recognized text on this page exceeds the 64KiB safety limit.", "pdf.readingProgress": "Reading PDF · {percent}%", "pdf.searching": "Searching PDF text", "pdf.searchFound": "Found {count} matches", "pdf.searchNoResults": "No matches found", "pdf.searchCleared": "Search cleared", "pdf.notePage": "Page {page} note", "pdf.noteUnsaved": "Note not saved", "pdf.noteSaved": "Note saved on this device", "pdf.marksTitle": "Marks on this page", "pdf.marksHint": "Select text to highlight or underline it. Marks stay on this device and are never written into the PDF.", "pdf.marksEmpty": "No marks on this page yet", "pdf.markHighlight": "Highlight", "pdf.markUnderline": "Underline", "pdf.markCopy": "Copy", "pdf.toolHighlight": "Highlight", "pdf.toolInk": "Draw", "pdf.toolFreeText": "Text", "pdf.toolStamp": "Image", "pdf.editUnavailable": "This PDF.js build does not support annotation editing", "pdf.editRestricted": "The document's author restricted adding annotations", "pdf.exportAnnotated": "Export a copy with the annotations baked in, leaving the original alone", "pdf.exportAnnotatedLabel": "PDF copy with annotations", "pdf.exportAnnotatedNeedsEdit": "Enter edit mode and add annotations to export a copy", "pdf.exportAnnotatedDone": "Exported {name}", "pdf.exportAnnotatedFailed": "Could not export the annotated copy", "pdf.annotatedCopySuffix": "annotated", "pdf.saveAnnotations": "Write the annotations into the original PDF", "pdf.saveAnnotationsDone": "Annotations written into the original file", "pdf.saveAnnotationsFailed": "Could not write the annotations into the original file", "pdf.saveAnnotationsConfirmTitle": "Write into the original PDF?", "pdf.saveAnnotationsConfirmDescription": "This writes the annotations from this editing session into “{fileName}” itself, overwriting the file on disk. Preview-mode highlights and underlines stay on this device and are not included.", "pdf.saveAnnotationsConfirm": "Write into original", "pdf.annotationsEmpty": "No annotations to save yet", "pdf.markAdded": "{kind} added; kept on this device only", "pdf.markRemoved": "Mark removed", "pdf.markDelete": "Delete mark", "pdf.markJump": "Scroll to this {kind}", "pdf.copied": "Copied the selected text", "pdf.copyFailed": "Copy failed", "pdf.selectionToolbarAria": "Selected text actions", "pdf.outlineLoading": "Reading outline", "pdf.outlineUnavailable": "This PDF has no readable outline", "pdf.outlineEmpty": "This PDF has no outline", "pdf.outlineUntitled": "Untitled outline item", "pdf.outlineGoTo": "Jump to page {page}", "pdf.outlineExternal": "Open external link", "pdf.linkInternal": "Jump to the link target in this document", "pdf.linkUnresolved": "The target page of this link could not be located", "pdf.annotationAria": "Annotation: {summary}", "pdf.readAloudStopped": "Read aloud stopped", "pdf.readAloudNoText": "No readable text on this page", "pdf.readAloudFinished": "Read aloud finished", "pdf.readAloudFailed": "Read aloud failed", "pdf.readAloudSelection": "Reading selected text", "pdf.readAloudPage": "Reading page {page}", "pdf.previewFailedTitle": "PDF preview failed", "pdf.passwordRequired": "This PDF requires a password. Encrypted PDFs are not handled in the current preview.", "pdf.renderErrorDetail": "PDF.js could not render this file: {detail}", "pdf.renderError": "PDF.js could not render this file.", "pdf.canvasUnavailable": "This browser cannot create a PDF canvas.", "office.loadingTitle": "Reading file", "office.loadingDescription": "Previewer will try to extract previewable content.", "office.kindStatus": "{kind} document", "office.parsing": "Parsing document content", "office.readFailed": "Unable to read the current local file.", "office.unsupportedExtension": "This Office extension is not supported yet.", "office.parseFailed": "Document parsing failed. The file may be encrypted, damaged, or ZIP decompression may be unavailable in this browser.", "office.noDocxBody": "No DOCX body content was found.", "office.docxMeta": "DOCX · {paragraphs} · Read-only text preview; complex styling follows the original file", "office.docxBodyLabel": "DOCX body", "office.docxReady": "DOCX loaded in read-only text preview.", "office.docxRendering": "Rendering DOCX preview", "office.docxRendered": "DOCX preview rendered", "office.docxFitWidth": "Fit Width", "office.docxZoomedOut": "DOCX preview zoomed out", "office.docxZoomedIn": "DOCX preview zoomed in", "office.docxFitWidthApplied": "Fit DOCX page to width", "office.docxActualSizeApplied": "Switched DOCX preview to 100% actual size", "office.noXlsxSheets": "No XLSX worksheets were found.", "office.sheet": "Sheet", "office.sheetCount": "{count}", "office.currentSheetTsv": "Current sheet TSV", "office.sheetSwitched": "Sheet switched.", "office.noPptxSlides": "No PPTX slides were found.", "office.pptxMissingViewer": "PptxViewJS is not loaded. Switched to text outline preview.", "office.pptxRendering": "Rendering PPTX with PptxViewJS", "office.pptxReady": "PPTX loaded in read-only text outline preview.", "office.pptxTextLabel": "PPTX slide text", "office.pptxOutlineTitle": "Text Outline", "office.pptxOutlineDescription": "View slide text", "office.pptxBackToSlides": "Back to Slide Preview", "office.pptxViewerLoading": "Loading {count}", "office.pptxPreviousPage": "Previous Page", "office.pptxNextPage": "Next Page", "office.pptxZoomOut": "Zoom Out", "office.pptxZoomIn": "Zoom In", "office.pptxFit": "Fit", "office.pptxViewerMountFailed": "PPTX preview mount failed. Switched to text outline preview.", "office.pptxRenderingPage": "Rendering page {page}", "office.pptxViewerReady": "PptxViewJS preview", "office.pptxViewerReadyStatus": "PPTX rendered with PptxViewJS.", "office.pptxRenderFailed": "PptxViewJS rendering failed. Switched to text outline preview.", "office.legacyUnsupportedStatus": "Legacy Office binary formats cannot be parsed in the browser yet.", "office.legacyUnsupportedTitle": "{extension} preview is not supported yet", "office.legacyUnsupportedDescription": "The lightweight path currently supports .docx, .xlsx, and .pptx. Save legacy formats as modern OOXML and open them again.", "office.parseErrorTitle": "Unable to parse document", "office.emptyDocxTitle": "No body text extracted", "office.emptyDocxDescription": "This document may mainly contain images, charts, or protected content.", "office.emptyXlsxTitle": "No table content to show", "office.emptyXlsxDescription": "Switch to edit mode to fill TSV content, then save.", "office.chart": "Chart", "office.chartSeries": "Series {index}", "office.chartUnresolved": "The chart structure was recognized, but the referenced data range cannot be resolved yet.", "office.embeddedChart": "Embedded chart", "office.emptyPptxTitle": "No slide text extracted", "office.emptyPptxDescription": "This presentation may mainly contain images or shapes.", "office.blankSlide": "Blank slide", "office.xlsxMeta": "XLSX · {sheetName} · {sheetCount} · Saves current displayed values; complex formulas and styles use lightweight compatibility", "office.pptxMeta": "PPTX · {slides} · {images} · Light editing saves text while images/layout keep the original structure", "unsupported.displayName": "Unsupported file", "unsupported.description": "This file type cannot be previewed directly. You can locate it in the file manager.", "unsupported.type": "Type", "unsupported.path": "Path", "unsupported.size": "Size", "unsupported.created": "Created", "unsupported.modified": "Modified", "unsupported.reading": "Reading...", "unsupported.revealRequested": "File manager reveal requested", "unsupported.revealFailed": "Unable to locate file", "unsupported.unreadable": "Unable to read", "zip.displayName": "Archive", "zip.kicker": "Archive", "zip.folderKicker": "Archive folder", "zip.entryKicker": "Archive entry", "zip.loading": "Reading archive...", "zip.ready": "Archive index loaded", "zip.refresh": "Reload from disk", "zip.description": "Expand the archive in the sidebar to browse its files without extracting it.", "zip.emptyArchive": "This archive contains no files.", "zip.entries": "File entries", "zip.childEntries": "In this folder", "zip.uncompressedSize": "Original size", "zip.compressedSize": "Compressed size", "zip.ratio": "Compression ratio", "zip.archiveSize": "Archive size", "zip.archive": "Archive", "zip.entryPath": "Path in archive", "zip.readOnly": "Files inside an archive are read-only and cannot be edited or renamed.", "zip.readFailed": "This archive could not be read.", "zip.tooLarge": "The archive is larger than the {size} preview budget and was not read.", "zip.corrupt": "This file is damaged or is not a valid ZIP archive.", "zip.skippedUnsafe": "Skipped {count} entries whose paths escape the archive root.", "zip.truncated": "Too many entries; only the first {count} were indexed.", "zip.encryptedEntries": "{count} entries are encrypted and cannot be previewed.", "zip.entryEncrypted": "This entry is encrypted and needs a password, so it cannot be previewed.", "zip.entryUnsupportedMethod": "This entry uses a compression method the browser cannot decode.", "zip.entryTooLarge": "This entry expands beyond the per-entry preview budget and was not opened.", "zip.entryRatio": "This entry has an abnormal compression ratio and was not expanded, as a safety measure.", "zip.entryCorrupt": "This entry is damaged and cannot be expanded.", "zip.entryNotFound": "This entry is not present in the archive.", "fileType.directory": "Folder", "fileType.markdown": "Markdown", "fileType.svg": "SVG", "fileType.image": "Image", "fileType.pdf": "PDF", "fileType.office": "Office", "fileType.html": "HTML", "fileType.code-text": "Code / Text", "fileType.unsupported": "Unsupported file" } }; const COUNT_UNITS = { "zh-CN": { line: { one: "行", other: "行" }, page: { one: "页", other: "页" }, sheet: { one: "个工作表", other: "个工作表" }, paragraph: { one: "段正文", other: "段正文" }, image: { one: "张图片", other: "张图片" }, slide: { one: "页幻灯片", other: "页幻灯片" }, entry: { one: "项", other: "项" } }, en: { line: { one: "line", other: "lines" }, page: { one: "page", other: "pages" }, sheet: { one: "sheet", other: "sheets" }, paragraph: { one: "paragraph", other: "paragraphs" }, image: { one: "image", other: "images" }, slide: { one: "slide", other: "slides" }, entry: { one: "item", other: "items" } } }; function currentLocale() { const language = browserUiLanguage(); if (!language) { return DEFAULT_LOCALE; } return normalizeLocale(language); } TabLocalFiles.currentLocale = currentLocale; function t(key, values = {}) { return messageTemplate(key).replace(/\{(\w+)\}/g, (_match, name) => String(values[name] ?? "")); } TabLocalFiles.t = t; function messageTemplate(key) { const locale = currentLocale(); return I18N_MESSAGES[locale][key] || I18N_MESSAGES[DEFAULT_LOCALE][key] || key; } TabLocalFiles.messageTemplate = messageTemplate; function formatCount(count, unit) { const locale = currentLocale(); const unitMessages = COUNT_UNITS[locale][unit]; const label = count === 1 ? unitMessages.one : unitMessages.other; return locale === "zh-CN" ? `${count} ${label}` : `${count} ${label}`; } TabLocalFiles.formatCount = formatCount; function fileTypeDisplayName(definition) { return t(`fileType.${definition.id}`) || definition.displayName; } TabLocalFiles.fileTypeDisplayName = fileTypeDisplayName; function applyDocumentLocale() { if (typeof document === "undefined") { return; } document.documentElement.lang = currentLocale(); document.documentElement.dir = "ltr"; } TabLocalFiles.applyDocumentLocale = applyDocumentLocale; function localizeDocument(root) { const target = root || safeDocument(); if (!target) { return; } applyDocumentLocale(); target.querySelectorAll("[data-tlf-i18n]").forEach((element) => { const key = element.dataset.tlfI18n; if (key) { element.textContent = t(key); } }); target.querySelectorAll("[data-tlf-i18n-title]").forEach((element) => { const key = element.dataset.tlfI18nTitle; if (key) { element.setAttribute("title", t(key)); } }); target.querySelectorAll("[data-tlf-i18n-aria-label]").forEach((element) => { const key = element.dataset.tlfI18nAriaLabel; if (key) { element.setAttribute("aria-label", t(key)); } }); target.querySelectorAll("[data-tlf-i18n-placeholder]").forEach((element) => { const key = element.dataset.tlfI18nPlaceholder; if (key) { element.setAttribute("placeholder", t(key)); } }); } TabLocalFiles.localizeDocument = localizeDocument; function browserUiLanguage() { const chromeI18n = globalThis.chrome?.i18n; try { const language = chromeI18n?.getUILanguage?.(); if (language) { return language; } } catch { } const nav = globalThis.navigator; return nav?.languages?.[0] || nav?.language; } function safeDocument() { return typeof document === "undefined" ? undefined : document; } function normalizeLocale(language) { const normalized = language.toLowerCase().replace("_", "-"); return normalized === "zh" || normalized.startsWith("zh-") ? "zh-CN" : "en"; } })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { TabLocalFiles.RENDERER_CONTENT_SCRIPT_ID = "tab-local-files-renderer"; TabLocalFiles.PREFERENCES_STORAGE_KEY = "tab-local-files:preferences"; })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var Preferences; (function (Preferences) { const LOCAL_CACHE_KEY = "tab-local-files:preferences-cache"; let cache; let hydration; let mutationRevision = 0; const changeListeners = []; function read() { if (!cache) { cache = parsePreferences(safeLocalStorageGet(LOCAL_CACHE_KEY)) || {}; } return cache; } Preferences.read = read; function readOpenModes() { return { ...(read().openModes || {}) }; } Preferences.readOpenModes = readOpenModes; function useDocumentTitle() { return (read().preview || {}).documentTitle !== false; } Preferences.useDocumentTitle = useDocumentTitle; function showLineNumbers() { return (read().preview || {}).lineNumbers !== false; } Preferences.showLineNumbers = showLineNumbers; function useWordWrap() { return (read().preview || {}).wordWrap !== false; } Preferences.useWordWrap = useWordWrap; const MARKDOWN_READING_WIDTHS = ["fixed", "full"]; const MARKDOWN_FONT_SIZES = ["medium", "large", "xlarge"]; function markdownReadingWidth() { return oneOf((read().preview || {}).markdownWidth, MARKDOWN_READING_WIDTHS, "fixed"); } Preferences.markdownReadingWidth = markdownReadingWidth; function markdownFontSize() { return oneOf((read().preview || {}).markdownFontSize, MARKDOWN_FONT_SIZES, "medium"); } Preferences.markdownFontSize = markdownFontSize; function oneOf(value, allowed, fallback) { return typeof value === "string" && allowed.includes(value) ? value : fallback; } function update(patch) { const current = read(); const next = { ...current, ...(patch.directory ? { directory: { ...(current.directory || {}), ...patch.directory } } : {}), ...(patch.openModes ? { openModes: { ...(current.openModes || {}), ...patch.openModes } } : {}), ...(patch.preview ? { preview: { ...(current.preview || {}), ...patch.preview } } : {}) }; commit(next); return next; } Preferences.update = update; function writeOpenMode(extension, mode) { const normalized = extension.toLowerCase(); const openModes = readOpenModes(); if (mode) { openModes[normalized] = mode; } else { delete openModes[normalized]; } commit({ ...read(), openModes }); } Preferences.writeOpenMode = writeOpenMode; function init() { if (hydration) { return hydration; } subscribeToExternalChanges(); const hydrationRevision = mutationRevision; hydration = new Promise((resolve) => { const storage = chromeStorageApi()?.local; const get = storage?.get; if (!get) { resolve(); return; } try { get.call(storage, TabLocalFiles.PREFERENCES_STORAGE_KEY, (result) => { if (mutationRevision === hydrationRevision) { applyExternalValue(result?.[TabLocalFiles.PREFERENCES_STORAGE_KEY]); } resolve(); }); } catch { resolve(); } }); return hydration; } Preferences.init = init; function onChange(listener) { changeListeners.push(listener); } Preferences.onChange = onChange; function commit(next) { mutationRevision += 1; cache = next; const serialized = JSON.stringify(next); safeLocalStorageSet(LOCAL_CACHE_KEY, serialized); const storage = chromeStorageApi()?.local; const set = storage?.set; if (!set) { return; } try { set.call(storage, { [TabLocalFiles.PREFERENCES_STORAGE_KEY]: next }); } catch { } } function subscribeToExternalChanges() { const changedEvent = chromeStorageApi()?.onChanged; const addListener = changedEvent?.addListener; if (!addListener) { return; } try { addListener.call(changedEvent, (changes, areaName) => { if (areaName !== "local" || !(TabLocalFiles.PREFERENCES_STORAGE_KEY in changes)) { return; } mutationRevision += 1; applyExternalValue(changes[TabLocalFiles.PREFERENCES_STORAGE_KEY]?.newValue); }); } catch { } } function applyExternalValue(value) { const parsed = normalizePreferences(value); if (!parsed || JSON.stringify(parsed) === JSON.stringify(read())) { return; } cache = parsed; safeLocalStorageSet(LOCAL_CACHE_KEY, JSON.stringify(parsed)); changeListeners.forEach((listener) => { try { listener(); } catch { } }); } function parsePreferences(raw) { if (!raw) { return undefined; } try { return normalizePreferences(JSON.parse(raw)); } catch { return undefined; } } function normalizePreferences(value) { if (!value || typeof value !== "object") { return undefined; } const record = value; const preferences = {}; if (record.directory && typeof record.directory === "object") { preferences.directory = record.directory; } if (record.preview && typeof record.preview === "object") { preferences.preview = record.preview; } if (record.openModes && typeof record.openModes === "object") { const openModes = {}; for (const [extension, mode] of Object.entries(record.openModes)) { if (mode === "native" || mode === "previewer") { openModes[extension.toLowerCase()] = mode; } } preferences.openModes = openModes; } return preferences; } function chromeStorageApi() { return globalThis.chrome?.storage; } function safeLocalStorageGet(key) { try { return globalThis.localStorage?.getItem(key) ?? null; } catch { return null; } } function safeLocalStorageSet(key, value) { try { globalThis.localStorage?.setItem(key, value); } catch { } } })(Preferences = TabLocalFiles.Preferences || (TabLocalFiles.Preferences = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { TabLocalFiles.CREATE_DOCUMENT_TYPES = [ { id: "markdown", label: "Markdown", extension: ".md" }, { id: "text", label: "Text", extension: ".txt" }, { id: "html", label: "HTML", extension: ".html" }, { id: "json", label: "JSON", extension: ".json" }, { id: "yaml", label: "YAML", extension: ".yaml" }, { id: "csv", label: "CSV", extension: ".csv" } ]; function createDocumentTypeById(typeId) { return TabLocalFiles.CREATE_DOCUMENT_TYPES.find((type) => type.id === typeId) || TabLocalFiles.CREATE_DOCUMENT_TYPES[0]; } TabLocalFiles.createDocumentTypeById = createDocumentTypeById; function suggestedDocumentFileName(typeId) { const type = createDocumentTypeById(typeId); return `${TabLocalFiles.t("common.untitled")}${type.extension}`; } TabLocalFiles.suggestedDocumentFileName = suggestedDocumentFileName; function suggestedUniqueDocumentFileName(typeId, existingNames) { const type = createDocumentTypeById(typeId); const occupiedNames = new Set(existingNames.map((name) => name.toLowerCase())); for (let index = 1; index <= 500; index += 1) { const suffix = index === 1 ? "" : ` ${index}`; const candidate = `${TabLocalFiles.t("common.untitled")}${suffix}${type.extension}`; if (!occupiedNames.has(candidate.toLowerCase())) { return candidate; } } return suggestedDocumentFileName(type.id); } TabLocalFiles.suggestedUniqueDocumentFileName = suggestedUniqueDocumentFileName; function normalizeNewDocumentFileName(rawName, typeId) { const type = createDocumentTypeById(typeId); const trimmedName = rawName.trim() || suggestedDocumentFileName(type.id); const currentExtension = extensionFromDocumentFileName(trimmedName); return currentExtension ? `${trimmedName.slice(0, -currentExtension.length)}${type.extension}` : `${trimmedName}${type.extension}`; } TabLocalFiles.normalizeNewDocumentFileName = normalizeNewDocumentFileName; function initialDocumentText(typeId, fileName) { const title = titleFromDocumentFileName(fileName) || TabLocalFiles.t("common.untitled"); if (typeId === "markdown") { return `# ${title}\n`; } if (typeId === "html") { const htmlTitle = escapeDocumentHtml(title); return `<!doctype html>\n<html lang="${TabLocalFiles.currentLocale()}">\n <head>\n <meta charset="utf-8">\n <title>${htmlTitle}\n \n \n

${htmlTitle}

\n \n\n`; } if (typeId === "json") { return "{\n}\n"; } return ""; } TabLocalFiles.initialDocumentText = initialDocumentText; function extensionFromDocumentFileName(fileName) { const match = fileName.toLowerCase().match(/\.[^.]+$/); return match ? match[0] : ""; } function titleFromDocumentFileName(fileName) { const extension = extensionFromDocumentFileName(fileName); return extension ? fileName.slice(0, -extension.length) : fileName; } function escapeDocumentHtml(value) { return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var FileBookmarks; (function (FileBookmarks) { FileBookmarks.RECENTS_STORAGE_KEY = "tab-local-files:recent-entries"; FileBookmarks.FAVORITES_STORAGE_KEY = "tab-local-files:favorite-entries"; FileBookmarks.RECENTS_LIMIT = 10; const ICON_NAME_PATTERN = /^[a-z0-9-]+$/; function normalizeEntry(value) { if (!value || typeof value !== "object") { return undefined; } const record = value; const url = typeof record.url === "string" ? record.url : ""; const path = typeof record.path === "string" ? record.path : ""; const name = typeof record.name === "string" ? record.name : ""; if (!url || !path || !name || !isFileUrl(url)) { return undefined; } const kind = record.kind === "directory" ? "directory" : "file"; const rawIcon = typeof record.icon === "string" ? record.icon : ""; const entry = { url, path, name, kind, icon: ICON_NAME_PATTERN.test(rawIcon) ? rawIcon : kind === "directory" ? "folder" : "file" }; if (typeof record.fileType === "string" && record.fileType) { entry.fileType = record.fileType; } if (typeof record.openedAt === "number" && Number.isFinite(record.openedAt)) { entry.openedAt = record.openedAt; } if (typeof record.addedAt === "number" && Number.isFinite(record.addedAt)) { entry.addedAt = record.addedAt; } return entry; } FileBookmarks.normalizeEntry = normalizeEntry; function normalizeEntries(value) { if (!Array.isArray(value)) { return []; } const entries = []; const seen = new Set(); for (const item of value) { const entry = normalizeEntry(item); if (entry && !seen.has(entry.url)) { seen.add(entry.url); entries.push(entry); } } return entries; } FileBookmarks.normalizeEntries = normalizeEntries; function hasEntry(entries, url) { return entries.some((entry) => entry.url === url); } FileBookmarks.hasEntry = hasEntry; function withRecent(entries, entry, now, limit = FileBookmarks.RECENTS_LIMIT) { const next = { ...entry, openedAt: now }; delete next.addedAt; return [next, ...entries.filter((item) => item.url !== entry.url)].slice(0, Math.max(0, limit)); } FileBookmarks.withRecent = withRecent; function withFavoriteToggled(entries, entry, now) { if (hasEntry(entries, entry.url)) { return { entries: entries.filter((item) => item.url !== entry.url), favorite: false }; } const next = { ...entry, addedAt: now }; delete next.openedAt; return { entries: [...entries, next], favorite: true }; } FileBookmarks.withFavoriteToggled = withFavoriteToggled; function withRelocated(entries, fromPath, toPath) { const relocated = []; for (const entry of entries) { const path = entry.kind === "directory" && !entry.path.endsWith("/") ? `${entry.path}/` : entry.path; const affected = path === fromPath || (fromPath.endsWith("/") && path.startsWith(fromPath)); if (!affected) { relocated.push(entry); continue; } if (toPath === undefined) { continue; } const nextPath = `${toPath}${path.slice(fromPath.length)}`; relocated.push({ ...entry, path: nextPath, url: `file://${nextPath.split("/").map(encodeURIComponent).join("/")}`, name: nextPath.replace(/\/+$/, "").split("/").pop() || entry.name }); } return relocated; } FileBookmarks.withRelocated = withRelocated; async function relocate(fromPath, toPath) { for (const key of [FileBookmarks.RECENTS_STORAGE_KEY, FileBookmarks.FAVORITES_STORAGE_KEY]) { const current = normalizeEntries(await storageGet(key)); const next = withRelocated(current, fromPath, toPath); if (JSON.stringify(next) !== JSON.stringify(current)) { await storageSet(key, next); } } } FileBookmarks.relocate = relocate; async function readRecents() { return normalizeEntries(await storageGet(FileBookmarks.RECENTS_STORAGE_KEY)); } FileBookmarks.readRecents = readRecents; async function readFavorites() { return normalizeEntries(await storageGet(FileBookmarks.FAVORITES_STORAGE_KEY)); } FileBookmarks.readFavorites = readFavorites; async function recordRecent(entry) { const normalized = normalizeEntry(entry); if (!normalized) { return; } await storageSet(FileBookmarks.RECENTS_STORAGE_KEY, withRecent(await readRecents(), normalized, Date.now())); } FileBookmarks.recordRecent = recordRecent; async function isFavorite(url) { return hasEntry(await readFavorites(), url); } FileBookmarks.isFavorite = isFavorite; async function toggleFavorite(entry) { const normalized = normalizeEntry(entry); if (!normalized) { return false; } const current = await readFavorites(); const toggled = withFavoriteToggled(current, normalized, Date.now()); const written = await storageSet(FileBookmarks.FAVORITES_STORAGE_KEY, toggled.entries); return written ? toggled.favorite : hasEntry(current, normalized.url); } FileBookmarks.toggleFavorite = toggleFavorite; function onChange(listener) { const changedEvent = chromeStorageApi()?.onChanged; const addListener = changedEvent?.addListener; if (!changedEvent || !addListener) { return () => undefined; } const storageListener = (changes, areaName) => { if (areaName !== "local" || !(FileBookmarks.RECENTS_STORAGE_KEY in changes || FileBookmarks.FAVORITES_STORAGE_KEY in changes)) { return; } try { listener(); } catch { } }; try { addListener.call(changedEvent, storageListener); } catch { return () => undefined; } return () => { try { changedEvent.removeListener?.call(changedEvent, storageListener); } catch { } }; } FileBookmarks.onChange = onChange; function isFileUrl(url) { try { return new URL(url).protocol === "file:"; } catch { return false; } } function storageGet(key) { const storage = chromeStorageApi()?.local; const get = storage?.get; if (!storage || !get) { return Promise.resolve(undefined); } return new Promise((resolve) => { try { get.call(storage, key, (result) => { resolve(chromeRuntimeLastError() ? undefined : result?.[key]); }); } catch { resolve(undefined); } }); } function storageSet(key, value) { const storage = chromeStorageApi()?.local; const set = storage?.set; if (!storage || !set) { return Promise.resolve(false); } return new Promise((resolve) => { try { set.call(storage, { [key]: value }, () => { resolve(!chromeRuntimeLastError()); }); } catch { resolve(false); } }); } function chromeStorageApi() { return globalThis.chrome?.storage; } function chromeRuntimeLastError() { return globalThis.chrome?.runtime?.lastError ?.message; } })(FileBookmarks = TabLocalFiles.FileBookmarks || (TabLocalFiles.FileBookmarks = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var TabLocalWorkspace; (function (TabLocalWorkspace) { TabLocalWorkspace.ROOT_PATH = ""; function isAvailable() { return Boolean(chromeApi().tabLocalWorkspace); } TabLocalWorkspace.isAvailable = isAvailable; async function createWorkspace(rootUrl, options) { return call((api, callback) => api.createWorkspace(rootUrl, options, callback)); } TabLocalWorkspace.createWorkspace = createWorkspace; async function createClient(rootUrl, options) { return bindWorkspace(await createWorkspace(rootUrl, options)); } TabLocalWorkspace.createClient = createClient; async function getWorkspaceInfo(token) { return call((api, callback) => api.getWorkspaceInfo(token, callback)); } TabLocalWorkspace.getWorkspaceInfo = getWorkspaceInfo; async function createHtmlPreview(sourceUrl, html) { return call((api, callback) => api.createHtmlPreview(sourceUrl, html, callback)); } TabLocalWorkspace.createHtmlPreview = createHtmlPreview; async function updateHtmlPreview(token, html) { return call((api, callback) => api.updateHtmlPreview(token, html, callback)); } TabLocalWorkspace.updateHtmlPreview = updateHtmlPreview; async function releaseHtmlPreview(token) { return call((api, callback) => api.releaseHtmlPreview(token, callback)); } TabLocalWorkspace.releaseHtmlPreview = releaseHtmlPreview; function bindWorkspace(info) { let currentInfo = info; return { get info() { return currentInfo; }, async refreshInfo() { currentInfo = await getWorkspaceInfo(currentInfo.token); return currentInfo; }, listDirectory(path = TabLocalWorkspace.ROOT_PATH, options) { return listDirectory(currentInfo.token, path, options); }, getEntryInfo(path) { return getEntryInfo(currentInfo.token, path); }, readFile(path, options) { return readFile(currentInfo.token, path, options); }, readTextFile(path, options) { return readTextFile(currentInfo.token, path, options); }, writeFile(path, data, options) { return writeFile(currentInfo.token, path, data, options); }, writeTextFile(path, text, options) { return writeTextFile(currentInfo.token, path, text, options); }, createFile(path) { return createFile(currentInfo.token, path); }, createDirectory(path) { return createDirectory(currentInfo.token, path); }, deleteEntry(path, options) { return deleteEntry(currentInfo.token, path, options); }, moveEntry(fromPath, toPath, options) { return moveEntry(currentInfo.token, fromPath, toPath, options); }, revealInFileManager(path) { return revealInFileManager(currentInfo.token, path); }, release() { return releaseWorkspace(currentInfo.token); } }; } TabLocalWorkspace.bindWorkspace = bindWorkspace; async function listDirectory(token, path = TabLocalWorkspace.ROOT_PATH, options) { return call((api, callback) => api.listDirectory(token, path, options, callback)); } TabLocalWorkspace.listDirectory = listDirectory; async function getEntryInfo(token, path) { return call((api, callback) => api.getEntryInfo(token, path, callback)); } TabLocalWorkspace.getEntryInfo = getEntryInfo; async function readFile(token, path, options) { return call((api, callback) => api.readFile(token, path, options, callback)); } TabLocalWorkspace.readFile = readFile; async function readTextFile(token, path, options) { return new TextDecoder().decode(await readFile(token, path, options)); } TabLocalWorkspace.readTextFile = readTextFile; async function writeFile(token, path, data, options) { return call((api, callback) => api.writeFile(token, path, data, options, callback)); } TabLocalWorkspace.writeFile = writeFile; async function writeTextFile(token, path, text, options) { return writeFile(token, path, new TextEncoder().encode(text).buffer, options); } TabLocalWorkspace.writeTextFile = writeTextFile; async function createFile(token, path) { return call((api, callback) => api.createFile(token, path, callback)); } TabLocalWorkspace.createFile = createFile; async function createDirectory(token, path) { return call((api, callback) => api.createDirectory(token, path, callback)); } TabLocalWorkspace.createDirectory = createDirectory; async function deleteEntry(token, path, options) { return call((api, callback) => api.deleteEntry(token, path, options, callback)); } TabLocalWorkspace.deleteEntry = deleteEntry; async function moveEntry(token, fromPath, toPath, options) { return call((api, callback) => api.moveEntry(token, fromPath, toPath, options, callback)); } TabLocalWorkspace.moveEntry = moveEntry; async function revealInFileManager(token, path) { return call((api, callback) => api.revealInFileManager(token, path, callback)); } TabLocalWorkspace.revealInFileManager = revealInFileManager; async function releaseWorkspace(token) { return call((api, callback) => api.releaseWorkspace(token, callback)); } TabLocalWorkspace.releaseWorkspace = releaseWorkspace; function call(invoke) { const chrome = chromeApi(); const workspaceApi = chrome.tabLocalWorkspace; if (!workspaceApi) { return Promise.reject(new Error("chrome.tabLocalWorkspace is unavailable")); } return new Promise((resolve, reject) => { try { invoke(workspaceApi, (result) => { const error = chrome.runtime?.lastError?.message; if (error) { reject(new Error(error)); return; } resolve(result); }); } catch (error) { reject(error); } }); } function chromeApi() { return (globalThis.chrome || {}); } })(TabLocalWorkspace = TabLocalFiles.TabLocalWorkspace || (TabLocalFiles.TabLocalWorkspace = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ZipArchive; (function (ZipArchive) { ZipArchive.SEPARATOR = "!/"; ZipArchive.ARCHIVE_EXTENSIONS = [ ".zip", ".jar", ".apk", ".crx", ".epub", ".skill", ".xpi", ".vsix", ".whl", ".nupkg", ".odt", ".ods", ".odp" ]; const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50; const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50; const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50; const ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06064b50; const ZIP64_LOCATOR_SIGNATURE = 0x07064b50; const EMPTY_ARCHIVE_SIGNATURE = 0x06054b50; const SPANNED_ARCHIVE_SIGNATURE = 0x08074b50; const END_OF_CENTRAL_DIRECTORY_SIZE = 22; const ZIP64_LOCATOR_SIZE = 20; const LOCAL_FILE_HEADER_SIZE = 30; const CENTRAL_DIRECTORY_HEADER_SIZE = 46; const MAX_COMMENT_SIZE = 65535; const ZIP64_RECORD_SEARCH_SIZE = 65536; const FLAG_ENCRYPTED = 0x0001; const FLAG_UTF8_NAMES = 0x0800; const METHOD_STORE = 0; const METHOD_DEFLATE = 8; const ZIP64_SENTINEL_32 = 0xffffffff; const ZIP64_SENTINEL_16 = 0xffff; const ZIP64_EXTRA_ID = 0x0001; const UNICODE_PATH_EXTRA_ID = 0x7075; const MSDOS_DIRECTORY_ATTRIBUTE = 0x10; ZipArchive.MAX_ENTRY_BYTES = 64 * 1024 * 1024; ZipArchive.MAX_ENTRY_COUNT = 50000; const COMPRESSION_RATIO_LIMIT = 200; const COMPRESSION_RATIO_FLOOR_BYTES = 16 * 1024 * 1024; function errorCode(error) { return error?.tlfZipCode; } ZipArchive.errorCode = errorCode; function zipError(code, message) { const error = new Error(message); error.tlfZipCode = code; return error; } function bufferSource(buffer) { const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); return { size: bytes.length, read(offset, length) { const start = clamp(Math.floor(offset), 0, bytes.length); const end = clamp(start + Math.max(0, Math.floor(length)), start, bytes.length); return Promise.resolve(bytes.subarray(start, end)); } }; } ZipArchive.bufferSource = bufferSource; async function looksLikeArchive(source) { if (source.size < END_OF_CENTRAL_DIRECTORY_SIZE) { return false; } const head = await source.read(0, 4); if (head.length === 4) { const signature = viewOf(head).getUint32(0, true); if (signature === LOCAL_FILE_HEADER_SIGNATURE || signature === EMPTY_ARCHIVE_SIGNATURE || signature === SPANNED_ARCHIVE_SIGNATURE) { return true; } } try { await locateEndOfCentralDirectory(source); return true; } catch { return false; } } ZipArchive.looksLikeArchive = looksLikeArchive; async function readCentralDirectory(source) { const end = await locateEndOfCentralDirectory(source); const layout = await resolveDirectoryLayout(source, end); const block = await source.read(layout.centralDirectoryStart, layout.centralDirectorySize); const view = viewOf(block); const entries = []; let skippedUnsafeCount = 0; let totalCompressedSize = 0; let totalUncompressedSize = 0; let truncated = false; let offset = 0; for (let index = 0; index < layout.entryCount; index += 1) { if (entries.length >= ZipArchive.MAX_ENTRY_COUNT) { truncated = true; break; } if (offset + CENTRAL_DIRECTORY_HEADER_SIZE > block.length) { truncated = true; break; } if (view.getUint32(offset, true) !== CENTRAL_DIRECTORY_SIGNATURE) { throw zipError("corrupt", "Invalid ZIP central directory header"); } const flags = view.getUint16(offset + 8, true); const method = view.getUint16(offset + 10, true); const modifiedTime = view.getUint16(offset + 12, true); const modifiedDate = view.getUint16(offset + 14, true); const crc = view.getUint32(offset + 16, true); const rawCompressedSize = view.getUint32(offset + 20, true); const rawUncompressedSize = view.getUint32(offset + 24, true); const nameLength = view.getUint16(offset + 28, true); const extraLength = view.getUint16(offset + 30, true); const commentLength = view.getUint16(offset + 32, true); const externalAttributes = view.getUint32(offset + 38, true); const rawLocalHeaderOffset = view.getUint32(offset + 42, true); const nameStart = offset + CENTRAL_DIRECTORY_HEADER_SIZE; const extraStart = nameStart + nameLength; const commentStart = extraStart + extraLength; const nextOffset = commentStart + commentLength; if (nextOffset > block.length) { truncated = true; break; } const rawName = block.subarray(nameStart, extraStart); const extra = parseExtraFields(block.subarray(extraStart, commentStart), { uncompressedSize: rawUncompressedSize === ZIP64_SENTINEL_32, compressedSize: rawCompressedSize === ZIP64_SENTINEL_32, localHeaderOffset: rawLocalHeaderOffset === ZIP64_SENTINEL_32 }); const decodedName = decodeEntryName(rawName, flags, extra); const normalized = normalizeEntryPath(decodedName, externalAttributes); offset = nextOffset; if (normalized.unsafe) { skippedUnsafeCount += 1; continue; } const compressedSize = extra.compressedSize ?? rawCompressedSize; const uncompressedSize = extra.uncompressedSize ?? rawUncompressedSize; const encrypted = (flags & FLAG_ENCRYPTED) !== 0; entries.push({ path: normalized.path, name: normalized.name, isDirectory: normalized.isDirectory, method, compressedSize, uncompressedSize, crc32: crc, localHeaderOffset: (extra.localHeaderOffset ?? rawLocalHeaderOffset) + layout.prependedBytes, encrypted, supported: !encrypted && (method === METHOD_STORE || method === METHOD_DEFLATE), mtime: dosTimestampToEpochMs(modifiedDate, modifiedTime) }); if (!normalized.isDirectory) { totalCompressedSize += compressedSize; totalUncompressedSize += uncompressedSize; } } return { entries, prependedBytes: layout.prependedBytes, skippedUnsafeCount, truncated, totalCompressedSize, totalUncompressedSize }; } ZipArchive.readCentralDirectory = readCentralDirectory; async function readEntryBytes(source, entry) { if (entry.isDirectory) { throw zipError("not-found", "ZIP directory entries have no content"); } if (entry.encrypted) { throw zipError("encrypted", "Encrypted ZIP entries cannot be previewed"); } if (entry.method !== METHOD_STORE && entry.method !== METHOD_DEFLATE) { throw zipError("unsupported-method", `Unsupported ZIP compression method: ${entry.method}`); } if (entry.uncompressedSize > ZipArchive.MAX_ENTRY_BYTES) { throw zipError("too-large", "ZIP entry exceeds the preview budget"); } if (entry.uncompressedSize > COMPRESSION_RATIO_FLOOR_BYTES && entry.compressedSize > 0 && entry.uncompressedSize / entry.compressedSize > COMPRESSION_RATIO_LIMIT) { throw zipError("ratio", "ZIP entry compression ratio exceeds the safety limit"); } const header = await source.read(entry.localHeaderOffset, LOCAL_FILE_HEADER_SIZE); if (header.length < LOCAL_FILE_HEADER_SIZE) { throw zipError("corrupt", "Truncated ZIP local file header"); } const headerView = viewOf(header); if (headerView.getUint32(0, true) !== LOCAL_FILE_HEADER_SIGNATURE) { throw zipError("corrupt", "Invalid ZIP local file header"); } const nameLength = headerView.getUint16(26, true); const extraLength = headerView.getUint16(28, true); const dataOffset = entry.localHeaderOffset + LOCAL_FILE_HEADER_SIZE + nameLength + extraLength; const compressed = await source.read(dataOffset, entry.compressedSize); if (compressed.length < entry.compressedSize) { throw zipError("corrupt", "Truncated ZIP entry data"); } const bytes = entry.method === METHOD_STORE ? copyBytes(compressed) : await inflateRaw(compressed); if (entry.crc32 !== 0 && crc32(bytes) !== entry.crc32) { throw zipError("corrupt", "ZIP entry failed its CRC check"); } return bytes; } ZipArchive.readEntryBytes = readEntryBytes; function childEntries(directory, entryPath) { const prefix = entryPath ? normalizeDirectoryPath(entryPath) : ""; const directories = new Map(); const files = []; directory.entries.forEach((entry) => { if (entry.path === prefix || !entry.path.startsWith(prefix)) { return; } const remainder = entry.path.slice(prefix.length); if (!remainder) { return; } const separatorIndex = remainder.indexOf("/"); if (separatorIndex >= 0) { const name = remainder.slice(0, separatorIndex + 1); const path = `${prefix}${name}`; if (!directories.has(path)) { directories.set(path, { name, path, kind: "directory" }); } return; } files.push({ name: remainder, path: entry.path, kind: "file", size: entry.uncompressedSize, entry }); }); const sortByName = (left, right) => left.name.localeCompare(right.name); return [...Array.from(directories.values()).sort(sortByName), ...files.sort(sortByName)]; } ZipArchive.childEntries = childEntries; function findEntry(directory, entryPath) { return directory.entries.find((entry) => entry.path === entryPath); } ZipArchive.findEntry = findEntry; function isArchivePath(decodedPath) { if (isVirtualPath(decodedPath)) { return false; } const normalized = decodedPath.toLowerCase(); return ZipArchive.ARCHIVE_EXTENSIONS.some((extension) => normalized.endsWith(extension)); } ZipArchive.isArchivePath = isArchivePath; function isVirtualPath(decodedPath) { return decodedPath.includes(ZipArchive.SEPARATOR); } ZipArchive.isVirtualPath = isVirtualPath; function splitVirtualPath(decodedPath) { const index = decodedPath.indexOf(ZipArchive.SEPARATOR); if (index < 0) { return undefined; } return { archivePath: decodedPath.slice(0, index), entryPath: decodedPath.slice(index + ZipArchive.SEPARATOR.length) }; } ZipArchive.splitVirtualPath = splitVirtualPath; function joinVirtualPath(archivePath, entryPath) { return `${archivePath}${ZipArchive.SEPARATOR}${entryPath}`; } ZipArchive.joinVirtualPath = joinVirtualPath; function normalizeDirectoryPath(entryPath) { if (!entryPath) { return ""; } return entryPath.endsWith("/") ? entryPath : `${entryPath}/`; } ZipArchive.normalizeDirectoryPath = normalizeDirectoryPath; async function locateEndOfCentralDirectory(source) { const tailSize = Math.min(source.size, END_OF_CENTRAL_DIRECTORY_SIZE + MAX_COMMENT_SIZE); if (tailSize < END_OF_CENTRAL_DIRECTORY_SIZE) { throw zipError("corrupt", "File is too small to be a ZIP archive"); } const tailOffset = source.size - tailSize; const tail = await source.read(tailOffset, tailSize); const view = viewOf(tail); for (let index = tail.length - END_OF_CENTRAL_DIRECTORY_SIZE; index >= 0; index -= 1) { if (view.getUint32(index, true) !== END_OF_CENTRAL_DIRECTORY_SIGNATURE) { continue; } const commentLength = view.getUint16(index + 20, true); if (index + END_OF_CENTRAL_DIRECTORY_SIZE + commentLength !== tail.length) { continue; } return { absoluteOffset: tailOffset + index, view, offset: index }; } throw zipError("corrupt", "ZIP end of central directory not found"); } async function resolveDirectoryLayout(source, end) { let entryCount = end.view.getUint16(end.offset + 10, true); let centralDirectorySize = end.view.getUint32(end.offset + 12, true); let declaredOffset = end.view.getUint32(end.offset + 16, true); let anchorAbsoluteOffset = end.absoluteOffset; const needsZip64 = entryCount === ZIP64_SENTINEL_16 || centralDirectorySize === ZIP64_SENTINEL_32 || declaredOffset === ZIP64_SENTINEL_32; if (needsZip64) { const zip64 = await readZip64Tail(source, end.absoluteOffset); if (!zip64) { throw zipError("corrupt", "ZIP64 end of central directory record not found"); } entryCount = zip64.entryCount; centralDirectorySize = zip64.centralDirectorySize; declaredOffset = zip64.centralDirectoryOffset; anchorAbsoluteOffset = zip64.recordAbsoluteOffset; } const centralDirectoryStart = await resolveCentralDirectoryStart(source, declaredOffset, centralDirectorySize, anchorAbsoluteOffset); return { entryCount, centralDirectorySize, centralDirectoryStart, prependedBytes: Math.max(0, centralDirectoryStart - declaredOffset) }; } async function readZip64Tail(source, endAbsoluteOffset) { if (endAbsoluteOffset < ZIP64_LOCATOR_SIZE) { return undefined; } const locatorOffset = endAbsoluteOffset - ZIP64_LOCATOR_SIZE; const locator = await source.read(locatorOffset, ZIP64_LOCATOR_SIZE); if (locator.length < ZIP64_LOCATOR_SIZE || viewOf(locator).getUint32(0, true) !== ZIP64_LOCATOR_SIGNATURE) { return undefined; } const declaredRecordOffset = readUint64(viewOf(locator), 8); const recordAbsoluteOffset = (await validateZip64Record(source, declaredRecordOffset)) ?? (await searchZip64Record(source, locatorOffset)); if (recordAbsoluteOffset === undefined) { return undefined; } const record = await source.read(recordAbsoluteOffset, 56); if (record.length < 56) { return undefined; } const recordView = viewOf(record); return { entryCount: readUint64(recordView, 32), centralDirectorySize: readUint64(recordView, 40), centralDirectoryOffset: readUint64(recordView, 48), recordAbsoluteOffset }; } async function validateZip64Record(source, offset) { if (offset < 0 || offset + 4 > source.size) { return undefined; } const probe = await source.read(offset, 4); if (probe.length < 4 || viewOf(probe).getUint32(0, true) !== ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE) { return undefined; } return offset; } async function searchZip64Record(source, locatorOffset) { const windowSize = Math.min(locatorOffset, ZIP64_RECORD_SEARCH_SIZE); if (windowSize < 4) { return undefined; } const windowStart = locatorOffset - windowSize; const block = await source.read(windowStart, windowSize); const view = viewOf(block); for (let index = block.length - 4; index >= 0; index -= 1) { if (view.getUint32(index, true) === ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE) { return windowStart + index; } } return undefined; } async function resolveCentralDirectoryStart(source, declaredOffset, centralDirectorySize, anchorAbsoluteOffset) { const anchoredOffset = anchorAbsoluteOffset - centralDirectorySize; const candidates = anchoredOffset === declaredOffset ? [declaredOffset] : [declaredOffset, anchoredOffset]; for (const candidate of candidates) { if (candidate < 0 || candidate + 4 > source.size) { continue; } const probe = await source.read(candidate, 4); if (probe.length === 4 && viewOf(probe).getUint32(0, true) === CENTRAL_DIRECTORY_SIGNATURE) { return candidate; } } if (centralDirectorySize === 0 && anchoredOffset >= 0) { return anchoredOffset; } throw zipError("corrupt", "ZIP central directory not found"); } function parseExtraFields(extra, zip64Fields) { const fields = {}; if (extra.length < 4) { return fields; } const view = viewOf(extra); let offset = 0; while (offset + 4 <= extra.length) { const id = view.getUint16(offset, true); const size = view.getUint16(offset + 2, true); const payloadStart = offset + 4; const payloadEnd = payloadStart + size; if (payloadEnd > extra.length) { break; } if (id === ZIP64_EXTRA_ID) { let cursor = payloadStart; if (zip64Fields.uncompressedSize && cursor + 8 <= payloadEnd) { fields.uncompressedSize = readUint64(view, cursor); cursor += 8; } if (zip64Fields.compressedSize && cursor + 8 <= payloadEnd) { fields.compressedSize = readUint64(view, cursor); cursor += 8; } if (zip64Fields.localHeaderOffset && cursor + 8 <= payloadEnd) { fields.localHeaderOffset = readUint64(view, cursor); cursor += 8; } } else if (id === UNICODE_PATH_EXTRA_ID && size > 5 && view.getUint8(payloadStart) === 1) { fields.unicodePathCrc = view.getUint32(payloadStart + 1, true); fields.unicodePath = decodeWithEncoding(extra.subarray(payloadStart + 5, payloadEnd), "utf-8"); } offset = payloadEnd; } return fields; } function decodeEntryName(rawName, flags, extra) { if (extra.unicodePath !== undefined && extra.unicodePathCrc === crc32(rawName)) { return extra.unicodePath; } if ((flags & FLAG_UTF8_NAMES) !== 0) { return decodeWithEncoding(rawName, "utf-8") || ""; } const strictUtf8 = decodeWithEncoding(rawName, "utf-8", true); if (strictUtf8 !== undefined) { return strictUtf8; } return decodeWithEncoding(rawName, "gb18030") ?? decodeWithEncoding(rawName, "utf-8") ?? ""; } function normalizeEntryPath(name, externalAttributes) { const normalized = name.replace(/\\/g, "/").replace(/^\/+/, ""); const trailingSlash = normalized.endsWith("/"); const segments = normalized.split("/").filter((segment) => segment.length > 0); const resolved = []; for (const segment of segments) { if (segment === ".") { continue; } if (segment === "..") { return { path: "", name: "", isDirectory: false, unsafe: true }; } resolved.push(segment); } if (!resolved.length) { return { path: "", name: "", isDirectory: true, unsafe: true }; } const isDirectory = trailingSlash || (externalAttributes & MSDOS_DIRECTORY_ATTRIBUTE) !== 0; const joined = resolved.join("/"); return { path: isDirectory ? `${joined}/` : joined, name: isDirectory ? `${resolved[resolved.length - 1]}/` : resolved[resolved.length - 1], isDirectory, unsafe: false }; } async function inflateRaw(data) { const decompressionStream = globalThis.DecompressionStream; if (!decompressionStream) { throw zipError("unsupported-method", "DecompressionStream is not available"); } try { const bytes = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); const stream = new Blob([bytes]).stream().pipeThrough(new decompressionStream("deflate-raw")); return new Uint8Array(await new Response(stream).arrayBuffer()); } catch { throw zipError("corrupt", "Failed to inflate ZIP entry"); } } function decodeWithEncoding(bytes, encoding, fatal = false) { try { return new TextDecoder(encoding, { fatal }).decode(bytes); } catch { return undefined; } } function dosTimestampToEpochMs(date, time) { if (!date) { return undefined; } const year = 1980 + ((date >> 9) & 0x7f); const month = ((date >> 5) & 0x0f) - 1; const day = date & 0x1f; const hours = (time >> 11) & 0x1f; const minutes = (time >> 5) & 0x3f; const seconds = (time & 0x1f) * 2; const timestamp = new Date(year, month, day, hours, minutes, seconds).getTime(); return Number.isFinite(timestamp) ? timestamp : undefined; } function readUint64(view, offset) { const value = view.getBigUint64(offset, true); if (value > BigInt(Number.MAX_SAFE_INTEGER)) { throw zipError("too-large", "ZIP field exceeds the addressable range"); } return Number(value); } function viewOf(bytes) { return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); } function copyBytes(bytes) { return new Uint8Array(bytes); } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } const CRC_TABLE = createCrcTable(); function createCrcTable() { const table = []; for (let value = 0; value < 256; value += 1) { let crc = value; for (let bit = 0; bit < 8; bit += 1) { crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1; } table[value] = crc >>> 0; } return table; } function crc32(bytes) { let crc = 0xffffffff; for (let index = 0; index < bytes.length; index += 1) { crc = CRC_TABLE[(crc ^ bytes[index]) & 0xff] ^ (crc >>> 8); } return (crc ^ 0xffffffff) >>> 0; } })(ZipArchive = TabLocalFiles.ZipArchive || (TabLocalFiles.ZipArchive = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var PdfRequest; (function (PdfRequest) { function queryValue(name, search = window.location.search) { const encodedName = encodeURIComponent(name); const entry = (search || "") .replace(/^\?/, "") .split("&") .find((part) => part === encodedName || part.startsWith(`${encodedName}=`)); if (!entry) { return undefined; } const separator = entry.indexOf("="); if (separator < 0) { return ""; } try { return decodeURIComponent(entry.slice(separator + 1).replace(/\+/g, " ")); } catch { return undefined; } } PdfRequest.queryValue = queryValue; })(PdfRequest = TabLocalFiles.PdfRequest || (TabLocalFiles.PdfRequest = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var PdfMemory; (function (PdfMemory) { const MEMORY_PARAMETER = "tlf-pdf-memory"; const CHANNEL_PARAMETER = "channel"; const TAB_ID_PARAMETER = "tab-id"; const MEMORY_SOURCE_URL = "memory://pdf/current.pdf"; const SNAPSHOT_WARNING_BYTES = 50 * 1024 * 1024; PdfMemory.MAX_SNAPSHOT_BYTES = 100 * 1024 * 1024; const MAX_OCR_TEXT_BYTES = 64 * 1024; const OCR_TIMEOUT_MS = 30000; const MAX_CACHED_TEXT_PAGES = 8; const SNAPSHOT_WARNING_MESSAGE = "tabbit-pdf-memory-warning"; const SNAPSHOT_CONFIRM_MESSAGE = "tabbit-pdf-memory-confirm"; let snapshotPromise; let currentSnapshot; let currentApi; let currentTabId; let extractedTextByPage = new Map(); const pendingTextRequests = new Map(); const nativeTextRequests = new Map(); function isRequest(search = window.location.search) { return TabLocalFiles.PdfRequest.queryValue(MEMORY_PARAMETER, search) === "1"; } PdfMemory.isRequest = isRequest; function sourceUrl(search = window.location.search) { const channel = TabLocalFiles.PdfRequest.queryValue(CHANNEL_PARAMETER, search) || "current"; const url = new URL(MEMORY_SOURCE_URL); url.searchParams.set(CHANNEL_PARAMETER, channel); return url.toString(); } PdfMemory.sourceUrl = sourceUrl; function isSourceUrl(value) { try { const url = new URL(value); return url.protocol === "memory:" && url.hostname === "pdf" && url.pathname === "/current.pdf"; } catch { return false; } } PdfMemory.isSourceUrl = isSourceUrl; async function readSnapshot() { if (!isRequest()) { throw new Error("The current Previewer page is not a PDF memory request"); } snapshotPromise || (snapshotPromise = loadSnapshot().catch((error) => { snapshotPromise = undefined; safeLog("failed", { error: error instanceof Error ? error.message : String(error) }, true); throw error; })); return snapshotPromise; } PdfMemory.readSnapshot = readSnapshot; function currentPage(source) { return isSourceUrl(source) ? currentSnapshot?.currentPage : undefined; } PdfMemory.currentPage = currentPage; function extractedPageText(pageNumber) { return extractedTextByPage.get(pageNumber - 1); } PdfMemory.extractedPageText = extractedPageText; function requestPageText(pageNumber) { const pageIndex = pageNumber - 1; if (!Number.isInteger(pageIndex) || pageIndex < 0 || pageIndex >= (currentSnapshot?.pageCount ?? 0)) { return Promise.resolve(undefined); } if (extractedTextByPage.has(pageIndex)) { const text = extractedTextByPage.get(pageIndex); extractedTextByPage.delete(pageIndex); extractedTextByPage.set(pageIndex, text); return Promise.resolve(text); } const pending = pendingTextRequests.get(pageIndex); if (pending) { return pending.promise; } if (!currentApi || currentTabId === undefined) { return Promise.resolve(undefined); } let resolve; let reject; const promise = new Promise((onResolve, onReject) => { resolve = onResolve; reject = onReject; }); const request = { pageIndex, promise, resolve, reject, cancelled: false }; pendingTextRequests.set(pageIndex, request); void runTextRequest(currentApi, currentTabId, request); return promise; } PdfMemory.requestPageText = requestPageText; function nativePageText(api, tabId, pageIndex) { const pending = nativeTextRequests.get(pageIndex); if (pending) { return pending; } const promise = extractPdfTextWithOcr(api, tabId, pageIndex); nativeTextRequests.set(pageIndex, promise); const release = () => { nativeTextRequests.delete(pageIndex); }; promise.then(release, release); return promise; } async function runTextRequest(api, tabId, request) { const pageIndex = request.pageIndex; try { const result = await nativePageText(api, tabId, pageIndex); if (request.cancelled) { return; } if (result.ocrTimedOut) { throw new Error("Timed out waiting for PDF OCR"); } if (result.truncated || pdfTextResultBytes(result) > MAX_OCR_TEXT_BYTES) { throw new Error("PDF page text exceeds the safe translation limit"); } const page = result.pages?.find((candidate, index) => { const candidateIndex = Number.isInteger(candidate.pageIndex) ? candidate.pageIndex : pageIndex + index; return candidateIndex === pageIndex; }); const text = page?.text?.trim() || ""; if (text) { extractedTextByPage.set(pageIndex, text); while (extractedTextByPage.size > MAX_CACHED_TEXT_PAGES) { extractedTextByPage.delete(extractedTextByPage.keys().next().value); } } safeLog("page text ready", { pageNumber: pageIndex + 1, characters: text.length }); request.resolve(text || undefined); } catch (error) { if (!request.cancelled) { request.reject(error); } } finally { if (pendingTextRequests.get(pageIndex) === request) { pendingTextRequests.delete(pageIndex); } } } function cancelPageText(pageNumber) { const pageIndex = pageNumber - 1; const request = pendingTextRequests.get(pageIndex); if (request) { request.cancelled = true; request.resolve(undefined); pendingTextRequests.delete(pageIndex); } } PdfMemory.cancelPageText = cancelPageText; function cancelPendingTextRequests() { for (const pageIndex of pendingTextRequests.keys()) { cancelPageText(pageIndex + 1); } } PdfMemory.cancelPendingTextRequests = cancelPendingTextRequests; async function notifyReady(source) { if (isSourceUrl(source)) { notifyParent("tabbit-pdf-memory-ready"); } } PdfMemory.notifyReady = notifyReady; function notifyError(source, error) { if (isSourceUrl(source)) { notifyParent("tabbit-pdf-memory-error", error instanceof Error ? error.message : String(error)); } } PdfMemory.notifyError = notifyError; async function loadSnapshot() { const api = chromeApi(); const tabId = await getCurrentTabId(api); safeLog("started", { tabId }); const info = validateSnapshotInfo(await getCurrentPdfSnapshotInfo(api)); if (info.byteLength > PdfMemory.MAX_SNAPSHOT_BYTES) { throw new Error("PDF exceeds the 100 MiB translation limit"); } if (info.byteLength > SNAPSHOT_WARNING_BYTES) { await requestLargeSnapshotApproval(info.byteLength); } const snapshot = await getCurrentPdfSnapshot(api); currentSnapshot = validateSnapshot(snapshot); currentApi = api; currentTabId = tabId; extractedTextByPage = new Map(); safeLog("ready", { tabId, bytes: currentSnapshot.data.byteLength, pages: currentSnapshot.pageCount, currentPage: currentSnapshot.currentPage }); return currentSnapshot; } function pdfTextResultBytes(result) { if (typeof result.textBytes === "number" && Number.isInteger(result.textBytes) && result.textBytes >= 0) { return result.textBytes; } return result.pages?.reduce((total, page) => total + (page.text?.length || 0) * 3, 0) || 0; } function extractPdfTextWithOcr(api, tabId, startPage) { const extract = api.tabContentExtractor?.extractPdfTextWithOcr; if (!extract) { return Promise.reject(new Error("PDF OCR API is unavailable")); } return new Promise((resolve, reject) => { extract(tabId, { startPage, maxPages: 1, maxTextBytes: MAX_OCR_TEXT_BYTES, ocrTimeoutMs: OCR_TIMEOUT_MS }, (result) => finishChromeCallback(api, result, resolve, reject)); }); } function getCurrentPdfSnapshot(api) { const getSnapshot = api.tabContentExtractor?.getCurrentPdfSnapshot; if (!getSnapshot) { return Promise.reject(new Error("PDF snapshot API is unavailable")); } return new Promise((resolve, reject) => { getSnapshot((snapshot) => finishChromeCallback(api, snapshot, resolve, reject)); }); } function getCurrentPdfSnapshotInfo(api) { const getInfo = api.tabContentExtractor?.getCurrentPdfSnapshotInfo; if (!getInfo) { return Promise.reject(new Error("PDF snapshot metadata API is unavailable")); } return new Promise((resolve, reject) => { getInfo((info) => finishChromeCallback(api, info, resolve, reject)); }); } function requestLargeSnapshotApproval(byteLength) { const channel = TabLocalFiles.PdfRequest.queryValue(CHANNEL_PARAMETER); if (!channel || window.parent === window) { return Promise.reject(new Error("Unable to confirm large PDF translation")); } return new Promise((resolve, reject) => { const onMessage = (event) => { if (event.source !== window.parent || event.data?.type !== SNAPSHOT_CONFIRM_MESSAGE || event.data?.channel !== channel) { return; } window.removeEventListener("message", onMessage); if (event.data.approved === true) { resolve(); } else { reject(new Error("PDF translation was cancelled")); } }; window.addEventListener("message", onMessage); window.parent.postMessage({ type: SNAPSHOT_WARNING_MESSAGE, channel, byteLength }, "*"); }); } function getCurrentTabId(api) { const rawTabId = TabLocalFiles.PdfRequest.queryValue(TAB_ID_PARAMETER); const queryTabId = rawTabId === undefined ? Number.NaN : Number(rawTabId); if (rawTabId !== "" && Number.isInteger(queryTabId) && queryTabId >= 0) { return Promise.resolve(queryTabId); } const getCurrent = api.tabs?.getCurrent; if (!getCurrent) { return Promise.reject(new Error("Unable to identify the containing PDF tab")); } return new Promise((resolve, reject) => { getCurrent((tab) => { const error = api.runtime?.lastError?.message; if (error) { reject(new Error(error)); return; } if (!Number.isInteger(tab?.id) || (tab?.id ?? -1) < 0) { reject(new Error("Unable to identify the containing PDF tab")); return; } resolve(tab.id); }); }); } function finishChromeCallback(api, value, resolve, reject) { const error = api.runtime?.lastError?.message; if (error) { reject(new Error(error)); return; } resolve(value); } function validateSnapshot(snapshot) { if (!snapshot?.data || typeof snapshot.data.byteLength !== "number" || snapshot.data.byteLength === 0) { throw new Error("PDF snapshot is empty"); } if (snapshot.data.byteLength > PdfMemory.MAX_SNAPSHOT_BYTES) { throw new Error("PDF snapshot exceeds the 100 MiB translation limit"); } if (!Number.isInteger(snapshot.pageCount) || snapshot.pageCount <= 0) { throw new Error("PDF snapshot has an invalid page count"); } if (!Number.isInteger(snapshot.currentPage) || snapshot.currentPage < 0 || snapshot.currentPage >= snapshot.pageCount) { throw new Error("PDF snapshot has an invalid current page"); } return snapshot; } function validateSnapshotInfo(info) { if (!Number.isSafeInteger(info?.byteLength) || info.byteLength <= 0) { throw new Error("PDF snapshot has an invalid byte length"); } if (!Number.isInteger(info.pageCount) || info.pageCount <= 0) { throw new Error("PDF snapshot has an invalid page count"); } return info; } function notifyParent(type, error) { const channel = TabLocalFiles.PdfRequest.queryValue(CHANNEL_PARAMETER); if (!channel || window.parent === window) { return; } window.parent.postMessage({ type, channel, error }, "*"); } function safeLog(event, details, warning = false) { try { const output = warning ? console.warn : console.info; output.call(console, `[Tab Local Files][PDF memory] ${event}`, details); } catch { } } function chromeApi() { return globalThis.chrome || {}; } })(PdfMemory = TabLocalFiles.PdfMemory || (TabLocalFiles.PdfMemory = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var PdfTranslation; (function (PdfTranslation) { PdfTranslation.MODE_ATTRIBUTE = "data-tabbit-pdf-translation-mode"; PdfTranslation.SURFACE_ATTRIBUTE = "data-tabbit-pdf-translation-surface"; PdfTranslation.PAGE_ATTRIBUTE = "data-tlf-pdf-translation-page"; PdfTranslation.PROTOCOL = "tabbit-pdf-translation-v1"; const MODE_QUERY_PARAMETER = "tlf-pdf-translation"; const MEMORY_QUERY_PARAMETER = "tlf-pdf-memory"; const MAX_BLOCKS_PER_REQUEST = 64; const MAX_CHARACTERS_PER_REQUEST = 48000; const REQUEST_TIMEOUT_MS = 100000; const BRIDGE_READY_TIMEOUT_MS = 100000; const MAX_CONCURRENT_BATCH_REQUESTS = 2; class Client { constructor(onResult, onError, onLanguageEpochChange = () => undefined) { this.onResult = onResult; this.onError = onError; this.onLanguageEpochChange = onLanguageEpochChange; this.active = false; this.ready = false; this.disposed = false; this.requestSequence = 0; this.languageEpoch = 0; this.sessionId = ""; this.queuedMessages = []; this.requests = new Map(); this.operations = new Map(); this.targetOrigin = window.location.origin; this.onMessage = (event) => { if (this.disposed || event.source !== window || event.origin !== this.targetOrigin) { return; } const message = event.data; if (!message || message.protocol !== PdfTranslation.PROTOCOL) { return; } if (message.type === "ready") { if (!this.active) { return; } this.updateLanguageEpoch(message.epoch); this.ready = true; window.clearTimeout(this.bridgeReadyTimer); this.bridgeReadyTimer = undefined; this.flushQueue(); return; } if (message.type === "epoch") { if (this.active) { this.updateLanguageEpoch(message.epoch); } return; } if (!this.active || message.sessionId !== this.sessionId || !validPageResponse(message)) { return; } if (message.type === "error" || message.ok === false) { const request = message.requestId ? this.requests.get(message.requestId) : undefined; if (!request) { return; } this.removeRequest(message.requestId); this.failOperation(request.operationKey, matchesRequest(message, request) ? message.error || "PDF translation failed" : "PDF translation response did not match its request"); this.dispatchAvailableBatches(); return; } if (message.type === "prepared") { const request = message.requestId ? this.requests.get(message.requestId) : undefined; if (!request || request.kind !== "prepare") { return; } this.removeRequest(message.requestId); if (!matchesRequest(message, request)) { this.failOperation(request.operationKey, "PDF translation response did not match its request"); this.dispatchAvailableBatches(); return; } this.acceptPreparation(request.operationKey, message); return; } if (message.type === "result" && Array.isArray(message.blocks)) { const request = message.requestId ? this.requests.get(message.requestId) : undefined; if (!request || request.kind !== "translate") { return; } const operation = this.operations.get(request.operationKey); if (!operation) { this.removeRequest(message.requestId); return; } if (!matchesRequest(message, request)) { this.removeRequest(message.requestId); this.failOperation(request.operationKey, "PDF translation response did not match its request"); this.dispatchAvailableBatches(); return; } const validBlocks = message.blocks.filter(validTranslatedBlock); const translatedById = new Map(validBlocks.map((block) => [block.id, block])); const translated = request.blockIds .map((id) => translatedById.get(id)) .filter((block) => Boolean(block)); this.removeRequest(message.requestId); if (validBlocks.length !== request.blockIds.length || translatedById.size !== request.blockIds.length || translated.length !== request.blockIds.length) { this.failOperation(request.operationKey, "PDF translation response was incomplete"); this.dispatchAvailableBatches(); return; } this.completeBatch(operation, translated); } }; this.onPageHide = () => { this.cancel(); }; window.addEventListener("message", this.onMessage); window.addEventListener("pagehide", this.onPageHide); } setActive(active) { if (this.disposed || active === this.active) { return; } if (!active) { this.cancel(); return; } this.active = true; this.ready = false; this.sessionId = createSessionId(); this.post({ protocol: PdfTranslation.PROTOCOL, type: "probe" }); this.bridgeReadyTimer = window.setTimeout(() => { this.failPendingRequests("PDF translation bridge timed out"); }, BRIDGE_READY_TIMEOUT_MS); } translatePage(pageIndex, revision, blocks) { if (!this.active || this.disposed || !blocks.length) { return; } const operationKey = pageOperationKey(pageIndex, revision); if (this.operations.has(operationKey)) { return; } const characters = blocks.reduce((total, block) => total + block.text.length, 0); const ids = new Set(blocks.map(({ id }) => id)); if (blocks.length > MAX_BLOCKS_PER_REQUEST || characters > MAX_CHARACTERS_PER_REQUEST || ids.size !== blocks.length) { this.onError(pageIndex, revision, "PDF translation page exceeds its safe budget"); return; } const operation = { key: operationKey, pageIndex, revision, blocksById: new Map(blocks.map((block) => [block.id, block])), completedIds: new Set(), queuedBatches: [] }; this.operations.set(operationKey, operation); this.sendPageRequest(operation, "prepare", blocks); } cancelPages(pageIndexes) { if (!this.active || this.disposed || !pageIndexes.length) { return; } const cancelledPages = new Set(pageIndexes); const interrupted = Array.from(this.operations.values()) .filter(({ pageIndex }) => cancelledPages.has(pageIndex)); if (!interrupted.length) { return; } this.post({ protocol: PdfTranslation.PROTOCOL, type: "cancel", sessionId: this.sessionId, pageIndexes: Array.from(new Set(interrupted.map(({ pageIndex }) => pageIndex))) }); const interruptedKeys = new Set(interrupted.map(({ key }) => key)); const requestIds = Array.from(this.requests.entries()) .filter(([, request]) => interruptedKeys.has(request.operationKey)) .map(([requestId]) => requestId); requestIds.forEach((requestId) => this.removeRequest(requestId)); const discardedRequests = new Set(requestIds); const retainedMessages = this.queuedMessages.filter((message) => { return !message.requestId || !discardedRequests.has(message.requestId); }); this.queuedMessages.splice(0, this.queuedMessages.length, ...retainedMessages); interrupted.forEach(({ key }) => this.operations.delete(key)); if (this.lastBatchOperationKey && interruptedKeys.has(this.lastBatchOperationKey)) { this.lastBatchOperationKey = undefined; } interrupted.forEach((operation) => { this.onError(operation.pageIndex, operation.revision, "cancelled"); }); this.dispatchAvailableBatches(); } dispose() { if (this.disposed) { return; } this.cancel(); this.disposed = true; window.removeEventListener("message", this.onMessage); window.removeEventListener("pagehide", this.onPageHide); } cancel() { if (this.active && this.sessionId) { this.post({ protocol: PdfTranslation.PROTOCOL, type: "cancel", sessionId: this.sessionId }); } this.active = false; this.ready = false; this.sessionId = ""; this.queuedMessages.length = 0; this.clearRequests(); this.operations.clear(); this.lastBatchOperationKey = undefined; window.clearTimeout(this.bridgeReadyTimer); this.bridgeReadyTimer = undefined; } sendOrQueue(message) { if (!this.ready) { this.queuedMessages.push(message); return; } this.post(message); } flushQueue() { const messages = this.queuedMessages.splice(0); messages.forEach((message) => this.post(message)); } sendPageRequest(operation, kind, blocks) { const requestId = `${this.sessionId}:${operation.pageIndex}:${operation.revision}:${++this.requestSequence}`; this.requests.set(requestId, { kind, operationKey: operation.key, pageIndex: operation.pageIndex, revision: operation.revision, blockIds: blocks.map(({ id }) => id), timeoutId: window.setTimeout(() => this.handleRequestTimeout(requestId), REQUEST_TIMEOUT_MS) }); this.sendOrQueue({ protocol: PdfTranslation.PROTOCOL, type: kind, sessionId: this.sessionId, requestId, pageIndex: operation.pageIndex, revision: operation.revision, blocks }); } acceptPreparation(operationKey, message) { const operation = this.operations.get(operationKey); if (!operation) { return; } const prepared = validatePreparation(message, operation.blocksById); if (!prepared) { this.failOperation(operationKey, "PDF translation preparation response was invalid"); this.dispatchAvailableBatches(); return; } prepared.cachedBlocks.forEach(({ id }) => operation.completedIds.add(id)); operation.queuedBatches = prepared.batches; if (prepared.cachedBlocks.length) { this.emitResult(operation, prepared.cachedBlocks); } if (operation.completedIds.size === operation.blocksById.size) { this.operations.delete(operation.key); return; } this.dispatchAvailableBatches(); } dispatchAvailableBatches() { while (this.activeTranslationRequestCount() < MAX_CONCURRENT_BATCH_REQUESTS) { const operation = this.nextOperationWithBatch(); if (!operation) { return; } const batch = operation.queuedBatches.shift(); if (!batch) { return; } this.lastBatchOperationKey = operation.key; this.sendPageRequest(operation, "translate", batch); } } nextOperationWithBatch() { const operations = Array.from(this.operations.values()); if (!operations.length) { return undefined; } const previousIndex = this.lastBatchOperationKey ? operations.findIndex(({ key }) => key === this.lastBatchOperationKey) : -1; for (let offset = 1; offset <= operations.length; offset += 1) { const operation = operations[(previousIndex + offset) % operations.length]; if (operation.queuedBatches.length) { return operation; } } return undefined; } activeTranslationRequestCount() { return Array.from(this.requests.values()) .filter(({ kind }) => kind === "translate") .length; } completeBatch(operation, blocks) { blocks.forEach(({ id }) => operation.completedIds.add(id)); this.emitResult(operation, blocks); if (operation.completedIds.size === operation.blocksById.size) { this.operations.delete(operation.key); } this.dispatchAvailableBatches(); } emitResult(operation, blocks) { this.onResult({ pageIndex: operation.pageIndex, revision: operation.revision, blocks, completed: operation.completedIds.size, total: operation.blocksById.size, done: operation.completedIds.size === operation.blocksById.size }); } failOperation(operationKey, error) { const operation = this.operations.get(operationKey); if (!operation) { return; } this.operations.delete(operationKey); const requestIds = Array.from(this.requests.entries()) .filter(([, request]) => request.operationKey === operationKey) .map(([requestId]) => requestId); requestIds.forEach((requestId) => this.removeRequest(requestId)); if (requestIds.length) { const discarded = new Set(requestIds); const retained = this.queuedMessages.filter((message) => { return !message.requestId || !discarded.has(message.requestId); }); this.queuedMessages.splice(0, this.queuedMessages.length, ...retained); } this.onError(operation.pageIndex, operation.revision, error); } removeRequest(requestId) { const request = requestId ? this.requests.get(requestId) : undefined; if (!request || !requestId) { return; } window.clearTimeout(request.timeoutId); this.requests.delete(requestId); } handleRequestTimeout(requestId) { const request = this.requests.get(requestId); if (!request || !this.active) { return; } this.post({ protocol: PdfTranslation.PROTOCOL, type: "cancel", sessionId: this.sessionId }); this.failPendingRequests("PDF translation timed out"); this.sessionId = createSessionId(); } failPendingRequests(error) { const operationKeys = Array.from(this.operations.keys()); this.queuedMessages.length = 0; operationKeys.forEach((operationKey) => this.failOperation(operationKey, error)); this.clearRequests(); } clearRequests() { this.requests.forEach(({ timeoutId }) => window.clearTimeout(timeoutId)); this.requests.clear(); } updateLanguageEpoch(value) { if (!Number.isSafeInteger(value) || value <= 0 || value === this.languageEpoch) { return; } const epoch = value; if (this.languageEpoch === 0) { this.languageEpoch = epoch; return; } this.post({ protocol: PdfTranslation.PROTOCOL, type: "cancel", sessionId: this.sessionId }); this.queuedMessages.length = 0; this.clearRequests(); this.operations.clear(); this.lastBatchOperationKey = undefined; this.sessionId = createSessionId(); this.languageEpoch = epoch; this.onLanguageEpochChange(epoch); } post(message) { try { window.postMessage(message, this.targetOrigin); } catch { } } } PdfTranslation.Client = Client; function initializeFromRequest() { if (TabLocalFiles.PdfRequest.queryValue(MODE_QUERY_PARAMETER) !== "1") { return; } document.documentElement.setAttribute(PdfTranslation.MODE_ATTRIBUTE, "true"); if (TabLocalFiles.PdfRequest.queryValue(MEMORY_QUERY_PARAMETER) === "1") { document.documentElement.setAttribute(PdfTranslation.SURFACE_ATTRIBUTE, "native"); } } PdfTranslation.initializeFromRequest = initializeFromRequest; function isActive() { return document.documentElement.getAttribute(PdfTranslation.MODE_ATTRIBUTE) === "true"; } PdfTranslation.isActive = isActive; function syncPageAccessibility(page) { if (isActive()) { page.removeAttribute("aria-hidden"); return; } page.setAttribute("aria-hidden", "true"); } PdfTranslation.syncPageAccessibility = syncPageAccessibility; function validPageResponse(message) { return Number.isSafeInteger(message.pageIndex) && (message.pageIndex ?? -1) >= 0 && Number.isSafeInteger(message.revision) && (message.revision ?? -1) >= 0; } function validBlock(value) { if (!value || typeof value !== "object") { return false; } const block = value; return typeof block.id === "string" && typeof block.text === "string"; } function matchesRequest(message, request) { return message.pageIndex === request.pageIndex && message.revision === request.revision; } function validTranslatedBlock(value) { return validBlock(value) && Boolean(value.text); } function validatePreparation(message, blocksById) { if (!Array.isArray(message.cachedBlocks) || !Array.isArray(message.batches)) { return undefined; } const cachedBlocks = message.cachedBlocks.filter(validBlock); if (cachedBlocks.length !== message.cachedBlocks.length) { return undefined; } const plannedIds = new Set(); for (const block of cachedBlocks) { if (!block.text || !blocksById.has(block.id) || plannedIds.has(block.id)) { return undefined; } plannedIds.add(block.id); } const batches = []; for (const candidate of message.batches) { if (!Array.isArray(candidate) || candidate.length === 0) { return undefined; } const batch = []; for (const id of candidate) { const block = typeof id === "string" ? blocksById.get(id) : undefined; if (!block || plannedIds.has(id)) { return undefined; } plannedIds.add(id); batch.push(block); } batches.push(batch); } if (plannedIds.size !== blocksById.size) { return undefined; } return { cachedBlocks, batches }; } function pageOperationKey(pageIndex, revision) { return `${pageIndex}:${revision}`; } function createSessionId() { try { return crypto.randomUUID(); } catch { return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; } } })(PdfTranslation = TabLocalFiles.PdfTranslation || (TabLocalFiles.PdfTranslation = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var SoftWrap; (function (SoftWrap) { const SHORT_LINE_LENGTH = 25; const CJK_END_PATTERN = /[\u3000-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF00-\uFFEF]$/; function joins(previous, next) { if (/(?: {2,}|\\)$/.test(previous)) { return false; } const before = previous.trim(); const after = next.trim(); if (!/^[a-z]/.test(after) || /[::]$/.test(before) || CJK_END_PATTERN.test(before)) { return false; } return before.length >= SHORT_LINE_LENGTH || after.length >= SHORT_LINE_LENGTH; } SoftWrap.joins = joins; })(SoftWrap = TabLocalFiles.SoftWrap || (TabLocalFiles.SoftWrap = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var HanVariants; (function (HanVariants) { HanVariants.SIMPLIFIED = "万与专业丛东丝丢两严丧个丫丰临为丽举义乌乐乔习乡书买乱争亏亘亚产亩亮亲亵亸亿什仅仆从仑仓仗仪们价众优会伛伞伟传伣伤伥伦伧伪伫体佝" + "佣佥侠侣侥侦侧侨侩侪侬俣俦俨俩俪俫俭债倾偬偻偾偿傥傧储傩儿兑兖党兰关兴兹养兽冁内冈册写军农冢冯冱决况冻凄凉减凑凛凤凫凭凯击凿刍刘" + "则刚创删别刬刭刹刽刿剀剂剐剑剥剧劝办务劢动励劲劳势勋勖勚勤匀匦匮区医华协单卖卢卤卧卫却厂厄厅历厉压厌厍厐厕厘厢厣厦厨厩厮县叁参双" + "变叙叠号叹叽吁同吓吕吗吣吨听启吴呐呒呓呕呖呗员呙呛呜咏咙咛咝咤哄响哑哒哓哔哕哗哙哜哝哟唛唝唠唡唢唤啕啧啬啭啮啰啴啸喂喷喽喾嗫嗳嘘" + "嘤嘱噜噪嚣团园囱围囵国图圆圣圹场坂坏块坚坛坜坝坞坟坠垄垅垆垒垦垩垫垭垱垲垴埘埙埚埯堑堕堤墙壮声壳壶壸处备复够头夸夹夺奁奂奋奖奥妆" + "妇妈妩妪妫姗姹娄娅娆娇娈娘娱娲娴婳婴婵婶媪媾嫒嫔嫱嬷孙学孪宁宝实宠审宪宫宽宾寝寸对寻导寿将尔尘尝尧尴尸尽层屃屉届属屡屦屿岁岂岖岗" + "岘岙岚岛岩岭岳岽岿峄峡峣峤峥峦崂崃崄崭嵘嵚嵝巅巩巯币帅师帏帐帘帜带帧帮帱帻帼幂并广庄庆庐庑库应庙庞废度廪开异弃弑张弥弦弪弯弹强归" + "当录彝彦彻径徕徭忆忏忧忾怀态怂怃怄怅怆怜总怼怿恋恒恤恳恶恸恹恺恻恼恽悦悫悬悭悮悯惊惧惨惩惫惬惭惮惯愠愤愦慑懑懒懔戆戋戏戗战戚戬戮" + "戯户扇扑扣执扩扪扫扬扰折抚抛抟抠抡抢护报担拓拟拢拣拥拦拧拨择挂挚挛挜挝挞挟挠挡挢挣挤挥挦挽捂捆捝捞损捡换捣据捻掳掴掷掸掺掼揽揾揿" + "搀搁搂搅携摄摅摆摇摈摊撄撑撵撷撸撺擞攒敌敛数斋斓斩断旁无旧时旷旸昆昌昙昵昼昽显晋晒晓晔晕晖暂暧术朱杀杂权杆条来杨杩杯杰极构枞枢枣" + "枥枧枨枪枫枭柠查柽栀栅标栈栉栊栋栌栎栏树栖样栾桠桡桢档桤桥桦桧桨桩梦梼梾梿检棁棂椁椟椠椤椭楫楼榄榅榇榈榉榨槚槛槟槠横樯樱橐橥橱橹" + "橼檐檩欢欤欧歼殁殇残殒殓殚殡殴殷毁毂毕毙毡毵氇氢氩氲汇汉汤汹沉沟没沣沤沥沦沧沩沪沾泞泪泶泷泸泺泻泼泽泾洁洒洼浃浅浆浇浈浊测浍济浏" + "浐浑浒浓浔浚涂涛涝涞涟涠涡涣涤润涧涨涩淬渊渌渍渎渐渑渔渗温湾湿溃溅溆滗滚滞滟滠满滢滤滥滦滨滩滪漓漤潆潇潋潍潜潴澜濑濒灏灭灯灵灾灿" + "炀炉炖炜炝炭点炼炽烁烂烃烛烦烧烨烩烫烬热焕焖焘焰煴熏熔爱爷牍牦牵牺犊状犷犸犹狈狝狞独狭狮狯狰狱狲猃猎猕猡猪猫猬献獭玑玚玛玮环现玱" + "玺珐珑珰珲球琅琏琐琼瑶瑷璎瓒瓮瓯电画畅畴疖疗疟疠疡疬疭疮疯疱疴痈痉痒痖痨痪痫瘅瘆瘗瘘瘪瘫瘾瘿癞癣癫皂皑皱皲盏盐监盖盗盘眍眬眯睁睐" + "睑睾瞆瞒瞩矫矶矾矿砀码砖砗砚砜砺砻砾础硁硕硖硗硙确硷碍碛碜碱磷礴礼祃祎祢祯祷祸禀禄禅离秃种秘积称秽秾稆税稣稳穑穷窃窍窎窑窜窝窥窦" + "窭竖竞笃笋笔笕笺笼笾筑筚筛筜筝筹筼签简箓箦箧箨箩箪箫篑篓篪篮篱簖籁籴类籼粜粝粤粪粮糁糇糟紧絷纟纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰" + "纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫绬续绮绯绰" + "绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缋缌缍缎缏缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱" + "缲缳缴缵罂网罗罚罢罴羁羟羡翘翱耀耢耧耸聂聋职聍联聩聪肃肠肤肮肴肾肿胀胁胜胧胨胪胫胶脉脍脏脐脑脓脔脚脱脶脸腌腭腻腼腽腾膑膻臜舆舣舰" + "舱舻艰艳艺节芈芗芜芦苁苇苈苋苌苍苏范茎茏茑茔茕茧荆荐荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药莅莱莲莳莴莶获莸莹莺莼萝萤营萦萧萨" + "葱蒇蒉蒋蒌蓝蓟蓠蓣蓥蓦蔂蔑蔷蔹蔺蔼蕰蕲蕴薮薯藓藤蘖虏虑虚虫虬虮虽虾虿蚀蚁蚂蚕蚬蛊蛎蛏蛮蛰蛱蛲蛳蛴蜕蜗蜷蝇蝈蝉蝎蝼蝾螀螨蟏蠹衅衔补" + "衬衮袄袅袆袜袭袯装裆裈裢裣裤裥褛褴见观觃规觅视觇览觉觊觋觌觍觎觏觐觑觞触觯訚誉誊讠计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许" + "讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诪诫诬语诮误诰诱诲诳说诵诶请诸" + "诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶豮贝贞" + "负贠贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞" + "赟赠赡赢赣赪赵赶趋趱趸跃跄跞践跶跷跸跹跻踊踌踪踬踯蹑蹒蹰蹿躏躜躯车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅" + "辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辩辫边辽达迁过迈运还这进远违连迟迩迳迹适选逊递逦逻逾遗遥邓邝邬邮邹邺邻郁郏郐郑郓郦郧郸" + "酂酝酦酱酸酽酾酿释里鉴銮錾钅钆钇针钉钊钋钌钍钎钏钐钑钒钓钔钕钖钗钘钙钚钛钜钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钶钷" + "钸钹钺钻钼钽钾钿铀铁铂铃铄铅铆铇铈铉铊铋铌铍铎铏铐铑铒铓铔铕铖铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铦铧铨铩铪铫铬铭铮铯铰铱铲铳铴铵银铷" + "铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗锘错锚锛锜锝锞锟锠锡锢锣锤锥锦锧锨锩锪锫锬锭键锯锰锱锲锳锴锵锶锷" + "锸锹锻锼锽锾锿镀镁镂镃镄镅镆镇镈镉镊镋镌镍镎镏镐镑镒镓镔镖镗镘镙镚镛镜镝镞镟镠镡镢镣镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镵镶长门闩" + "闪闫闬闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阓阔阕阖阗阘阙阚阛队阳阴阵阶陀际陆陇陈陉陕陧陨" + "险随隐隶隽难雏雠雳雾霁霡霭靓静靥鞑鞒鞯韦韧韨韩韪韫韬韵页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颒颓颔颕颖颗题颙" + "颚颛颜额颞颟颠颡颢颤颥颦颧风飏飐飑飒飓飔飕飖飗飘飙飚飞飨餍饣饤饥饦饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饾饿馁馂馃馄馅馆" + "馇馈馉馊馋馌馍馎馏馐馑馒馓馔馕马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑骒骓骔骕骖骗骘骙骚骛骜" + "骝骞骟骠骡骢骣骤骥骦骧髅髋髌鬓魇魉鱼鱽鱾鱿鲀鲁鲂鲃鲄鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲓鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪" + "鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲶鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳆鳇鳈鳉鳊鳋鳌鳍鳎鳏鳐鳑鳒鳓鳔鳕鳖鳗鳘鳙鳚鳛鳜鳝鳞鳟鳠鳡鳢鳣鸟鸠鸡鸢鸣鸤鸥" + "鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸴鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹓鹔鹕鹖鹗鹘鹙鹚鹛鹜鹝鹞鹟鹠鹡鹢鹣鹤鹥" + "鹦鹧鹨鹩鹪鹫鹬鹭鹯鹰鹱鹲鹳鹴鹾麦麸麻黄黉黡黩黪黾鼋鼍鼗鼹齐齑齿龀龁龂龃龄龅龆龇龈龉龊龋龌龙龚龛龟"; HanVariants.TRADITIONAL = "丟並亂亙亞份佇佈佔佗併來侖侶侷俁係俔俠俬俱倀倆倈倉個們倖倡倣倫偉側偵偽傑傖傘備傢傭傯傳傴債傷傾僂僅僇僉像僑僕僞僥僨僱價儀儂億儈儉" + "儐儔儕儘償優儲儷儸儺儻儼兇兌兒兗兩冊冪凈凍凜凱別刪剄則剋剎剗剛剝剮剴創剷劃劇劉劊劌劍劏劑劚勁動勗務勛勝勞勢勩勱勳勵勸勻匭匯匱區協" + "卹卻厙厠厭厲厴參叄叢吋吒吢吳吶呂咷咼員唄唚唸問啓啞啟啢喎喚喨喪喫喬單喲嗆嗇嗊嗎嗚嗩嗶嘆嘍嘔嘖嘗嘜嘩嘮嘯嘰嘵嘸嘽噓噚噝噠噥噦噯噲噴" + "噸噹嚀嚇嚌嚐嚕嚙嚥嚦嚨嚮嚲嚳嚴嚶囀囁囂囅囈囉囍囑囓囪圇國圍園圓圖團垵埡埰執堅堊堖堝堯報場塊塋塏塒塗塚塢塤塵塹墊墜墮墳墻墾壇壋壎壓" + "壘壙壚壜壞壟壠壢壩壯壺壼壽夠夢夥夾奐奧奩奪奬奮奼妝姊姍姦姪娛婁婦婭媧媯媼媽嫋嫗嫵嫻嫿嬀嬈嬋嬌嬙嬝嬡嬤嬪嬰嬸孃孌孫學孿宮察寢實寧審" + "寫寬寵寶尅將專尋對導尷屆屍屓屜屢層屨屬岡峴島峽崍崑崗崙崢崬嵐嶁嶄嶇嶔嶗嶠嶢嶧嶮嶴嶸嶺嶼嶽巋巒巔巖巰帥師帳帶幀幃幗幘幟幣幫幬幹幾庫" + "廁廂廄廈廚廝廟廠廡廢廣廩廬廳廻弒弔弳張強彆彈彌彎彙彞彥彿後徑從徠復徬徹怵恆恥悅悞悳悵悶悽惡惱惲惻愛愜愨愴愷愾慄慇態慍慘慚慟慣慤慪" + "慫慮慳慶慼慾憂憊憐憑憒憚憤憫憮憲憶懃懇應懌懍懞懟懣懨懮懲懶懷懸懺懼懾戀戇戔戧戩戰戱戲戶拋挩挾捨捫捲掃掄掗掙掛採揀揚換揮搆損搖搗搥" + "搧搨搵搶搾摀摑摜摟摯摳摶摺摻撈撏撐撓撚撝撟撢撣撥撫撲撳撻撾撿擁擄擇擊擋擓擔據擠擣擬擯擰擱擲擴擷擺擻擼擾攄攆攏攔攖攙攛攜攝攢攣攤攪" + "攬敗敘敵數斂斃斕斬斷於昇時晉晝暈暉暘暢暫暱曄曆曇曉曏曖曠曨曬書會朧杖東枒柵桿梔梘條梟梲棄棖棗棟棧棲棶椏楊楓楨業極榖榪榮榲榿構槍槓" + "槖槤槧槨槳樁樂樅樑樓標樞樣樸樹樺橈橋機橢橫檁檉檔檜檝檟檢檣檮檯檳檸檻櫓櫚櫛櫝櫞櫟櫥櫧櫨櫪櫫櫬櫱櫳櫸櫺櫻欄權欏欒欖欞欵欽歎歐歛歟歡" + "歲歷歸歿殘殞殤殨殫殮殯殰殲殺殻殼毀毆毬毿氂氈氌氣氫氬氳氹氾汎汙決沍沒沖況洩洶浹涇涼淒淚淥淪淵淶淺渙減渦測渾湊湞湧湯溈準溝溫溼滄滅" + "滌滎滬滯滲滷滸滻滾滿漁漚漢漣漬漲漵漸漿潁潑潔潙潛潤潯潰潷潿澀澆澇澗澠澤澦澩澮澱濁濃濕濘濟濤濫濬濰濱濺濼濾瀅瀆瀇瀉瀋瀏瀕瀘瀝瀟瀠瀦" + "瀧瀨瀰瀲瀾灃灄灑灕灘灝灠灣灤灧災為烏烴焠無煉煒煙煢煥煩煬煱熅熒熗熱熲熾燁燄燈燉燐燒燙燜營燦燬燭燴燶燻燼燾燿爍爐爛爭爲爺爾牀牆牋牘" + "牽犖犢犧狀狹狽猙猶猻獁獃獄獅獎獨獪獫獮獰獱獲獵獷獸獺獻獼玀現琺琿瑋瑒瑣瑤瑩瑪瑯瑲璉璣璦璫環璽瓊瓏瓔瓚甌甕甚產産畝畢畫異當疇疊痀痙" + "痠痾瘂瘋瘍瘓瘞瘡瘧瘮瘲瘺瘻療癆癇癉癒癘癟癡癢癤癥癧癩癬癭癮癰癱癲發皁皚皰皸皺盃盜盞盡監盤盧盪眥眾睏睜睞睪瞇瞘瞜瞞瞭瞶瞼矓矚矯砂砲" + "硃硜硤硨硯碩碭碳碸確碼磑磚磣磧磯磽礆礎礙礡礦礪礫礬礮礱祕祿禍禎禕禡禦禪禮禰禱禿秈稅稈稏稜稟種稱穀穌積穎穠穡穢穩穫穭窩窪窮窯窵窶窺" + "竄竅竇竈竊竪競筆筍筧筴箇箋箎箏箝節範築篋篔篤篩篳簀簆簍簞簡簣簫簷簹簽簾籃籌籐籙籜籟籠籤籩籪籬籮籲粧粵糝糞糧糰糲糴糶糹糾紀紂約紅紆" + "紇紈紉紋納紐紓純紕紖紗紘紙級紛紜紝紡紬紮細紱紲紳紵紹紺紼紿絀終絃組絅絆絎結絕絛絝絞絡絢給絨絰統絲絳絶絹綁綃綆綈綉綌綏綐綑經綜綞綠" + "綢綣綫綬維綯綰綱網綳綴綵綸綹綺綻綽綾綿緄緇緊緋緑緒緓緔緗緘緙線緝緞締緡緣緦編緩緬緯緱緲練緶緹緻縈縉縊縋縐縑縕縗縛縝縞縟縣縧縫縭縮" + "縱縲縳縴縵縶縷縹總績繃繅繆繇繒織繕繚繞繡繢繩繪繫繭繮繯繰繳繸繹繼繽繾繿纈纊續纍纏纓纔纘纜缽罈罌罎罣罰罵罷羅羆羈羋羣羥羨義羶習翫翹" + "翺耬耮聖聞聯聰聲聳聵聶職聹聽聾肅脅脈脛脣脫脹腎腖腡腦腫腳腸膃膚膠膩膽膾膿臉臍臏臘臚臟臠臢臥臨臺與興舉舊舖艙艤艦艫艱艷芻茲荊荳莊莖" + "莢莧菓華菸萇萊萬萵葉葒葤葦葯葷蒐蒓蒔蒞蒼蓀蓆蓋蓮蓯蓽蔔蔞蔣蔥蔦蔭蔴蕁蕆蕎蕒蕓蕕蕘蕢蕩蕪蕭蕷薀薈薊薌薑薔薘薟薦薩薰薳薴薺藍藎藝藥藪" + "藴藶藷藹藺蘄蘆蘇蘊蘋蘚蘞蘢蘭蘺蘿虆處虛虜號虧虯蛺蛻蜆蝕蝟蝦蝨蝸螄螞螢螮螻螿蟄蟈蟎蟣蟬蟯蟲蟶蟻蠅蠆蠍蠐蠑蠔蠟蠣蠧蠨蠱蠶蠻衆衊術衕衚" + "衛衝袞袴裊裏補裝裡製複褌褘褲褳褸褻襇襏襖襝襠襤襪襬襯襲覈見覎規覓視覘覡覥覦親覬覯覲覷覺覽覿觀觴觶觸訁訂訃計訊訌討訐訒訓訕訖託記訛" + "訝訟訢訣訥訩訪設許訴訶診註証詁詆詎詐詒詔評詖詗詘詛詞詠詡詢詣試詩詫詬詭詮詰話該詳詵詼詿誄誅誆誇誌認誑誒誕誘誚語誠誡誣誤誥誦誨說説" + "誰課誶誹誼誾調諂諄談諉請諍諏諑諒論諗諛諜諝諞諡諢諤諦諧諫諭諮諱諳諶諷諸諺諼諾謀謁謂謄謅謊謎謐謔謖謗謙謚講謝謠謡謨謫謬謭謳謹謾譁譅" + "證譎譏譖識譙譚譜譟譫譯議譴護譸譽譾讀變讌讎讒讓讕讖讚讜讞豈豎豐豔豬豶貍貓貙貝貞貟負財貢貧貨販貪貫責貯貰貲貳貴貶買貸貺費貼貽貿賀賁" + "賂賃賄賅資賈賊賑賒賓賕賙賚賜賞賠賡賢賣賤賦賧質賫賬賭賰賴賵賸賺賻購賽賾贄贅贇贈贊贋贍贏贐贓贔贖贗贛贜赬趕趙趨趲跡跤跼踐踡踰踴蹌蹕" + "蹟蹣蹤蹧蹺躂躉躊躋躍躑躒躓躕躚躡躥躦躪軀車軋軌軍軑軒軔軛軟軤軫軲軸軹軺軻軼軾較輅輇輈載輊輒輓輔輕輛輜輝輞輟輥輦輩輪輬輯輳輸輻輾輿" + "轀轂轄轅轆轉轍轎轔轝轟轡轢轤辦辭辮辯農迴逕這連週進遊運過達違遙遜遞遠適遯遲遷選遺遼邁還邇邊邏邐郟郵鄆鄉鄒鄔鄖鄧鄭鄰鄲鄴鄶鄺酇酈酥" + "醃醖醜醞醫醬醱醼釀釁釃釅釋釐釒釓釔釕釗釘釙針釣釤釦釧釩釵釷釹釺鈀鈁鈃鈄鈈鈉鈍鈎鈐鈑鈒鈔鈕鈞鈣鈥鈦鈧鈮鈰鈳鈴鈷鈸鈹鈺鈽鈾鈿鉀鉅鉈鉉" + "鉋鉍鉑鉕鉗鉚鉛鉞鉢鉤鉦鉬鉭鉶鉸鉺鉻鉿銃銅銍銑銓銖銘銚銛銜銠銣銥銦銨銩銪銫銬銱銲銳銷銹銻銼鋁鋃鋅鋇鋌鋏鋒鋙鋝鋟鋣鋤鋥鋦鋨鋩鋪鋭鋮鋯" + "鋰鋱鋶鋸鋼錁錄錆錇錈錏錐錒錕錘錙錚錛錟錠錡錢錦錨錩錫錮錯録錳錶錸鍀鍁鍃鍆鍇鍈鍊鍋鍍鍔鍘鍚鍛鍠鍤鍥鍩鍬鍰鍵鍶鍺鍾鎂鎄鎇鎊鎔鎖鎗鎘鎚" + "鎛鎡鎢鎣鎦鎧鎩鎪鎬鎮鎰鎲鎳鎵鎸鎿鏃鏇鏈鏌鏍鏐鏑鏗鏘鏜鏝鏞鏟鏡鏢鏤鏨鏰鏵鏷鏹鏽鐃鐋鐐鐒鐓鐔鐘鐙鐝鐠鐦鐧鐨鐫鐮鐲鐳鐵鐶鐸鐺鐿鑄鑊鑌鑑" + "鑒鑔鑕鑞鑠鑣鑥鑭鑰鑱鑲鑷鑹鑼鑽鑾鑿钁長門閂閃閆閈閉開閌閎閏閑閒間閔閘閡関閣閥閧閨閩閫閬閭閱閲閶閹閻閼閽閾閿闃闆闇闈闊闋闌闍闐闒闓" + "闔闕闖闘關闞闠闡闢闤闥阨阪陘陝陞陣陰陳陸陽隄隉隊階隕際隨險隱隴隸隻雋雖雙雛雜雞離難雲電霑霢霧霽靂靄靈靚靜靦靨靷鞀鞏鞝鞦鞽韁韃韆韉" + "韋韌韍韓韙韜韞韮韻響頁頂頃項順頇須頊頌頎頏預頑頒頓頗領頜頡頤頦頭頮頰頲頴頷頸頹頻頽顆題額顎顏顒顓顔願顙顛類顢顥顧顫顬顯顰顱顳顴風" + "颭颮颯颱颳颶颸颺颻颼飀飄飆飈飛飠飢飣飥飩飪飫飭飯飲飴飼飽飾飿餃餄餅餉養餌餎餏餑餒餓餕餖餘餚餛餜餞餡館餬餱餳餵餶餷餺餼餽餾餿饁饃饅" + "饈饉饊饋饌饑饒饗饜饞饢馬馭馮馱馳馴馹駁駐駑駒駔駕駘駙駛駝駟駡駢駭駰駱駸駿騁騂騅騌騍騎騏騖騙騤騧騫騭騮騰騶騷騸騾驀驁驂驃驄驅驊驌驍" + "驏驕驗驚驛驟驢驤驥驦驪驫骯髏髒體髕髖鬀鬆鬍鬚鬢鬥鬧鬨鬩鬭鬮鬱魎魘魚魛魢魨魯魴魷魺鮁鮃鮊鮋鮍鮎鮐鮑鮒鮓鮚鮜鮝鮞鮦鮪鮫鮭鮮鮳鮶鮺鯀鯁" + "鯇鯉鯊鯒鯔鯕鯖鯛鯝鯡鯢鯤鯧鯨鯪鯫鯰鯴鯷鯽鯿鰁鰂鰃鰈鰉鰍鰏鰐鰒鰓鰜鰟鰠鰣鰥鰨鰩鰭鰮鰱鰲鰳鰵鰷鰹鰺鰻鰼鰾鱂鱅鱈鱉鱒鱔鱖鱗鱘鱝鱟鱠鱣鱤" + "鱧鱨鱭鱯鱷鱸鱺鳥鳧鳩鳬鳲鳳鳴鳶鳾鴆鴇鴉鴒鴕鴛鴝鴞鴟鴣鴦鴨鴯鴰鴴鴷鴻鴿鵁鵂鵃鵐鵑鵒鵓鵜鵝鵠鵡鵪鵬鵮鵯鵲鵷鵾鶄鶇鶉鶊鶓鶖鶘鶚鶡鶥鶩鶪" + "鶬鶯鶲鶴鶹鶺鶻鶼鷀鷁鷂鷄鷈鷊鷓鷖鷗鷙鷚鷥鷦鷫鷯鷲鷳鷸鷹鷺鷽鷿鸂鸇鸌鸏鸕鸘鸚鸛鸝鸞鹵鹹鹺鹼鹽麗麤麥麩麯麵麼麽黃黌點黨黲黴黶黷黽黿鼇" + "鼈鼉鼕鼴齊齋齎齏齒齔齕齗齙齜齟齠齡齣齦齧齩齪齬齲齶齷龍龎龐龔龕龜"; })(HanVariants = TabLocalFiles.HanVariants || (TabLocalFiles.HanVariants = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var DocumentLanguage; (function (DocumentLanguage) { const TARGET_SCRIPTS = { cmn: "han", "cmn-Hant": "han", jpn: "japanese", kor: "hangul", eng: "latin", fra: "latin", deu: "latin", spa: "latin", rus: "cyrillic" }; const SINGLE_LANGUAGE_SCRIPTS = new Set(["han", "japanese", "hangul"]); const PLACEHOLDER_PATTERN = /⟦\d+⟧/g; const UNIT_PATTERN = /\p{Script=Han}|[\p{Script=Hiragana}\p{Script=Katakana}]|\p{Script=Hangul}|\p{Script=Latin}+|\p{Script=Cyrillic}+|[^\P{L}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Latin}\p{Script=Cyrillic}]+/gu; let simplifiedOnly; let traditionalOnly; function isInTargetLanguage(text, targetCode) { const target = TARGET_SCRIPTS[targetCode]; return target !== undefined && SINGLE_LANGUAGE_SCRIPTS.has(target) && dominantScript(text) === target && fitsChineseVariant(text, targetCode); } DocumentLanguage.isInTargetLanguage = isInTargetLanguage; function isProvablyForeign(text, targetCode) { const target = TARGET_SCRIPTS[targetCode]; const script = dominantScript(text); if (target === undefined || script === undefined) { return false; } return script !== target || !fitsChineseVariant(text, targetCode); } DocumentLanguage.isProvablyForeign = isProvablyForeign; function dominantScript(text) { const counts = { han: 0, kana: 0, hangul: 0, latin: 0, cyrillic: 0, other: 0 }; for (const [unit] of text.replace(PLACEHOLDER_PATTERN, " ").matchAll(UNIT_PATTERN)) { if (/\p{Script=Han}/u.test(unit)) { counts.han += 1; } else if (/[\p{Script=Hiragana}\p{Script=Katakana}]/u.test(unit)) { counts.kana += 1; } else if (/\p{Script=Hangul}/u.test(unit)) { counts.hangul += 1; } else if (/\p{Script=Latin}/u.test(unit)) { counts.latin += 1; } else if (/\p{Script=Cyrillic}/u.test(unit)) { counts.cyrillic += 1; } else { counts.other += 1; } } const candidates = [ counts.kana > 0 ? ["japanese", counts.kana + counts.han] : ["han", counts.han], ["hangul", counts.hangul], ["latin", counts.latin], ["cyrillic", counts.cyrillic], ["other", counts.other] ]; let dominant; for (const candidate of candidates) { if (candidate[1] > (dominant?.[1] ?? 0)) { dominant = candidate; } } return dominant?.[0]; } function fitsChineseVariant(text, targetCode) { if (targetCode !== "cmn" && targetCode !== "cmn-Hant") { return true; } simplifiedOnly ?? (simplifiedOnly = new Set(TabLocalFiles.HanVariants.SIMPLIFIED)); traditionalOnly ?? (traditionalOnly = new Set(TabLocalFiles.HanVariants.TRADITIONAL)); const [target, opposite] = targetCode === "cmn" ? [simplifiedOnly, traditionalOnly] : [traditionalOnly, simplifiedOnly]; let targetCount = 0; let oppositeCount = 0; for (const character of text) { if (target.has(character) && !opposite.has(character)) { targetCount += 1; } else if (opposite.has(character) && !target.has(character)) { oppositeCount += 1; } } return oppositeCount <= targetCount; } })(DocumentLanguage = TabLocalFiles.DocumentLanguage || (TabLocalFiles.DocumentLanguage = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var DocumentSegments; (function (DocumentSegments) { const PLACEHOLDER_OPEN = "⟦"; const PLACEHOLDER_CLOSE = "⟧"; const FENCE_PATTERN = /^(\s*)(```+|~~~+)(.*)$/; const HEADING_PATTERN = /^(\s{0,3}#{1,6}[ \t]+)(.*)$/; const HEADING_CLOSING_PATTERN = /[ \t]+#+[ \t]*$/; const LIST_ITEM_PATTERN = /^(\s*(?:[-*+]|\d+[.)])[ \t]+(?:\[[ xX]\][ \t]+)?)(.*)$/; const TASK_BOX_PATTERN = /\[[ xX]\][ \t]+$/; const ORDERED_OR_TASK_PREFIX_PATTERN = /^\s*(?:\d+[.)]|[-*+][ \t]+\[[ xX]\])/; const QUOTE_PATTERN = /^(\s{0,3}(?:>[ \t]?)+)(.*)$/; const CALLOUT_MARKER_PATTERN = /^(\[![A-Za-z][\w-]*\][+-]?[ \t]*)(.*)$/; const THEMATIC_BREAK_PATTERN = /^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/; const TABLE_DELIMITER_PATTERN = /^\s{0,3}\|?[\s:|-]*-[\s:|-]*\|?\s*$/; const SETEXT_UNDERLINE_PATTERN = /^\s{0,3}(?:=+|-+)\s*$/; const HTML_BLOCK_PATTERN = /^\s{0,3}<[!/a-z]/i; const MATH_FENCE_PATTERN = /^\s*\$\$\s*$/; const REFERENCE_DEFINITION_PATTERN = /^\s{0,3}\[(?!\^)([^\]]+)\]:[ \t]*\S/; const UNTITLED_REFERENCE_DEFINITION_PATTERN = /^\s{0,3}\[(?!\^)[^\]]+\]:[ \t]*\S+[ \t]*$/; const LINK_TITLE_LINE_PATTERN = /^\s*(?:"[^"]*"|'[^']*'|\([^)]*\))\s*$/; const ABBREVIATION_DEFINITION_PATTERN = /^\s{0,3}\*\[[^\]]+\]:/; const FOOTNOTE_DEFINITION_PATTERN = /^(\[\^[^\]\s]+\]:[ \t]*)(.*)$/; const FOOTNOTE_CONTINUATION_PREFIX = " "; const FIELD_LINE_PATTERN = /^\s{0,3}[\p{L}\p{N}_][\p{L}\p{N}_ \-/]*::/u; const TASK_KEYWORDS = "TODO|DOING|DONE|LATER|NOW|WAITING|WAIT|CANCELED|CANCELLED|IN-PROGRESS"; const TASK_KEYWORD_START_PATTERN = new RegExp(`^(?:${TASK_KEYWORDS})(?=[ \\t]|$)`); const CONTAINER_FENCE_PATTERN = /^\s{0,3}:{2,}(?:[ \t]*[A-Za-z{]|\s*$)/; const LETTER_PATTERN = /\p{L}/u; function isTranslatableText(text) { return LETTER_PATTERN.test(text); } DocumentSegments.isTranslatableText = isTranslatableText; function segmentPlainText(source) { const segments = []; const lines = source.split("\n"); let index = 0; let nextId = 0; while (index < lines.length) { if (!lines[index].trim()) { segments.push({ translatable: false, lines: [lines[index]] }); index += 1; continue; } const start = index; while (index < lines.length && lines[index].trim()) { index += 1; } const paragraph = lines.slice(start, index); segments.push(paragraphSegment(paragraph, `s${nextId++}`) ?? { translatable: false, lines: paragraph }); } return segments; } DocumentSegments.segmentPlainText = segmentPlainText; function segmentMarkdown(source) { const segments = []; const lines = source.split("\n"); let index = 0; let nextId = 0; const nextSegmentId = () => `s${nextId++}`; let listContentIndent; const frontmatterFence = lines[0]?.trim(); if (frontmatterFence === "---" || frontmatterFence === "+++") { const close = lines.findIndex((line, position) => position > 0 && line.trim() === frontmatterFence); if (close > 0) { segments.push({ translatable: false, lines: lines.slice(0, close + 1) }); index = close + 1; } } while (index < lines.length) { const line = lines[index]; if (!line.trim()) { segments.push({ translatable: false, lines: [line] }); index += 1; continue; } const indent = indentWidth(line); if (listContentIndent !== undefined && indent < listContentIndent && !LIST_ITEM_PATTERN.test(line)) { listContentIndent = undefined; } if (THEMATIC_BREAK_PATTERN.test(line)) { segments.push({ translatable: false, lines: [line] }); listContentIndent = undefined; index += 1; continue; } const codeIndent = (listContentIndent ?? 0) + 4; if (indent >= codeIndent) { const end = findIndentedCodeEnd(lines, index, codeIndent); segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end; continue; } const fence = line.match(FENCE_PATTERN); if (fence) { const [, , marker] = fence; const end = findFenceEnd(lines, index + 1, marker[0], marker.length); segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end; continue; } if (MATH_FENCE_PATTERN.test(line)) { const end = findLineEnd(lines, index + 1, candidate => MATH_FENCE_PATTERN.test(candidate)); segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end; continue; } if (HTML_BLOCK_PATTERN.test(line)) { const end = findBlankLine(lines, index); segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end; continue; } if (REFERENCE_DEFINITION_PATTERN.test(line) || ABBREVIATION_DEFINITION_PATTERN.test(line)) { const hasTitleLine = UNTITLED_REFERENCE_DEFINITION_PATTERN.test(line) && LINK_TITLE_LINE_PATTERN.test(lines[index + 1] ?? ""); const end = hasTitleLine ? index + 2 : index + 1; segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end; continue; } if (CONTAINER_FENCE_PATTERN.test(line)) { segments.push({ translatable: false, lines: [line] }); index += 1; continue; } if (opensBlockComment(line)) { const end = findLineEnd(lines, index + 1, candidate => candidate.includes("%%")); segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end; continue; } if (line.includes("|") && TABLE_DELIMITER_PATTERN.test(lines[index + 1] ?? "")) { const end = findBlankLine(lines, index); segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end; continue; } const heading = line.match(HEADING_PATTERN); if (heading) { const closing = heading[2].match(HEADING_CLOSING_PATTERN)?.[0] ?? ""; const text = heading[2].slice(0, heading[2].length - closing.length); const segment = makeSegment("heading", heading[1], heading[1], [line], text, [""], nextSegmentId()); segments.push(segment.translatable ? { ...segment, suffix: closing } : segment); index += 1; continue; } const quote = line.match(QUOTE_PATTERN); if (quote) { const end = findLineEnd(lines, index, candidate => !QUOTE_PATTERN.test(candidate) || !candidate.trim(), true); segments.push(...quoteSegments(lines.slice(index, end), nextSegmentId)); index = end; continue; } const footnote = line.match(FOOTNOTE_DEFINITION_PATTERN); if (footnote) { const end = findLineEnd(lines, index + 1, candidate => !/^(?: {4}|\t)\S/.test(candidate), true); const definitionLines = lines.slice(index, end); const text = joinWrappedLines([footnote[2], ...definitionLines.slice(1)]).trim(); segments.push(makeSegment("footnote", footnote[1], FOOTNOTE_CONTINUATION_PREFIX, definitionLines, text, [], nextSegmentId(), "same-item")); index = end; continue; } const listItem = line.match(LIST_ITEM_PATTERN); if (listItem) { const end = findListItemEnd(lines, index + 1); const itemLines = lines.slice(index, end); const prefix = listItem[1]; const continuationPrefix = " ".repeat(prefix.length); const text = joinWrappedLines([listItem[2], ...itemLines.slice(1)]).trimEnd(); segments.push(makeSegment("list-item", prefix, continuationPrefix, itemLines, text, [], nextSegmentId(), ORDERED_OR_TASK_PREFIX_PATTERN.test(prefix) || TASK_KEYWORD_START_PATTERN.test(listItem[2]) ? "same-item" : "blocks")); listContentIndent = prefix.replace(TASK_BOX_PATTERN, "").length; index = end; continue; } if (FIELD_LINE_PATTERN.test(line)) { segments.push({ translatable: false, lines: [line] }); index += 1; continue; } const end = findParagraphEnd(lines, index); const paragraph = lines.slice(index, end); const hasSetextUnderline = SETEXT_UNDERLINE_PATTERN.test(lines[end] ?? ""); segments.push((hasSetextUnderline ? undefined : paragraphSegment(paragraph, nextSegmentId(), true)) ?? { translatable: false, lines: paragraph }); index = end; } return segments; } DocumentSegments.segmentMarkdown = segmentMarkdown; function quoteSegments(lines, nextSegmentId) { const segments = []; let run; const flush = () => { if (run) { const { prefix } = run; segments.push(makeSegment("quote", prefix, prefix, run.lines, run.texts.join("\n").trimEnd(), [prefix.trimEnd()], nextSegmentId())); run = undefined; } }; let previousDepth = 0; for (let index = 0; index < lines.length; index += 1) { const [, prefix = "", content = ""] = lines[index].match(QUOTE_PATTERN) ?? []; const depth = (prefix.match(/>/g) ?? []).length; const opensQuote = depth > previousDepth; previousDepth = depth; const fence = content.match(FENCE_PATTERN); if (fence) { flush(); const [, , marker] = fence; let end = index + 1; while (end < lines.length) { const inner = (lines[end].match(QUOTE_PATTERN)?.[2] ?? "").match(FENCE_PATTERN); end += 1; if (inner && inner[2][0] === marker[0] && inner[2].length >= marker.length && !inner[3].trim()) { break; } } segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end - 1; continue; } const nextContent = lines[index + 1]?.match(QUOTE_PATTERN)?.[2] ?? ""; if (content.includes("|") && TABLE_DELIMITER_PATTERN.test(nextContent)) { flush(); let end = index + 1; while (end < lines.length && (lines[end].match(QUOTE_PATTERN)?.[2] ?? "").trim()) { end += 1; } segments.push({ translatable: false, lines: lines.slice(index, end) }); index = end - 1; continue; } const callout = opensQuote ? content.match(CALLOUT_MARKER_PATTERN) : null; if (callout) { flush(); const marker = prefix + callout[1]; segments.push(makeSegment("callout-title", marker, marker, [lines[index]], callout[2].trimEnd(), [], nextSegmentId(), "same-line")); continue; } if (!content.trim()) { flush(); segments.push({ translatable: false, lines: [lines[index]] }); continue; } if (run && run.depth !== depth) { flush(); } run ?? (run = { prefix, depth, lines: [], texts: [] }); run.lines.push(lines[index]); run.texts.push(content); } flush(); return segments; } function paragraphSegment(lines, id, joinWraps = false) { const indentMatch = lines[0].match(/^\s*/); const prefix = indentMatch ? indentMatch[0] : ""; const text = (joinWraps ? joinWrappedLines(lines) : lines.map(line => line.trimStart()).join("\n")).trimEnd(); const segment = makeSegment("paragraph", prefix, prefix, lines, text, [""], id); return segment.translatable ? segment : undefined; } function joinWrappedLines(lines) { return lines.reduce((text, line, index) => { const content = line.trimStart(); if (index === 0) { return content; } return TabLocalFiles.SoftWrap.joins(lines[index - 1], line) ? `${text.trimEnd()} ${content}` : `${text}\n${content}`; }, ""); } function makeSegment(kind, prefix, continuationPrefix, lines, text, separatorLines, id, bilingualLayout = "blocks") { if (!isTranslatableText(text)) { return { translatable: false, lines }; } return { translatable: true, id, kind, prefix, continuationPrefix, suffix: "", lines, text, separatorLines, bilingualLayout }; } function opensBlockComment(line) { return /^\s*%%/.test(line) && !/^\s*%%[\s\S]*?%%/.test(line); } function indentWidth(line) { let width = 0; for (const char of line) { if (char === " ") { width += 1; } else if (char === "\t") { width += 4 - (width % 4); } else { break; } } return width; } function findIndentedCodeEnd(lines, start, codeIndent) { let end = start + 1; for (let index = start + 1; index < lines.length; index += 1) { if (!lines[index].trim()) { continue; } if (indentWidth(lines[index]) < codeIndent) { break; } end = index + 1; } return end; } function findFenceEnd(lines, start, marker, length) { for (let index = start; index < lines.length; index += 1) { const match = lines[index].match(FENCE_PATTERN); if (match && match[2][0] === marker && match[2].length >= length && !match[3].trim()) { return index + 1; } } return lines.length; } function findLineEnd(lines, start, predicate, stopBefore = false) { for (let index = start; index < lines.length; index += 1) { if (predicate(lines[index])) { return stopBefore ? index : index + 1; } } return lines.length; } function findBlankLine(lines, start) { for (let index = start; index < lines.length; index += 1) { if (!lines[index].trim()) { return index; } } return lines.length; } function findListItemEnd(lines, start) { for (let index = start; index < lines.length; index += 1) { const line = lines[index]; if (!line.trim() || LIST_ITEM_PATTERN.test(line) || !/^\s/.test(line) || FENCE_PATTERN.test(line) || MATH_FENCE_PATTERN.test(line)) { return index; } } return lines.length; } function findParagraphEnd(lines, start) { for (let index = start + 1; index < lines.length; index += 1) { const line = lines[index]; if (!line.trim() || HEADING_PATTERN.test(line) || QUOTE_PATTERN.test(line) || LIST_ITEM_PATTERN.test(line) || FENCE_PATTERN.test(line) || MATH_FENCE_PATTERN.test(line) || THEMATIC_BREAK_PATTERN.test(line) || SETEXT_UNDERLINE_PATTERN.test(line) || FOOTNOTE_DEFINITION_PATTERN.test(line) || CONTAINER_FENCE_PATTERN.test(line) || FIELD_LINE_PATTERN.test(line) || opensBlockComment(line)) { return index; } } return lines.length; } const LINK_TARGET_SOURCE = String.raw `\((?:<[^<>\n]*>|(?:[^()\s]|\([^()\s]*\))*)(?:[ \t\n]+(?:"[^"]*"|'[^']*'|\([^()]*\)))?[ \t\n]*\)`; const IMAGE_WITH_TARGET_PATTERN = new RegExp(String.raw `(!\[[^\[\]]*\])(${LINK_TARGET_SOURCE})`, "g"); const LINK_WITH_TARGET_PATTERN = new RegExp(String.raw `(\[(?:[^\[\]]|\[[^\[\]]*\])*\])(${LINK_TARGET_SOURCE})`, "g"); const REFERENCE_LINK_PATTERN = /(\[(?:[^\[\]]|\[[^\[\]]*\])*\])(\[[^\[\]]*\])/g; const SHORTCUT_REFERENCE_PATTERN = /\[([^\[\]\n]+)\](?![(\[:⟦])/g; const BARE_URL_PATTERN = /(?[\]\u27E6\u27E7\u3000-\u303F\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFF00-\uFFEF\u201C\u201D\u2018\u2019]+/gi; const BARE_EMAIL_PATTERN = /(? { const expanded = value.replace(PLACEHOLDER_PATTERN, (_match, position) => { const inner = tokens[Number(position)]; tokens[Number(position)] = ""; kinds[Number(position)] = "absorbed"; return inner; }); tokens.push(expanded); kinds.push(kind); return `${PLACEHOLDER_OPEN}${tokens.length - 1}${PLACEHOLDER_CLOSE}`; }; const hideLiteral = (value) => hide(value); const protectedText = text .replace(/⟦/g, hideLiteral) .replace(/(`+)[\s\S]*?(?|%%[\s\S]*?%%/g, hideLiteral) .replace(/\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}|\$\{[^}\n]*\}/g, hideLiteral) .replace(/\\\([\s\S]+?\\\)|\\\[[\s\S]+?\\\]|\$\$[^$]+\$\$|\$(?:\\.|[^$\\\n])+?\$(?!\$)/g, hideLiteral) .replace(/\[[^\[\]\n]*?::[^\[\]\n]*\]|\([^()\n]*?::[^()\n]*\)/g, hideLiteral) .replace(/(^|\n)([ \t]*)([\p{L}\p{N}_][\p{L}\p{N}_ \-/]*::[^\n]*)/gu, (_match, lineBreak, indent, field) => { return `${lineBreak}${indent}${hide(field)}`; }) .replace(TASK_MARKER_PATTERN, (_match, lineBreak, marker) => `${lineBreak}${hide(marker)}`) .replace(/\b(?:TODO|FIXME)\b|\[#[A-Ca-c]\]/g, hideLiteral) .replace(/!?\[\[[^\[\]\n]+\]\]/g, hideLiteral) .replace(/\[\^[^\]\s]+\]/g, hideLiteral) .replace(IMAGE_WITH_TARGET_PATTERN, (_match, label, target) => `${label}${hide(target, "link-target")}`) .replace(LINK_WITH_TARGET_PATTERN, (_match, label, target) => `${label}${hide(target, "link-target")}`) .replace(REFERENCE_LINK_PATTERN, (match, label, reference) => { return reference === "[]" ? hide(match) : `${label}${hide(reference, "reference-label")}`; }) .replace(SHORTCUT_REFERENCE_PATTERN, (match, label) => { return referenceLabels.has(normalizeReferenceLabel(label)) ? hide(match) : match; }) .replace(/\[toc\]/gi, hideLiteral) .replace(/<[A-Za-z][A-Za-z0-9+.-]{1,31}:[^<>\s]*>|<[^<>\s@]+@[^<>\s@]+>/g, hideLiteral) .replace(/<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^<>]*)?\/?>/g, hideLiteral) .replace(BARE_URL_PATTERN, (match) => { const url = trimUrlEnd(match); return `${hide(url, "bare-url")}${match.slice(url.length)}`; }) .replace(BARE_EMAIL_PATTERN, (match) => hide(match, "bare-url")) .replace(/&(?:[A-Za-z][A-Za-z0-9]{1,31}|#\d{1,7}|#[xX][0-9A-Fa-f]{1,6});/g, hideLiteral) .replace(/\\[!-/:-@[-`{-~]/g, hideLiteral) .replace(/(?: {2,}|\\)(?=\n)/g, (match) => hide(match, "hard-break")) .replace(/(? `${lineBreak}${hide(marker, "line-start")}`); return { text: protectedText, tokens, kinds }; } DocumentSegments.protectInlineMarkdown = protectInlineMarkdown; function normalizeReferenceLabel(label) { return label.trim().replace(/\s+/g, " ").toLowerCase(); } function referenceLabels(segments) { const labels = new Set(); for (const segment of segments) { if (segment.translatable) { continue; } for (const line of segment.lines) { const match = line.match(REFERENCE_DEFINITION_PATTERN); if (match) { labels.add(normalizeReferenceLabel(match[1])); } } } return labels; } DocumentSegments.referenceLabels = referenceLabels; function trimUrlEnd(url) { let end = url.length; while (end > 0) { const char = url[end - 1]; if (`?!.,:;*_~'"`.includes(char)) { end -= 1; continue; } if (char === ")") { const candidate = url.slice(0, end); if ((candidate.match(/\)/g) ?? []).length > (candidate.match(/\(/g) ?? []).length) { end -= 1; continue; } } break; } return url.slice(0, end); } function restoreInlineMarkdown(text, tokens, kinds = [], sourceText) { if (!tokens.length) { return text; } const seen = new Set(); let output = ""; let cursor = 0; for (const match of text.matchAll(PLACEHOLDER_PATTERN)) { const position = Number(match[1]); if (!Number.isInteger(position) || position < 0 || position >= tokens.length || seen.has(position) || kinds[position] === "absorbed") { return undefined; } seen.add(position); output += text.slice(cursor, match.index); cursor = (match.index ?? 0) + match[0].length; const token = tokens[position]; switch (kinds[position] ?? "literal") { case "link-target": case "reference-label": { const attached = attachToLinkText(output); if (attached === undefined) { return undefined; } output = attached + token; break; } case "bare-url": { const placeholder = sourceText?.indexOf(match[0]) ?? -1; const writtenGlued = placeholder >= 0 && !isDelimitedBareUrl(sourceText.slice(0, placeholder), sourceText.slice(placeholder + match[0].length)); output += writtenGlued || isDelimitedBareUrl(output, text.slice(cursor)) ? token : delimitBareUrl(token); break; } case "hard-break": { output += token; let next = cursor; while (text[next] === " " || text[next] === "\t") { next += 1; } if (next < text.length && text[next] !== "\n") { output += "\n"; } cursor = next; break; } case "line-start": if (output && !output.endsWith("\n")) { output = `${output.replace(/[ \t]+$/, "")}\n`; } output += token; break; default: output += token; } } output += text.slice(cursor); const expected = tokens.length - kinds.filter(kind => kind === "absorbed").length; return seen.size === expected ? output : undefined; } DocumentSegments.restoreInlineMarkdown = restoreInlineMarkdown; function attachToLinkText(output) { const trimmed = output.replace(/[ \t]+$/, ""); if (trimmed.endsWith("]")) { return trimmed; } if (trimmed.endsWith("】")) { const open = trimmed.lastIndexOf("【"); return open < 0 ? undefined : `${trimmed.slice(0, open)}[${trimmed.slice(open + 1, -1)}]`; } return undefined; } function isDelimitedBareUrl(before, after) { const glued = after.match(/^[^\s<\]]*/)?.[0] ?? ""; return (before === "" || /[\s*_~(\[]$/.test(before)) && /^[?!.,:*_~)]*$/.test(glued); } function delimitBareUrl(url) { return /^www\./i.test(url) ? `[${url}](http://${url})` : `<${url}>`; } const BLOCK_START_ESCAPES = [ { pattern: /^#{1,6}(?:[ \t]|$)/, escape: line => `\\${line}` }, { pattern: /^[-*+][ \t]/, escape: line => `\\${line}` }, { pattern: /^\d{1,9}[.)](?:[ \t]|$)/, escape: line => line.replace(/^(\d+)/, "$1\\") }, { pattern: /^>/, escape: line => `\\${line}` }, { pattern: /^(?:```|~~~)/, escape: line => `\\${line}` }, { pattern: /^(?:=+|-+|(?:[*_][ \t]*){3,})[ \t]*$/, escape: line => `\\${line}` } ]; const EMPHASIS_MARKERS = [ { marker: "**", moveSpaces: true }, { marker: "__", moveSpaces: true }, { marker: "~~", moveSpaces: true }, { marker: "==", moveSpaces: false }, { marker: "++", moveSpaces: false } ]; function finishTranslation(source, translated) { if (!emphasisStaysBalanced(source.text, translated)) { return undefined; } const sourceStarts = new Set(source.text .split("\n") .map(line => BLOCK_START_ESCAPES.findIndex(({ pattern }) => pattern.test(line.trimStart()))) .filter(index => index >= 0)); const lines = translated.split("\n").map(line => { const trimmed = line.trimStart(); const blockStart = BLOCK_START_ESCAPES.findIndex(({ pattern }) => pattern.test(trimmed)); return blockStart < 0 || sourceStarts.has(blockStart) ? trimmed : BLOCK_START_ESCAPES[blockStart].escape(trimmed); }); const repaired = EMPHASIS_MARKERS.reduce((text, { marker, moveSpaces }) => repairEmphasisPairs(text, marker, moveSpaces), lines.join("\n")); return restoreInlineMarkdown(convertUnderscoreEmphasis(repaired, source.text), source.tokens, source.kinds, source.text); } DocumentSegments.finishTranslation = finishTranslation; function markerRuns(text, char, length) { const runs = []; const pattern = new RegExp(`${char.replace(/[*+]/g, "\\$&")}+`, "g"); for (const match of text.matchAll(pattern)) { if (match[0].length === length) { runs.push(match.index ?? 0); } } return runs; } function emphasisStaysBalanced(sourceText, translated) { const markers = [...EMPHASIS_MARKERS.map(({ marker }) => marker), "*", "_"]; return markers.every(marker => { const sourceCount = markerRuns(sourceText, marker[0], marker.length).length; const translatedCount = markerRuns(translated, marker[0], marker.length).length; return sourceCount % 2 === 1 || translatedCount % 2 === 0; }); } function isWordCharacter(char) { return Boolean(char) && /[\p{L}\p{N}]/u.test(char); } function isMovablePunctuation(char) { if (!char || char === PLACEHOLDER_OPEN || char === PLACEHOLDER_CLOSE) { return false; } if (char.charCodeAt(0) < 0x80) { return `"',.:;?!`.includes(char); } return /[\p{P}\p{S}]/u.test(char); } const CLOSING_PUNCTUATION = { "“": "”", "‘": "’", "「": "」", "『": "』", "(": ")", "《": "》", "〈": "〉", "【": "】", "\"": "\"", "'": "'" }; function repairEmphasisPairs(text, marker, moveSpaces) { const runs = markerRuns(text, marker[0], marker.length); if (runs.length % 2) { return text; } let result = text; for (let pair = runs.length - 2; pair >= 0; pair -= 2) { result = repairEmphasisPair(result, runs[pair], runs[pair + 1], marker, moveSpaces); } return result; } function repairEmphasisPair(text, open, close, marker, moveSpaces) { let before = text.slice(0, open); let inner = text.slice(open + marker.length, close); let after = text.slice(close + marker.length); if (moveSpaces) { const leading = /^[ \t]+/.exec(inner)?.[0] ?? ""; const trailing = /[ \t]+$/.exec(inner)?.[0] ?? ""; if (leading.length + trailing.length < inner.length) { before += leading; after = trailing + after; inner = inner.slice(leading.length, inner.length - trailing.length); } } if (isWordCharacter(before[before.length - 1])) { let moved = 0; while (moved < inner.length - 1 && isMovablePunctuation(inner[moved]) && !(inner[moved] === "!" && inner[moved + 1] === "[")) { moved += 1; } const lead = inner.slice(0, moved); before += lead; inner = inner.slice(moved); const partners = [...lead].reverse().map(char => CLOSING_PUNCTUATION[char]); const closing = partners.every(Boolean) ? partners.join("") : ""; if (closing && inner.length > closing.length && inner.endsWith(closing)) { after = closing + after; inner = inner.slice(0, -closing.length); } } if (isWordCharacter(after[0])) { let kept = inner.length; while (kept > 1 && isMovablePunctuation(inner[kept - 1])) { kept -= 1; } after = inner.slice(kept) + after; inner = inner.slice(0, kept); } return `${before}${marker}${inner}${marker}${after}`; } function convertUnderscoreEmphasis(text, sourceText) { if (inWordUnderscorePairs(sourceText).length) { return text; } let result = text; for (const { open, close, length } of inWordUnderscorePairs(text)) { const star = "*".repeat(length); result = `${result.slice(0, open)}${star}${result.slice(open + length, close)}${star}${result.slice(close + length)}`; } return result; } function inWordUnderscorePairs(text) { const pairs = []; for (const length of [2, 1]) { const runs = markerRuns(text, "_", length); if (runs.length % 2) { continue; } for (let pair = 0; pair < runs.length; pair += 2) { const [open, close] = [runs[pair], runs[pair + 1]]; const outside = [text[open - 1], text[close + length]]; const hasCjkNeighbour = outside.some(char => isWordCharacter(char) && char.charCodeAt(0) >= 0x80); if (hasCjkNeighbour && /[^\x00-\x7F]/.test(text.slice(open + length, close))) { pairs.push({ open, close, length }); } } } return pairs.sort((first, second) => second.open - first.open); } function merge(segments, translations, mode) { const lines = []; for (const segment of segments) { if (!segment.translatable) { lines.push(...segment.lines); continue; } const translated = translations.get(segment.id)?.trim(); if (!translated) { lines.push(...segment.lines); continue; } if (translated === segment.text.trim()) { lines.push(...segment.lines); continue; } const translatedLines = segment.bilingualLayout === "same-line" ? [`${segment.prefix}${translated.replace(/\s*\n\s*/g, " ")}`] : applyPrefixes(translated, segment); if (mode === "replace") { lines.push(...translatedLines); continue; } if (segment.bilingualLayout === "same-item") { lines.push(...continueItem(segment, translatedLines, mode)); continue; } if (segment.bilingualLayout === "same-line") { const original = segment.lines[0].slice(segment.prefix.length).trimEnd(); const translation = translatedLines[0].slice(segment.prefix.length); const [first, second] = mode === "original-first" ? [original, translation] : [translation, original]; lines.push(`${segment.prefix}${first} / ${second}`); continue; } if (mode === "original-first") { lines.push(...segment.lines, ...segment.separatorLines, ...translatedLines); continue; } lines.push(...translatedLines, ...segment.separatorLines, ...segment.lines); } return lines.join("\n"); } DocumentSegments.merge = merge; function continueItem(segment, translatedLines, mode) { const asContinuation = (lines) => [ segment.continuationPrefix + lines[0].slice(segment.prefix.length), ...lines.slice(1) ]; return mode === "original-first" ? [...withHardBreak(segment.lines), ...asContinuation(translatedLines)] : [...withHardBreak(translatedLines), ...asContinuation(segment.lines)]; } function withHardBreak(lines) { const last = lines[lines.length - 1]; const ended = last.endsWith("\\") || / {2,}$/.test(last); return ended ? [...lines] : [...lines.slice(0, -1), `${last}\\`]; } function applyPrefixes(translated, segment) { const lines = translated.split("\n").map((line, index) => { const prefix = index === 0 ? segment.prefix : segment.continuationPrefix; return line ? `${prefix}${line}` : prefix.trimEnd(); }); lines[lines.length - 1] += segment.suffix; return lines; } function headingAnchorRenames(before, after) { const renames = new Map(); if (before.length !== after.length) { return renames; } before.forEach((id, index) => { if (id !== after[index]) { renames.set(id, after[index]); } }); return renames; } DocumentSegments.headingAnchorRenames = headingAnchorRenames; function rewriteHeadingAnchors(markdown, renames) { if (!renames.size) { return markdown; } const rename = (fragment) => { let decoded = fragment; try { decoded = decodeURIComponent(fragment); } catch { } const renamed = renames.get(decoded); if (renamed === undefined) { return fragment; } return decoded === fragment ? renamed : encodeURIComponent(renamed); }; let fence; return markdown .split("\n") .map(line => { const match = line.match(FENCE_PATTERN); if (fence) { if (match && match[2][0] === fence.marker && match[2].length >= fence.length && !match[3].trim()) { fence = undefined; } return line; } if (match) { fence = { marker: match[2][0], length: match[2].length }; return line; } return line.replace(/(`+)[\s\S]*?(?]+)/g, (whole, ticks, lead, fragment) => { return ticks ? whole : `${lead}#${rename(fragment)}`; }); }) .join("\n"); } DocumentSegments.rewriteHeadingAnchors = rewriteHeadingAnchors; })(DocumentSegments = TabLocalFiles.DocumentSegments || (TabLocalFiles.DocumentSegments = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var DocumentTranslation; (function (DocumentTranslation) { const TAB_TRANSLATION_EXTENSION_ID = "iobiajbbjgoiehijaledajjiainhmafb"; const TRANSLATE_MESSAGE = "tabTranslation.previewerTranslateBatch"; const CAPABILITY_MESSAGE = "tabTranslation.previewerTranslateCapability"; const MAX_BLOCKS_PER_BATCH = 24; const MAX_CHARACTERS_PER_BATCH = 20000; const MAX_CONCURRENT_BATCHES = 2; const REQUEST_TIMEOUT_MS = 100000; const OVERLAY_ID = "tlf-document-translate-overlay"; const LANGUAGE_OPTIONS = [ { code: "cmn", label: "简体中文", fileTag: "zh" }, { code: "cmn-Hant", label: "繁體中文", fileTag: "zh-Hant" }, { code: "eng", label: "English", fileTag: "en" }, { code: "jpn", label: "日本語", fileTag: "ja" }, { code: "kor", label: "한국어", fileTag: "ko" }, { code: "fra", label: "Français", fileTag: "fr" }, { code: "deu", label: "Deutsch", fileTag: "de" }, { code: "spa", label: "Español", fileTag: "es" }, { code: "rus", label: "Русский", fileTag: "ru" } ]; let state; function languageOption(code) { return LANGUAGE_OPTIONS.find((option) => option.code === code); } function planRequests(segments, syntax) { const requests = []; const referenceLabels = syntax === "markdown" ? TabLocalFiles.DocumentSegments.referenceLabels(segments) : new Set(); for (const segment of segments) { if (!segment.translatable) { continue; } if (syntax !== "markdown") { requests.push({ id: segment.id, text: segment.text, tokens: [], kinds: [] }); continue; } const protectedText = TabLocalFiles.DocumentSegments.protectInlineMarkdown(segment.text, referenceLabels); if (!TabLocalFiles.DocumentSegments.isTranslatableText(protectedText.text)) { continue; } requests.push({ id: segment.id, ...protectedText }); } return requests; } DocumentTranslation.planRequests = planRequests; function planBatches(requests, maxBlocks, maxCharacters) { const batches = []; let current = []; let characters = 0; for (const request of requests) { if (current.length > 0 && (current.length >= maxBlocks || characters + request.text.length > maxCharacters)) { batches.push(current); current = []; characters = 0; } current.push(request); characters += request.text.length; } if (current.length) { batches.push(current); } return batches; } DocumentTranslation.planBatches = planBatches; function collectTranslation(request, translated, syntax) { const trimmed = cleanTranslatorOutput(request.text, translated); if (!trimmed) { return undefined; } if (syntax !== "markdown") { return trimmed; } return TabLocalFiles.DocumentSegments.finishTranslation(request, trimmed); } DocumentTranslation.collectTranslation = collectTranslation; const QUOTE_PAIRS = [ ["“", "”"], ["\"", "\""], ["「", "」"], ["『", "』"], ["‘", "’"], ["'", "'"], ["«", "»"] ]; function cleanTranslatorOutput(sourceText, output) { let text = output.trim(); if (!sourceText.includes("\\\"")) { text = text.replace(/\\"/g, "\""); } if (!/[ \t]\n/.test(sourceText)) { text = text.replace(/[ \t]+\n/g, "\n"); } const source = sourceText.trim(); const sourceQuoted = QUOTE_PAIRS.some(([open, close]) => source.startsWith(open) || source.endsWith(close)); for (const [open, close] of QUOTE_PAIRS) { if (sourceQuoted || text.length < 2 || !text.startsWith(open) || !text.endsWith(close)) { continue; } const inner = text.slice(open.length, -close.length); if (isBalanced(inner, open, close)) { return inner.trim(); } } return text; } DocumentTranslation.cleanTranslatorOutput = cleanTranslatorOutput; function isBalanced(text, open, close) { if (open === close) { return text.split(open).length % 2 === 1; } let depth = 0; for (const character of text) { depth += character === open ? 1 : character === close ? -1 : 0; if (depth < 0) { return false; } } return depth === 0; } function sameWording(first, second) { const wording = (text) => text.replace(/\s+/g, "").replace(/[“”„‟"]/g, "\"").replace(/[‘’‚‛']/g, "'"); return wording(first) === wording(second); } function reviewTranslation(request, output, syntax, targetCode) { if (sameWording(cleanTranslatorOutput(request.text, output), request.text)) { return TabLocalFiles.DocumentLanguage.isProvablyForeign(request.text, targetCode) ? { status: "skipped" } : { status: "unchanged" }; } const text = collectTranslation(request, output, syntax); return text === undefined ? { status: "rejected" } : { status: "translated", text }; } DocumentTranslation.reviewTranslation = reviewTranslation; function requestsToTranslate(requests, targetCode) { return requests.filter((request) => !TabLocalFiles.DocumentLanguage.isInTargetLanguage(request.text, targetCode)); } DocumentTranslation.requestsToTranslate = requestsToTranslate; function composeDocument(sourceText, segments, translations, mode, syntax) { const merged = TabLocalFiles.DocumentSegments.merge(segments, translations, mode); if (mode !== "replace" || syntax !== "markdown") { return merged; } const renames = TabLocalFiles.DocumentSegments.headingAnchorRenames(TabLocalFiles.Markdown.headingIds(sourceText), TabLocalFiles.Markdown.headingIds(merged)); return TabLocalFiles.DocumentSegments.rewriteHeadingAnchors(merged, renames); } DocumentTranslation.composeDocument = composeDocument; function exportFileName(fileName, targetCode, extension) { const baseName = fileName.replace(/\.[^.]+$/, ""); const tag = languageOption(targetCode)?.fileTag || targetCode; return `${baseName}.${tag}${extension}`; } DocumentTranslation.exportFileName = exportFileName; function sendToTranslationService(payload) { return new Promise((resolve, reject) => { const runtime = globalThis .chrome?.runtime; if (!runtime?.sendMessage) { reject(new Error("runtime unavailable")); return; } let settled = false; const timeoutId = window.setTimeout(() => { if (!settled) { settled = true; reject(new Error("timeout")); } }, REQUEST_TIMEOUT_MS); try { runtime.sendMessage(TAB_TRANSLATION_EXTENSION_ID, payload, (response) => { if (settled) { return; } settled = true; window.clearTimeout(timeoutId); const lastError = runtime.lastError?.message; if (lastError || !response) { reject(new Error(lastError || "no response")); return; } resolve(response); }); } catch (error) { if (!settled) { settled = true; window.clearTimeout(timeoutId); reject(error instanceof Error ? error : new Error(String(error))); } } }); } function bind(context, kind) { TabLocalFiles.onToolbarAction("document.translate", () => { const text = TabLocalFiles.TextEditor.getText(kind.editorRootId) || ""; const segments = kind.syntax === "markdown" ? TabLocalFiles.DocumentSegments.segmentMarkdown(text) : TabLocalFiles.DocumentSegments.segmentPlainText(text); const requests = planRequests(segments, kind.syntax); if (!requests.length) { TabLocalFiles.showToast(TabLocalFiles.t("document.translateNothing"), "neutral"); return; } state = { context, kind, sourceText: text, segments, requests, mode: "translation-first", targetCode: "cmn", output: "new", overwriteAllowed: TabLocalFiles.isEditableLocalFileSourceUrl(context.sourceUrl), running: false, cancelled: false, requestSequence: 0 }; openDialog(); }); if (context.mountId !== undefined) { TabLocalFiles.registerDocumentMountCleanup(context.mountId, () => { const run = state; if (!run || run.context.mountId !== context.mountId) { return; } if (run.running) { recordRunResult(run, "cancelled"); } run.cancelled = true; state = undefined; document.getElementById(OVERLAY_ID)?.remove(); }); } } DocumentTranslation.bind = bind; function recordRunResult(run, result, failureReason) { TabLocalFiles.recordFileTranslateResult(run.context, { kind: "document", result, failureReason, output: run.output === "overwrite" && run.overwriteAllowed ? "overwrite" : "export", targetCode: run.targetCode, blockCount: requestsToTranslate(run.requests, run.targetCode).length, mode: run.mode }); } function openDialog() { const overlay = ensureOverlay(); const run = state; if (!overlay || !run) { return; } overlay.hidden = false; setRunning(false); syncForm(); syncSummary(); setError(""); setProgress(undefined); void probeCapability(run); overlay.querySelector("[data-tlf-translate-language]")?.focus(); } async function probeCapability(run) { try { const response = await sendToTranslationService({ type: CAPABILITY_MESSAGE }); if (isRunMounted(run) && !response.ok) { setError(TabLocalFiles.t("document.translateUnavailable")); } } catch { if (isRunMounted(run)) { setError(TabLocalFiles.t("document.translateUnavailable")); } } } function ensureOverlay() { let overlay = document.getElementById(OVERLAY_ID); if (overlay) { return overlay; } overlay = document.createElement("div"); overlay.id = OVERLAY_ID; overlay.className = "tlf-translate-overlay"; overlay.hidden = true; overlay.innerHTML = ` `; document.body.appendChild(overlay); overlay.addEventListener("click", (event) => { if (event.target === overlay) { requestClose(); return; } const target = event.target; if (target?.closest("[data-tlf-translate-cancel]")) { requestClose(); } else if (target?.closest("[data-tlf-translate-start]")) { void startTranslation(); } }); overlay.addEventListener("keydown", (event) => { if (event.key === "Escape") { event.stopPropagation(); requestClose(); } }); overlay.addEventListener("change", (event) => { const input = event.target; if (!state || !input) { return; } if (input instanceof HTMLSelectElement && input.dataset.tlfTranslateLanguage !== undefined) { state.targetCode = input.value; } else if (input instanceof HTMLSelectElement && input.dataset.tlfTranslateMode !== undefined) { state.mode = readTranslationMode(input.value); } else if (input instanceof HTMLInputElement && input.name === "tlf-document-translate-output") { state.output = input.value === "overwrite" ? "overwrite" : "new"; } syncSummary(); }); return overlay; } function readTranslationMode(value) { if (value === "replace" || value === "original-first") { return value; } return "translation-first"; } function overlayElement(selector) { return document.getElementById(OVERLAY_ID)?.querySelector(selector) || null; } function syncForm() { if (!state) { return; } const language = overlayElement("[data-tlf-translate-language]"); if (language) { language.value = state.targetCode; } const overlay = document.getElementById(OVERLAY_ID); const mode = overlayElement("[data-tlf-translate-mode]"); if (mode) { mode.value = state.mode; } const overwriteInput = overlay?.querySelector('input[name="tlf-document-translate-output"][value="overwrite"]'); if (overwriteInput) { overwriteInput.disabled = !state.overwriteAllowed; if (!state.overwriteAllowed) { state.output = "new"; } } overlay?.querySelectorAll('input[name="tlf-document-translate-output"]').forEach((input) => { input.checked = input.value === state?.output; }); } function syncSummary() { const summary = overlayElement("[data-tlf-translate-summary]"); if (!summary || !state) { return; } const count = requestsToTranslate(state.requests, state.targetCode).length; const start = overlayElement("[data-tlf-translate-start]"); if (start) { start.disabled = state.running || count === 0; } if (!count) { summary.textContent = TabLocalFiles.t("document.translateNothing"); return; } summary.textContent = state.output === "overwrite" ? TabLocalFiles.t("document.translateSummaryOverwrite", { count }) : TabLocalFiles.t("document.translateSummaryExport", { count, fileName: exportFileName(state.context.fileName, state.targetCode, state.kind.extension) }); } function setError(message) { const error = overlayElement("[data-tlf-translate-error]"); if (!error) { return; } error.textContent = message; error.hidden = !message; } function setProgress(progress) { const container = overlayElement("[data-tlf-translate-progress]"); const bar = overlayElement("[data-tlf-translate-progress-bar]"); const text = overlayElement("[data-tlf-translate-progress-text]"); if (!container || !bar || !text) { return; } if (!progress) { container.hidden = true; bar.style.width = "0%"; text.textContent = ""; return; } container.hidden = false; const ratio = progress.total ? Math.round((progress.completed / progress.total) * 100) : 0; bar.style.width = `${ratio}%`; text.textContent = TabLocalFiles.t("document.translateRunning", { completed: progress.completed, total: progress.total }); } function setRunning(running) { if (state) { state.running = running; } const overlay = document.getElementById(OVERLAY_ID); overlay?.querySelectorAll("input, select").forEach((input) => { if (input instanceof HTMLInputElement && input.value === "overwrite" && !state?.overwriteAllowed) { input.disabled = true; return; } input.disabled = running; }); const start = overlayElement("[data-tlf-translate-start]"); if (start) { start.disabled = running; } } function requestClose() { if (state?.running) { state.cancelled = true; return; } closeDialog(); } function closeDialog() { const overlay = document.getElementById(OVERLAY_ID); if (overlay) { overlay.hidden = true; } state = undefined; } function isRunMounted(run) { return (state === run && (run.context.mountId === undefined || TabLocalFiles.isCurrentDocumentMount(run.context.mountId))); } async function startTranslation() { if (!state || state.running) { return; } const run = state; const pending = requestsToTranslate(run.requests, run.targetCode); if (!pending.length) { return; } run.cancelled = false; setError(""); setRunning(true); const translations = new Map(); let completed = 0; let total = pending.length; let workerFailed = false; let serviceRejected = false; setProgress({ completed, total }); const translateBatches = async (batches, onMiss) => { let nextBatch = 0; const worker = async () => { while (nextBatch < batches.length) { if (run.cancelled || !isRunMounted(run)) { return; } const batch = batches[nextBatch]; nextBatch += 1; run.requestSequence += 1; let response; try { response = await sendToTranslationService({ type: TRANSLATE_MESSAGE, requestId: `document-${run.requestSequence}`, targetCode: run.targetCode, blocks: batch.map((request) => ({ id: request.id, text: request.text })) }); } catch (error) { if (!run.cancelled) { workerFailed = true; } run.cancelled = true; throw error; } if (run.cancelled || !isRunMounted(run)) { return; } if (!response.ok) { workerFailed = true; run.cancelled = true; serviceRejected = true; throw new Error(response.error || "translation failed"); } const answers = new Map((response.blocks || []).map((block) => [block.id, block.text || ""])); for (const request of batch) { const review = reviewTranslation(request, answers.get(request.id) || "", run.kind.syntax, run.targetCode); if (review.status === "translated") { translations.set(request.id, review.text); } else if (review.status !== "unchanged") { onMiss(request, review); } } completed += batch.length; setProgress({ completed, total }); } }; await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT_BATCHES, batches.length) }, () => worker())); }; try { const retries = []; await translateBatches(planBatches(pending, MAX_BLOCKS_PER_BATCH, MAX_CHARACTERS_PER_BATCH), (request) => { retries.push(request); }); let untranslated = 0; if (retries.length && !run.cancelled && isRunMounted(run)) { total += retries.length; setProgress({ completed, total }); await translateBatches(retries.map((request) => [request]), (_request, review) => { if (review.status === "rejected") { untranslated += 1; } }); } if (!isRunMounted(run)) { return; } if (run.cancelled) { recordRunResult(run, "cancelled"); setRunning(false); setProgress(undefined); closeDialog(); TabLocalFiles.showToast(TabLocalFiles.t("document.translateCancelled"), "neutral"); return; } await persistTranslation(run, translations, untranslated); } catch (error) { const userCancelled = run.cancelled && !workerFailed; run.cancelled = true; if (!isRunMounted(run)) { return; } setRunning(false); setProgress(undefined); if (userCancelled) { recordRunResult(run, "cancelled"); closeDialog(); TabLocalFiles.showToast(TabLocalFiles.t("document.translateCancelled"), "neutral"); return; } recordRunResult(run, "failed", serviceRejected ? "service_error" : workerFailed ? TabLocalFiles.translateTransportFailureReason(error) : "unknown"); const message = error instanceof Error ? error.message : String(error); setError(TabLocalFiles.t("document.translateFailed", { error: message })); syncSummary(); } } async function persistTranslation(run, translations, untranslated) { if (!translations.size) { recordRunResult(run, "failed", "empty_result"); setRunning(false); setProgress(undefined); const reason = untranslated ? "document.translateAllRejected" : "document.translateNothing"; setError(TabLocalFiles.t("document.translateFailed", { error: TabLocalFiles.t(reason) })); return; } const merged = composeDocument(run.sourceText, run.segments, translations, run.mode, run.kind.syntax); const translatedCount = translations.size; const keptOriginal = untranslated ? TabLocalFiles.t("document.translateKeptOriginal", { count: untranslated }) : ""; const tone = untranslated ? "neutral" : "success"; if (run.output === "overwrite" && run.overwriteAllowed) { if (TabLocalFiles.TextEditor.getText(run.kind.editorRootId) !== run.sourceText) { recordRunResult(run, "failed", "source_changed"); setRunning(false); setProgress(undefined); setError(TabLocalFiles.t("document.translateSourceChanged")); return; } const saved = await TabLocalFiles.TextEditor.saveReplacement(run.kind.editorRootId, merged, run.sourceText); if (!isRunMounted(run)) { return; } if (!saved) { recordRunResult(run, "failed", "save_failed"); setRunning(false); setProgress(undefined); return; } recordRunResult(run, "success"); closeDialog(); TabLocalFiles.showToast(TabLocalFiles.t("document.translateDoneOverwrite", { count: translatedCount }) + keptOriginal, tone); return; } const fileName = exportFileName(run.context.fileName, run.targetCode, run.kind.extension); TabLocalFiles.saveDerivedTextFile(fileName, merged, TabLocalFiles.mimeTypeFromExtension(run.kind.extension)); recordRunResult(run, "success"); closeDialog(); TabLocalFiles.showToast(TabLocalFiles.t("document.translateDoneExported", { count: translatedCount, fileName }) + keptOriginal, tone); } function exportItem() { return TabLocalFiles.toolbarAction("document.translate", TabLocalFiles.t("document.translate"), "languages", { labelText: TabLocalFiles.t("document.translateShort") }); } DocumentTranslation.exportItem = exportItem; })(DocumentTranslation = TabLocalFiles.DocumentTranslation || (TabLocalFiles.DocumentTranslation = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { const FALLBACK_TEXT_ENCODINGS = ["gb18030", "big5", "windows-1252"]; function currentFileUrl() { return window.location.href; } TabLocalFiles.currentFileUrl = currentFileUrl; function rawFileUrl(sourceUrl = currentFileUrl(), refreshToken) { const url = new URL(sourceUrl); url.searchParams.set("tlf-raw", "1"); if (refreshToken) { url.searchParams.set("tlf-refresh", refreshToken); } else { url.searchParams.delete("tlf-refresh"); } return url.toString(); } TabLocalFiles.rawFileUrl = rawFileUrl; async function readCurrentFileAsText(sourceUrl = currentFileUrl()) { if (isArchiveEntrySourceUrl(sourceUrl)) { const bytes = await readArchiveEntryBytes(sourceUrl); return bytes ? decodeBytes(bytes) : undefined; } const workspaceText = await readTextFromSourceUrlViaWorkspace(sourceUrl); if (workspaceText !== undefined) { return workspaceText; } return readTextViaFetch(sourceUrl); } TabLocalFiles.readCurrentFileAsText = readCurrentFileAsText; async function readCurrentFilePreviewAsText(sourceUrl = currentFileUrl(), maxBytes) { const safeBytes = Math.max(1, Math.floor(maxBytes)); const chunk = await readCurrentFileChunkAsText(sourceUrl, 0, safeBytes); return chunk?.text; } TabLocalFiles.readCurrentFilePreviewAsText = readCurrentFilePreviewAsText; async function readCurrentFileChunkAsText(sourceUrl = currentFileUrl(), offset, size) { const safeOffset = Math.max(0, Math.floor(offset)); const safeSize = Math.max(1, Math.floor(size)); const overlapBefore = safeOffset > 0 ? 3 : 0; const readOffset = Math.max(0, safeOffset - overlapBefore); const readSize = safeSize + overlapBefore + 3; const bytes = await readCurrentFileRangeAsBytes(sourceUrl, readOffset, readSize); if (!bytes) { return undefined; } const requestedStart = safeOffset - readOffset; const requestedEnd = Math.min(bytes.length, requestedStart + safeSize); const startIndex = utf8ChunkStart(bytes, requestedStart); const endIndex = utf8ChunkEnd(bytes, requestedEnd); return { text: decodeBytes(bytes.subarray(startIndex, endIndex)), offset: readOffset + startIndex, endOffset: readOffset + endIndex }; } TabLocalFiles.readCurrentFileChunkAsText = readCurrentFileChunkAsText; async function readCurrentFileRangeAsBytes(sourceUrl = currentFileUrl(), offset, size) { if (isArchiveEntrySourceUrl(sourceUrl)) { const entryBytes = await readCachedArchiveEntryBytes(sourceUrl); if (!entryBytes) { return undefined; } const start = Math.min(Math.max(0, Math.floor(offset)), entryBytes.length); const end = Math.min(start + Math.max(1, Math.floor(size)), entryBytes.length); return entryBytes.subarray(start, end); } const safeOffset = Math.max(0, Math.floor(offset)); const safeSize = Math.max(1, Math.floor(size)); const workspaceBytes = await readBinaryFromSourceUrlViaWorkspace(sourceUrl, { offset: safeOffset, size: safeSize }); if (workspaceBytes) { return workspaceBytes; } return readBinaryRangeViaFetch(sourceUrl, safeOffset, safeSize); } TabLocalFiles.readCurrentFileRangeAsBytes = readCurrentFileRangeAsBytes; async function readTextViaFetch(sourceUrl) { try { const response = await fetch(sourceUrl, { cache: "no-store" }); if (!response.ok) { return undefined; } const bytes = new Uint8Array(await response.arrayBuffer()); return decodeBytes(bytes, charsetFromContentType(response.headers.get("content-type"))); } catch { return undefined; } } async function readBinaryRangeViaFetch(sourceUrl, offset, size) { try { const endOffset = offset + size - 1; const response = await fetch(sourceUrl, { cache: "no-store", headers: { Range: `bytes=${offset}-${Math.max(offset, endOffset)}` } }); if (!response.ok) { return undefined; } const contentLength = Number(response.headers.get("content-length") || "0"); if (response.status !== 206 && (offset > 0 || !contentLength || contentLength > size)) { return undefined; } return new Uint8Array(await response.arrayBuffer()); } catch { return undefined; } } async function readCurrentFileAsArrayBuffer(sourceUrl = currentFileUrl()) { if (TabLocalFiles.PdfMemory.isSourceUrl(sourceUrl)) { return (await TabLocalFiles.PdfMemory.readSnapshot()).data; } if (isArchiveEntrySourceUrl(sourceUrl)) { const entryBytes = await readArchiveEntryBytes(sourceUrl); return entryBytes ? toArrayBuffer(entryBytes) : undefined; } const workspaceBytes = await readBinaryFromSourceUrlViaWorkspace(sourceUrl); if (workspaceBytes) { return toArrayBuffer(workspaceBytes); } try { const response = await fetch(sourceUrl, { cache: "no-store" }); return response.ok ? await response.arrayBuffer() : undefined; } catch { return undefined; } } TabLocalFiles.readCurrentFileAsArrayBuffer = readCurrentFileAsArrayBuffer; let cachedArchive; function clearArchiveCache() { cachedArchive = undefined; cachedArchiveEntry = undefined; } TabLocalFiles.clearArchiveCache = clearArchiveCache; function isArchiveEntrySourceUrl(sourceUrl) { return Boolean(splitArchiveSourceUrl(sourceUrl)?.entryPath); } TabLocalFiles.isArchiveEntrySourceUrl = isArchiveEntrySourceUrl; async function loadArchiveFromUrl(archiveUrl, options) { if (!options?.refresh && cachedArchive?.url === archiveUrl) { return { source: cachedArchive.source, directory: cachedArchive.directory, byteLength: cachedArchive.byteLength }; } const read = await readArchiveBytes(archiveUrl, TabLocalFiles.LargeFile.ARCHIVE_FULL_BYTES); if (read.oversize) { return { error: "too-large" }; } if (!read.bytes) { return { error: "read-failed" }; } try { const source = TabLocalFiles.ZipArchive.bufferSource(read.bytes); const directory = await TabLocalFiles.ZipArchive.readCentralDirectory(source); cachedArchive = { url: archiveUrl, source, directory, byteLength: read.bytes.length }; return { source, directory, byteLength: read.bytes.length }; } catch { return { error: "corrupt", byteLength: read.bytes.length }; } } TabLocalFiles.loadArchiveFromUrl = loadArchiveFromUrl; function loadArchiveForPath(archivePath, options) { return loadArchiveFromUrl(pathToFileUrl(archivePath), options); } TabLocalFiles.loadArchiveForPath = loadArchiveForPath; async function readArchiveEntry(sourceUrl) { const split = splitArchiveSourceUrl(sourceUrl); if (!split?.entryPath) { return { error: "read-failed" }; } const archive = await loadArchiveFromUrl(split.archiveUrl); if (!archive.directory || !archive.source) { return { error: archive.error || "read-failed" }; } const entry = TabLocalFiles.ZipArchive.findEntry(archive.directory, split.entryPath); if (!entry) { return { error: "not-found" }; } try { return { bytes: await TabLocalFiles.ZipArchive.readEntryBytes(archive.source, entry) }; } catch (error) { return { error: TabLocalFiles.ZipArchive.errorCode(error) || "corrupt" }; } } TabLocalFiles.readArchiveEntry = readArchiveEntry; async function readArchiveEntryBytes(sourceUrl) { return (await readArchiveEntry(sourceUrl)).bytes; } TabLocalFiles.readArchiveEntryBytes = readArchiveEntryBytes; let cachedArchiveEntry; async function readCachedArchiveEntryBytes(sourceUrl) { if (cachedArchiveEntry?.url === sourceUrl) { return cachedArchiveEntry.bytes; } const bytes = await readArchiveEntryBytes(sourceUrl); cachedArchiveEntry = bytes ? { url: sourceUrl, bytes } : undefined; return bytes; } async function archiveChildEntries(decodedPath) { const split = TabLocalFiles.ZipArchive.splitVirtualPath(decodedPath); const archivePath = split?.archivePath || decodedPath.replace(/\/+$/, ""); const entryPath = split?.entryPath || ""; const archive = await loadArchiveForPath(archivePath); if (!archive.directory) { return undefined; } return TabLocalFiles.ZipArchive.childEntries(archive.directory, entryPath).map((child) => ({ name: child.name, href: pathToFileUrl(TabLocalFiles.ZipArchive.joinVirtualPath(archivePath, child.path)), kind: child.kind, hidden: isHiddenFileName(child.name), size: child.size })); } TabLocalFiles.archiveChildEntries = archiveChildEntries; async function statArchiveEntry(path) { const split = TabLocalFiles.ZipArchive.splitVirtualPath(path); if (!split) { return undefined; } const archive = await loadArchiveForPath(split.archivePath); if (!archive.directory) { return undefined; } if (!split.entryPath) { return archive.byteLength ? { size: archive.byteLength } : undefined; } const entry = TabLocalFiles.ZipArchive.findEntry(archive.directory, split.entryPath); if (!entry) { return undefined; } return { size: entry.uncompressedSize, mtime: entry.mtime ? new Date(entry.mtime).toISOString() : undefined }; } function splitArchiveSourceUrl(sourceUrl) { try { const url = new URL(sourceUrl); const split = TabLocalFiles.ZipArchive.splitVirtualPath(decodeURIComponent(url.pathname)); if (!split) { return undefined; } const archiveUrl = new URL(url.toString()); archiveUrl.pathname = split.archivePath.split("/").map(encodeURIComponent).join("/"); archiveUrl.search = ""; archiveUrl.hash = ""; return { archiveUrl: archiveUrl.toString(), entryPath: split.entryPath }; } catch { return undefined; } } async function readArchiveBytes(archiveUrl, maxBytes) { try { const response = await fetch(archiveUrl, { cache: "no-store" }); if (!response.ok) { return {}; } if (!response.body) { const buffer = await response.arrayBuffer(); return buffer.byteLength > maxBytes ? { oversize: true } : { bytes: new Uint8Array(buffer) }; } const reader = response.body.getReader(); const chunks = []; let total = 0; for (;;) { const { done, value } = await reader.read(); if (done) { break; } if (!value) { continue; } total += value.length; if (total > maxBytes) { await reader.cancel().catch(() => undefined); return { oversize: true }; } chunks.push(value); } const bytes = new Uint8Array(total); let offset = 0; chunks.forEach((chunk) => { bytes.set(chunk, offset); offset += chunk.length; }); return { bytes }; } catch { return {}; } } async function saveTextAsFile(text, fileName, mimeType) { const blob = new Blob([text], { type: `${mimeType};charset=utf-8` }); return saveBlobViaPickerOrDownload(blob, fileName, mimeType, TabLocalFiles.t("common.textFile"), [".txt"]); } TabLocalFiles.saveTextAsFile = saveTextAsFile; async function saveTextToOriginalFile(path, text, _fallbackFileName, _mimeType, expectedMtime) { rejectArchiveEntryWrite(path); const profile = TabLocalFiles.TextCodec.profileFor(path); const encoded = TabLocalFiles.TextCodec.encode(TabLocalFiles.TextCodec.emit(text, profile), profile); if (encoded.lossy && profile) { TabLocalFiles.showToast(TabLocalFiles.t("editor.savedAsUtf8", { encoding: profile.encoding.toUpperCase() }), "neutral"); TabLocalFiles.TextCodec.rememberProfile(path, { ...profile, encoding: "utf-8", hasBom: false }); } await writeBinaryToWorkspaceFile(path, encoded.bytes, expectedMtime); return "workspace"; } TabLocalFiles.saveTextToOriginalFile = saveTextToOriginalFile; async function saveBytesToOriginalFile(path, bytes, _fallbackFileName, _mimeType, expectedMtime) { rejectArchiveEntryWrite(path); await writeBinaryToWorkspaceFile(path, bytes, expectedMtime); return "workspace"; } TabLocalFiles.saveBytesToOriginalFile = saveBytesToOriginalFile; async function readTextFromOriginalFile(path) { try { if (TabLocalFiles.ZipArchive.isVirtualPath(path)) { const bytes = await readArchiveEntryBytes(pathToFileUrl(path)); return bytes ? decodeBytes(bytes) : undefined; } return ingestDecodedText(path, await readTextFromWorkspaceFile(path)); } catch { return undefined; } } TabLocalFiles.readTextFromOriginalFile = readTextFromOriginalFile; function ingestDecodedText(path, text) { const ingested = TabLocalFiles.TextCodec.ingest(text, lastDetectedEncoding, lastDetectedBom); TabLocalFiles.TextCodec.rememberProfile(path, ingested.profile); return ingested.text; } TabLocalFiles.ingestDecodedText = ingestDecodedText; async function readBinaryFromOriginalFile(path) { try { if (TabLocalFiles.ZipArchive.isVirtualPath(path)) { return await readArchiveEntryBytes(pathToFileUrl(path)); } return await readBinaryFromWorkspaceFile(path); } catch { return undefined; } } TabLocalFiles.readBinaryFromOriginalFile = readBinaryFromOriginalFile; function rejectArchiveEntryWrite(path) { if (TabLocalFiles.ZipArchive.isVirtualPath(path)) { throw new Error(TabLocalFiles.t("zip.readOnly")); } } async function statOriginalFile(path) { if (TabLocalFiles.ZipArchive.isVirtualPath(path)) { return statArchiveEntry(path); } try { const workspacePath = workspacePathForEntry(path); return await withWorkspace(workspacePath.rootPath, { writable: false }, async (client) => { const entry = await client.getEntryInfo(workspacePath.relativePath); return { size: entry.size, birthtime: formatWorkspaceTimestamp(entry.btime), mtime: formatWorkspaceTimestamp(entry.mtime) }; }); } catch { return undefined; } } TabLocalFiles.statOriginalFile = statOriginalFile; async function listDirectoryEntries(path) { if (TabLocalFiles.ZipArchive.isVirtualPath(path) || TabLocalFiles.ZipArchive.isArchivePath(path)) { return archiveChildEntries(path); } try { const rootPath = normalizeDirectoryPath(path); return await withWorkspace(rootPath, { writable: false }, async (client) => { const entries = await client.listDirectory(TabLocalFiles.TabLocalWorkspace.ROOT_PATH, { maxEntries: TabLocalFiles.LargeFile.DIRECTORY_ENTRY_LIMIT + 1 }); return entries.map((entry) => workspaceEntryToLocalDirectoryEntry(rootPath, entry)); }); } catch { return undefined; } } TabLocalFiles.listDirectoryEntries = listDirectoryEntries; async function openPathInFileManager(path) { try { const targetPath = TabLocalFiles.ZipArchive.splitVirtualPath(path)?.archivePath || path; const workspacePath = workspacePathForEntry(targetPath); return await withWorkspace(workspacePath.rootPath, { writable: false }, (client) => client.revealInFileManager(workspacePath.relativePath)); } catch { return false; } } TabLocalFiles.openPathInFileManager = openPathInFileManager; async function createTextDocument(directoryPath, fileName, text) { if (!directoryPath) { return { ok: false, error: TabLocalFiles.t("create.missingDirectory") }; } try { const rootPath = normalizeDirectoryPath(directoryPath); await withWorkspace(rootPath, { writable: true }, async (client) => { await client.createFile(fileName); try { await client.writeTextFile(fileName, text, { overwrite: true }); } catch (error) { await client.deleteEntry(fileName).catch(() => undefined); throw error; } }); const path = `${rootPath}${fileName}`; return { ok: true, path, fileUrl: pathToFileUrl(path) }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } } TabLocalFiles.createTextDocument = createTextDocument; async function renameOriginalFile(path, newFileName) { if (TabLocalFiles.ZipArchive.isVirtualPath(path)) { return { ok: false, error: TabLocalFiles.t("zip.readOnly") }; } try { const workspacePath = workspacePathForEntry(path); const movedEntry = await withWorkspace(workspacePath.rootPath, { writable: true }, (client) => client.moveEntry(workspacePath.relativePath, newFileName)); const movedPath = `${workspacePath.rootPath}${movedEntry.path || newFileName}`; return { ok: true, path: movedPath, fileUrl: pathToFileUrl(movedPath) }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } } TabLocalFiles.renameOriginalFile = renameOriginalFile; async function createUniqueDirectory(parentPath, baseName) { const rootPath = normalizeDirectoryPath(parentPath); try { const name = await withWorkspace(rootPath, { writable: true }, async (client) => { const existing = (await client.listDirectory(TabLocalFiles.TabLocalWorkspace.ROOT_PATH)).map((entry) => entry.name); const candidate = uniqueEntryName(baseName, existing); await client.createDirectory(candidate); return candidate; }); return { ok: true, path: `${rootPath}${name}/` }; } catch (error) { return { ok: false, error: errorMessage(error) }; } } TabLocalFiles.createUniqueDirectory = createUniqueDirectory; function uniqueEntryName(baseName, existingNames) { const taken = new Set(existingNames.map((name) => name.replace(/\/+$/, "").toLowerCase())); for (let index = 1; index <= 999; index += 1) { const candidate = index === 1 ? baseName : `${baseName} ${index}`; if (!taken.has(candidate.toLowerCase())) { return candidate; } } return `${baseName} ${Date.now()}`; } TabLocalFiles.uniqueEntryName = uniqueEntryName; async function renameEntry(path, newName) { const { parentPath, name, isDirectory } = splitEntryPath(path); if (!name || TabLocalFiles.ZipArchive.isVirtualPath(path)) { return { ok: false, error: TabLocalFiles.t("zip.readOnly") }; } try { await withWorkspace(parentPath, { writable: true }, (client) => client.moveEntry(name, newName)); return { ok: true, path: `${parentPath}${newName}${isDirectory ? "/" : ""}` }; } catch (error) { return { ok: false, error: errorMessage(error) }; } } TabLocalFiles.renameEntry = renameEntry; async function moveEntryToDirectory(sourcePath, targetDirectory) { const { parentPath, name, isDirectory } = splitEntryPath(sourcePath); const targetPath = normalizeDirectoryPath(targetDirectory); if (!name || TabLocalFiles.ZipArchive.isVirtualPath(sourcePath) || TabLocalFiles.ZipArchive.isVirtualPath(targetPath)) { return { ok: false, error: TabLocalFiles.t("zip.readOnly") }; } const rootPath = commonAncestorDirectory(parentPath, targetPath); try { await withWorkspace(rootPath, { writable: true }, (client) => client.moveEntry(`${parentPath.slice(rootPath.length)}${name}`, `${targetPath.slice(rootPath.length)}${name}`, { overwrite: false })); return { ok: true, path: `${targetPath}${name}${isDirectory ? "/" : ""}` }; } catch (error) { return { ok: false, error: errorMessage(error) }; } } TabLocalFiles.moveEntryToDirectory = moveEntryToDirectory; async function moveEntryToTrash(path) { const { parentPath, name } = splitEntryPath(path); if (!name || TabLocalFiles.ZipArchive.isVirtualPath(path)) { return { ok: false, error: TabLocalFiles.t("zip.readOnly") }; } try { await withWorkspace(parentPath, { writable: true }, (client) => client.deleteEntry(name, { moveToTrash: true })); return { ok: true }; } catch (error) { return { ok: false, error: errorMessage(error) }; } } TabLocalFiles.moveEntryToTrash = moveEntryToTrash; function splitEntryPath(path) { const isDirectory = path.endsWith("/"); const trimmed = path.replace(/\/+$/, ""); const slashIndex = trimmed.lastIndexOf("/"); return { parentPath: normalizeDirectoryPath(slashIndex >= 0 ? trimmed.slice(0, slashIndex + 1) : "/"), name: trimmed.slice(slashIndex + 1), isDirectory }; } TabLocalFiles.splitEntryPath = splitEntryPath; function commonAncestorDirectory(left, right) { const leftSegments = normalizeDirectoryPath(left).split("/").filter(Boolean); const rightSegments = normalizeDirectoryPath(right).split("/").filter(Boolean); const shared = []; while (shared.length < leftSegments.length && leftSegments[shared.length] === rightSegments[shared.length]) { shared.push(leftSegments[shared.length]); } return shared.length ? `/${shared.join("/")}/` : "/"; } TabLocalFiles.commonAncestorDirectory = commonAncestorDirectory; function errorMessage(error) { return error instanceof Error ? error.message : String(error); } async function saveBlobAsFile(blob, fileName, mimeType) { return saveBlobViaPickerOrDownload(blob, fileName, mimeType, TabLocalFiles.t("common.filePickerLocalFile"), []); } TabLocalFiles.saveBlobAsFile = saveBlobAsFile; async function saveRemoteSourceFile(sourceUrl, fallbackFileName) { const response = await fetch(sourceUrl, { cache: "no-store" }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const responseBlob = await response.blob(); const fileName = sanitizeDownloadFileName(fileNameFromContentDisposition(response.headers.get("content-disposition"))) || sanitizeDownloadFileName(fileNameFromSourceUrl(sourceUrl)) || sanitizeDownloadFileName(fallbackFileName) || TabLocalFiles.t("common.localFile"); const extension = extensionFromFileName(fileName); const mimeType = normalizedMimeType(responseBlob.type || response.headers.get("content-type")) || (extension ? mimeTypeFromExtension(extension) : "application/octet-stream"); const blob = responseBlob.type === mimeType ? responseBlob : new Blob([responseBlob], { type: mimeType }); return saveBlobViaPickerOrDownload(blob, fileName, mimeType, TabLocalFiles.t("common.filePickerLocalFile"), []); } TabLocalFiles.saveRemoteSourceFile = saveRemoteSourceFile; async function saveBlobViaPickerOrDownload(blob, fileName, mimeType, description, fallbackExtensions) { const picker = window.showSaveFilePicker; if (picker) { const extension = extensionFromFileName(fileName); const handle = await picker({ suggestedName: fileName, types: [ { description, accept: { [mimeType]: extension ? [extension] : fallbackExtensions } } ] }); const writable = await handle.createWritable(); await writable.write(blob); await writable.close(); return "picker"; } downloadBlob(blob, fileName); return "download"; } function downloadBlob(blob, fileName) { const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = fileName; anchor.style.display = "none"; document.body.append(anchor); anchor.click(); anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 1000); } TabLocalFiles.downloadBlob = downloadBlob; function fileNameFromContentDisposition(header) { if (!header) { return undefined; } const encodedMatch = header.match(/filename\*\s*=\s*([^;]+)/i); if (encodedMatch) { const encodedValue = stripHeaderFileNameQuotes(encodedMatch[1]); const value = encodedHeaderFileNameValue(encodedValue); return decodeFileNameValue(value); } const quotedMatch = header.match(/filename\s*=\s*"([^"]+)"/i); if (quotedMatch) { return decodeFileNameValue(quotedMatch[1]); } const plainMatch = header.match(/filename\s*=\s*([^;]+)/i); return plainMatch ? decodeFileNameValue(stripHeaderFileNameQuotes(plainMatch[1])) : undefined; } function fileNameFromSourceUrl(sourceUrl) { try { const segment = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop(); return segment ? decodeURIComponent(segment) : undefined; } catch { return undefined; } } function stripHeaderFileNameQuotes(value) { return value.trim().replace(/^['"]|['"]$/g, ""); } function encodedHeaderFileNameValue(value) { const firstQuote = value.indexOf("'"); const secondQuote = firstQuote >= 0 ? value.indexOf("'", firstQuote + 1) : -1; return secondQuote >= 0 ? value.slice(secondQuote + 1) : value; } function decodeFileNameValue(value) { try { return decodeURIComponent(value); } catch { return value; } } function sanitizeDownloadFileName(fileName) { const sanitized = (fileName || "").trim().replace(/[\\/:*?"<>|\u0000-\u001f]/g, "_").replace(/\.+$/g, ""); if (!sanitized || sanitized === "." || sanitized === "..") { return ""; } return sanitized.slice(0, 180); } function normalizedMimeType(value) { const mimeType = value?.split(";")[0].trim().toLowerCase(); return mimeType || undefined; } function extensionFromFileName(fileName) { const match = fileName.toLowerCase().match(/\.[^.]+$/); return match ? match[0] : ""; } TabLocalFiles.extensionFromFileName = extensionFromFileName; function fileNameWithExtension(fileName, extension) { const currentExtension = extensionFromFileName(fileName); if (!currentExtension) { return `${fileName}${extension}`; } return `${fileName.slice(0, -currentExtension.length)}${extension}`; } TabLocalFiles.fileNameWithExtension = fileNameWithExtension; function mimeTypeFromExtension(extension) { const mimeTypes = { ".bib": "text/plain", ".c": "text/x-c", ".cpp": "text/x-c++", ".dfxp": "application/ttml+xml", ".idx": "text/plain", ".lrc": "text/plain", ".mpl": "text/plain", ".sbv": "text/plain", ".smi": "text/plain", ".ssa": "text/plain", ".srt": "text/plain", ".sub": "text/plain", ".ttml": "application/ttml+xml", ".vtt": "text/vtt", ".ass": "text/plain", ".csv": "text/csv", ".css": "text/css", ".go": "text/x-go", ".h": "text/x-c", ".hpp": "text/x-c++", ".html": "text/html", ".htm": "text/html", ".xhtml": "application/xhtml+xml", ".xht": "application/xhtml+xml", ".xhtm": "application/xhtml+xml", ".text_html": "text/html", ".java": "text/x-java-source", ".js": "text/javascript", ".json": "application/json", ".jsx": "text/javascript", ".log": "text/plain", ".md": "text/markdown", ".markdown": "text/markdown", ".py": "text/x-python", ".rs": "text/x-rust", ".rtf": "application/rtf", ".sh": "text/x-shellscript", ".sql": "application/sql", ".ts": "text/typescript", ".tsx": "text/typescript", ".tsv": "text/tab-separated-values", ".txt": "text/plain", ".xml": "application/xml", ".yaml": "application/yaml", ".yml": "application/yaml" }; return mimeTypes[extension] || "text/plain"; } TabLocalFiles.mimeTypeFromExtension = mimeTypeFromExtension; function formatBytes(bytes) { if (!Number.isFinite(bytes) || bytes <= 0) { return "0 B"; } const units = ["B", "KB", "MB", "GB"]; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`; } TabLocalFiles.formatBytes = formatBytes; function lineCount(value) { return value ? value.split(/\r\n|\r|\n/).length : 0; } TabLocalFiles.lineCount = lineCount; function toArrayBuffer(bytes) { return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); } async function readTextFromSourceUrlViaWorkspace(sourceUrl) { const filePath = filePathFromSourceUrl(sourceUrl); if (!filePath || filePath.endsWith("/")) { return undefined; } try { return await readTextFromWorkspaceFile(filePath); } catch { return undefined; } } async function readBinaryFromSourceUrlViaWorkspace(sourceUrl, options) { const filePath = filePathFromSourceUrl(sourceUrl); if (!filePath || filePath.endsWith("/")) { return undefined; } try { return await readBinaryFromWorkspaceFile(filePath, options); } catch { return undefined; } } async function readTextFromWorkspaceFile(path) { return decodeBytes(await readBinaryFromWorkspaceFile(path)); } async function readBinaryFromWorkspaceFile(path, options) { const workspacePath = workspacePathForEntry(path); const buffer = await withWorkspace(workspacePath.rootPath, { writable: false }, (client) => client.readFile(workspacePath.relativePath, options)); return new Uint8Array(buffer); } let lastDetectedEncoding; let lastDetectedBom = false; function recordDetectedTextEncoding(encoding, hasBom = false) { lastDetectedEncoding = encoding; lastDetectedBom = hasBom; } TabLocalFiles.recordDetectedTextEncoding = recordDetectedTextEncoding; function lastDetectedTextEncoding() { return lastDetectedEncoding; } TabLocalFiles.lastDetectedTextEncoding = lastDetectedTextEncoding; function decodeBytes(bytes, preferredEncoding) { const bomEncoding = byteOrderMarkEncoding(bytes); if (bomEncoding) { recordDetectedTextEncoding(bomEncoding, true); return decodeBytesWithEncoding(bytes, bomEncoding) || ""; } const normalizedPreferred = normalizeEncodingLabel(preferredEncoding); if (normalizedPreferred && normalizedPreferred !== "utf-8") { const preferred = decodeBytesWithEncoding(bytes, normalizedPreferred); if (preferred !== undefined) { recordDetectedTextEncoding(normalizedPreferred); return preferred; } } const utf8 = decodeBytesWithEncoding(bytes, "utf-8", true); if (utf8 !== undefined) { recordDetectedTextEncoding("utf-8"); return utf8; } for (const encoding of FALLBACK_TEXT_ENCODINGS) { const decoded = decodeBytesWithEncoding(bytes, encoding); if (decoded !== undefined) { recordDetectedTextEncoding(encoding); return decoded; } } recordDetectedTextEncoding(undefined); return new TextDecoder("utf-8").decode(bytes); } function decodeBytesWithEncoding(bytes, encoding, fatal = false) { try { return new TextDecoder(encoding, { fatal }).decode(bytes); } catch { return undefined; } } function byteOrderMarkEncoding(bytes) { if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) { return "utf-8"; } if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) { return "utf-16le"; } if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) { return "utf-16be"; } return undefined; } function charsetFromContentType(contentType) { return contentType?.match(/charset\s*=\s*["']?([^;"'\s]+)/i)?.[1]; } function normalizeEncodingLabel(label) { const normalized = label?.trim().toLowerCase(); if (!normalized) { return undefined; } if (normalized === "gbk" || normalized === "gb2312" || normalized === "chinese") { return "gb18030"; } return normalized; } function isUtf8ContinuationByte(byte) { return (byte & 0xc0) === 0x80; } function utf8ChunkStart(bytes, requestedStart) { let index = Math.min(Math.max(0, requestedStart), bytes.length); while (index > 0 && isUtf8ContinuationByte(bytes[index])) { index -= 1; } return index; } function utf8ChunkEnd(bytes, requestedEnd) { let index = Math.min(Math.max(0, requestedEnd), bytes.length); while (index > 0 && index < bytes.length && isUtf8ContinuationByte(bytes[index])) { index -= 1; } return index; } async function writeTextToWorkspaceFile(path, text, expectedMtime) { const workspacePath = workspacePathForEntry(path); await withWorkspace(workspacePath.rootPath, { writable: true, currentFileWrite: true }, (client) => client.writeTextFile(workspacePath.relativePath, text, writeOptions(expectedMtime))); } async function writeBinaryToWorkspaceFile(path, bytes, expectedMtime) { const workspacePath = workspacePathForEntry(path); await withWorkspace(workspacePath.rootPath, { writable: true, currentFileWrite: true }, (client) => client.writeFile(workspacePath.relativePath, toArrayBuffer(bytes), writeOptions(expectedMtime))); } async function writeBinaryIntoSubdirectory(directoryPath, subdirectory, fileName, bytes) { await withWorkspace(normalizeDirectoryPath(directoryPath), { writable: true }, async (client) => { try { await client.createDirectory(subdirectory); } catch { } await client.writeFile(`${subdirectory}/${fileName}`, toArrayBuffer(bytes), { overwrite: false }); }); } TabLocalFiles.writeBinaryIntoSubdirectory = writeBinaryIntoSubdirectory; function saveDerivedTextFile(fileName, text, mimeType) { downloadBlob(new Blob([text], { type: `${mimeType};charset=utf-8` }), fileName); } TabLocalFiles.saveDerivedTextFile = saveDerivedTextFile; function writeOptions(expectedMtime) { return Number.isFinite(expectedMtime) ? { overwrite: true, expectedMtime } : { overwrite: true }; } async function withWorkspace(rootPath, options, handler) { const client = await TabLocalFiles.TabLocalWorkspace.createClient(pathToFileUrl(rootPath), options); try { return await handler(client); } finally { await client.release().catch(() => undefined); } } function workspacePathForEntry(path) { if (path.endsWith("/")) { return { rootPath: normalizeDirectoryPath(path), relativePath: TabLocalFiles.TabLocalWorkspace.ROOT_PATH }; } const slashIndex = path.lastIndexOf("/"); const rootPath = slashIndex >= 0 ? path.slice(0, slashIndex + 1) : "/"; return { rootPath: normalizeDirectoryPath(rootPath), relativePath: path.slice(slashIndex + 1) }; } function workspaceEntryToLocalDirectoryEntry(rootPath, entry) { const isDirectory = entry.kind === "directory"; const entryName = isDirectory && !entry.name.endsWith("/") ? `${entry.name}/` : entry.name; const entryPath = `${rootPath}${entry.path || entry.name}${isDirectory && !(entry.path || entry.name).endsWith("/") ? "/" : ""}`; return { name: entryName, href: pathToFileUrl(entryPath), kind: entry.kind, hidden: isHiddenFileName(entry.name), size: Number.isFinite(entry.size) ? entry.size : undefined, mtime: Number.isFinite(entry.mtime) && entry.mtime > 0 ? entry.mtime : undefined }; } function normalizeDirectoryPath(path) { return path.endsWith("/") ? path : `${path}/`; } function pathToFileUrl(pathname) { return `file://${pathname.split("/").map(encodeURIComponent).join("/")}`; } function filePathFromSourceUrl(sourceUrl) { try { const url = new URL(sourceUrl); return url.protocol === "file:" ? decodeURIComponent(url.pathname) : undefined; } catch { return undefined; } } function isHiddenFileName(name) { return name.replace(/\/+$/, "").startsWith("."); } function formatWorkspaceTimestamp(value) { if (value === undefined || !Number.isFinite(value) || value <= 0) { return undefined; } const timestamp = value > 1000000000000 ? value : value * 1000; return new Date(timestamp).toISOString(); } })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { const ANALYTICS_STORED_JSON_MAX_LENGTH = 250; const ANALYTICS_DEVICE_ID_STORAGE_KEY = "analytics_device_id"; const UNKNOWN_VALUE = "unknown"; function buildAnalyticsEvent(input, context, timestamp = Date.now()) { return { type: input.type, module_name: input.moduleName, sub_module_name: input.subModuleName || input.moduleName, name: input.name, content: boundedAnalyticsJson({ value: input.value, ...(input.fields || {}), hostname: analyticsHostname(input.currentUrl) }), timestamp, extra_info: boundedAnalyticsJson({ trigger_method: context.trigger_method, user_id: context.user_id, device_id: context.device_id, browser_version: context.browser_version }) }; } TabLocalFiles.buildAnalyticsEvent = buildAnalyticsEvent; function boundedAnalyticsJson(fields, maxLength = ANALYTICS_STORED_JSON_MAX_LENGTH) { if (!Number.isFinite(maxLength) || maxLength < 2) { return "{}"; } const bounded = {}; Object.entries(fields).forEach(([key, value]) => { if (value === undefined) { return; } const candidate = { ...bounded, [key]: value }; if (analyticsStoredJsonLength(JSON.stringify(candidate)) <= maxLength) { bounded[key] = value; return; } if (typeof value !== "string") { return; } const truncatedValue = longestAnalyticsPrefix(bounded, key, value, maxLength); if (truncatedValue !== undefined) { bounded[key] = truncatedValue; } }); return JSON.stringify(bounded); } TabLocalFiles.boundedAnalyticsJson = boundedAnalyticsJson; async function recordAnalyticsEvent(inputOrEventOrType, moduleName, eventName, options = {}) { if (isAnalyticsEventInput(inputOrEventOrType)) { const context = await getAnalyticsContext(inputOrEventOrType.triggerMethod); return recordRawAnalyticsEvent(buildAnalyticsEvent(inputOrEventOrType, context)); } if (typeof inputOrEventOrType === "string") { return recordRawAnalyticsEvent({ type: inputOrEventOrType, module_name: moduleName || "", sub_module_name: options.sub_module_name || moduleName || "", name: eventName || "", ...options, timestamp: options.timestamp ?? Date.now() }); } return recordRawAnalyticsEvent({ ...inputOrEventOrType, sub_module_name: inputOrEventOrType.sub_module_name || inputOrEventOrType.module_name, timestamp: inputOrEventOrType.timestamp ?? Date.now() }); } TabLocalFiles.recordAnalyticsEvent = recordAnalyticsEvent; function recordRawAnalyticsEvent(event) { const chrome = analyticsChromeApi(); const recordEvent = chrome.tabAnalytics?.recordEvent; if (typeof recordEvent !== "function") { return Promise.resolve(null); } return new Promise((resolve) => { try { recordEvent.call(chrome.tabAnalytics, event, (response) => { if (chrome.runtime?.lastError?.message) { resolve(null); return; } resolve(response || null); }); } catch { resolve(null); } }); } TabLocalFiles.recordRawAnalyticsEvent = recordRawAnalyticsEvent; function recordFileAnalyticsEvent(input) { return recordAnalyticsEvent({ type: input.type, moduleName: "tab_local_files", subModuleName: input.subModuleName, name: input.name, value: input.value, triggerMethod: input.triggerMethod, currentUrl: input.sourceUrl, fields: { ...(input.fields || {}), file_type: input.fileType || UNKNOWN_VALUE, file_extension: analyticsFileExtension(input.fileName, input.sourceUrl), source_protocol: analyticsSourceProtocol(input.sourceUrl) } }); } TabLocalFiles.recordFileAnalyticsEvent = recordFileAnalyticsEvent; function parseAnalyticsBrowserVersion(userAgent) { if (!userAgent) { return UNKNOWN_VALUE; } const edge = userAgent.match(/Edg\/[\d.]+/); if (edge) { return edge[0]; } const chromium = userAgent.match(/(?:Chrome|Chromium)\/[\d.]+/); if (chromium) { return chromium[0]; } const firefox = userAgent.match(/Firefox\/[\d.]+/); if (firefox) { return firefox[0]; } const safari = userAgent.includes("Safari/") ? userAgent.match(/Version\/([\d.]+)/) : null; return safari ? `Safari/${safari[1]}` : UNKNOWN_VALUE; } TabLocalFiles.parseAnalyticsBrowserVersion = parseAnalyticsBrowserVersion; function longestAnalyticsPrefix(current, key, value, maxLength) { const ellipsis = "…"; if (analyticsStoredJsonLength(JSON.stringify({ ...current, [key]: ellipsis })) > maxLength) { return undefined; } let low = 0; let high = value.length; while (low < high) { const middle = Math.ceil((low + high) / 2); const candidate = `${value.slice(0, middle)}${ellipsis}`; if (analyticsStoredJsonLength(JSON.stringify({ ...current, [key]: candidate })) <= maxLength) { low = middle; } else { high = middle - 1; } } return `${value.slice(0, low)}${ellipsis}`; } function analyticsStoredJsonLength(value) { return value.replace(/[&<>"']/g, (character) => { switch (character) { case "&": return "&"; case "<": return "<"; case ">": return ">"; case '"': return """; default: return "'"; } }).length; } function isAnalyticsEventInput(value) { return typeof value === "object" && value !== null && "moduleName" in value; } async function getAnalyticsContext(triggerMethod) { const deviceInfo = await getAnalyticsDeviceInfo(); return { user_id: deviceInfo?.userId || deviceInfo?.user_id || UNKNOWN_VALUE, device_id: deviceInfo?.deviceId || (await getOrCreateAnalyticsDeviceId()), browser_version: deviceInfo?.browserVersion || parseAnalyticsBrowserVersion(analyticsUserAgent()), trigger_method: triggerMethod }; } function getAnalyticsDeviceInfo() { const api = analyticsChromeApi().tabInstance; if (typeof api?.getDeviceInfo !== "function") { return Promise.resolve(null); } return new Promise((resolve) => { try { api.getDeviceInfo?.((deviceInfo) => resolve(deviceInfo || null)); } catch { resolve(null); } }); } async function getOrCreateAnalyticsDeviceId() { const stored = await readAnalyticsStorage(ANALYTICS_DEVICE_ID_STORAGE_KEY); if (typeof stored === "string" && stored) { return stored; } const generated = generateAnalyticsDeviceId(); await writeAnalyticsStorage(ANALYTICS_DEVICE_ID_STORAGE_KEY, generated); return generated; } function readAnalyticsStorage(key) { const storage = analyticsChromeApi().storage?.local; if (typeof storage?.get !== "function") { return Promise.resolve(undefined); } return new Promise((resolve) => { try { storage.get?.(key, (values) => resolve(values?.[key])); } catch { resolve(undefined); } }); } function writeAnalyticsStorage(key, value) { const storage = analyticsChromeApi().storage?.local; if (typeof storage?.set !== "function") { return Promise.resolve(); } return new Promise((resolve) => { try { storage.set?.({ [key]: value }, resolve); } catch { resolve(); } }); } function generateAnalyticsDeviceId() { const cryptoApi = globalThis.crypto; if (typeof cryptoApi?.randomUUID === "function") { return cryptoApi.randomUUID(); } return `fallback-${Date.now()}-${Math.random().toString(16).slice(2)}`; } function analyticsHostname(url) { try { return new URL(url).hostname; } catch { return ""; } } function analyticsFileExtension(fileName, sourceUrl) { const direct = analyticsExtensionFromFileName(fileName); if (direct) { return direct; } try { const pathFileName = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop() || ""; return analyticsExtensionFromFileName(pathFileName); } catch { return ""; } } function analyticsExtensionFromFileName(fileName) { const match = fileName.toLowerCase().match(/\.[^.]+$/); return match ? match[0] : ""; } function analyticsSourceProtocol(url) { try { return new URL(url).protocol.replace(/:$/, ""); } catch { return ""; } } function analyticsUserAgent() { return typeof window === "undefined" ? "" : window.navigator?.userAgent || ""; } function analyticsChromeApi() { return (globalThis.chrome || {}); } })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var Theme; (function (Theme) { const LEGACY_MODE_STORAGE_KEY = "tab-local-files:theme"; const MODE_STORAGE_KEY = "tab-local-files:theme-mode"; const MEDIA_QUERY = "(prefers-color-scheme: dark)"; const PORT_NAME = "tlf-theme"; const PORT_RECONNECT_DELAY_MS = 1000; const CHROMIUM_UNSET_USER_COLOR = -6645094; const TABBIT_DEFAULT_ORANGE = -810932; let activeGeneration = 0; let bindingCleanup; let lastBrowserColors; let lastAppliedTheme; let lastAppliedAccent; let lastAppliedMode; const changeHandlers = new Set(); function start() { stop(); clearLegacyPreference(); const generation = ++activeGeneration; const isActive = () => generation === activeGeneration; applyColors(); void readBrowserThemeColors().then((colors) => { if (isActive() && colors) { lastBrowserColors = colors; applyColors(colors); } }); const mediaQuery = window.matchMedia?.(MEDIA_QUERY); const onSystemThemeChange = () => { if (isActive()) { applyColors(); } }; mediaQuery?.addEventListener("change", onSystemThemeChange); const api = tabThemeColorApi(); const onTabThemeChange = (colors) => { if (!isActive()) { return; } const normalized = normalizeColors(colors); if (typeof normalized?.is_dark_mode === "boolean") { lastBrowserColors = { ...lastBrowserColors, is_dark_mode: normalized.is_dark_mode }; } applyColors(lastBrowserColors); void readBrowserThemeColors().then((refreshed) => { if (isActive() && refreshed) { lastBrowserColors = refreshed; applyColors(refreshed); } }); }; api?.onChanged?.addListener?.(onTabThemeChange); const onUserColorChange = (colors) => { if (!isActive()) { return; } const normalized = normalizeColors(colors); if (!normalized?.user_color) { return; } lastBrowserColors = { ...lastBrowserColors, user_color: normalized.user_color, is_dark_mode: normalized.is_dark_mode ?? lastBrowserColors?.is_dark_mode }; applyColors(lastBrowserColors); }; api?.onUserColorChanged?.addListener?.(onUserColorChange); let port; let reconnectTimer; const connectBackgroundPort = () => { port = connectThemePort((colors) => { if (!isActive()) { return; } lastBrowserColors = colors || undefined; applyColors(lastBrowserColors); }, () => { port = undefined; if (!isActive() || reconnectTimer !== undefined) { return; } reconnectTimer = window.setTimeout(() => { reconnectTimer = undefined; if (isActive()) { connectBackgroundPort(); } }, PORT_RECONNECT_DELAY_MS); }); }; connectBackgroundPort(); bindingCleanup = () => { if (reconnectTimer !== undefined) { window.clearTimeout(reconnectTimer); reconnectTimer = undefined; } mediaQuery?.removeEventListener("change", onSystemThemeChange); api?.onChanged?.removeListener?.(onTabThemeChange); api?.onUserColorChanged?.removeListener?.(onUserColorChange); port?.disconnect?.(); }; } Theme.start = start; function stop() { activeGeneration += 1; bindingCleanup?.(); bindingCleanup = undefined; } Theme.stop = stop; function onChange(handler) { changeHandlers.add(handler); return () => { changeHandlers.delete(handler); }; } Theme.onChange = onChange; function currentState() { const theme = lastAppliedTheme ?? resolveTheme(lastBrowserColors); return { theme, mode: currentMode(), source: browserAccentValue(lastBrowserColors) !== undefined ? "browser" : "fallback" }; } Theme.currentState = currentState; function currentMode() { const storedMode = readStorage(MODE_STORAGE_KEY); return storedMode === "dark" || storedMode === "light" ? storedMode : "browser"; } Theme.currentMode = currentMode; function toggleMode() { const mode = currentMode(); if (mode === "browser") { setMode(resolveTheme(lastBrowserColors) === "light" ? "dark" : "light"); } else { setMode("browser"); } applyColors(); const generation = activeGeneration; void readBrowserThemeColors().then((colors) => { if (generation === activeGeneration && colors) { lastBrowserColors = colors; applyColors(colors); } }); } Theme.toggleMode = toggleMode; function applyColors(colors = lastBrowserColors) { applyResolvedTheme(resolveTheme(colors), colors); } function clearLegacyPreference() { if (readStorage(MODE_STORAGE_KEY)) { return; } removeStorage(LEGACY_MODE_STORAGE_KEY); } function setMode(mode) { if (mode === "browser") { removeStorage(MODE_STORAGE_KEY); return; } writeStorage(MODE_STORAGE_KEY, mode); } function resolveTheme(colors) { const mode = currentMode(); if (mode === "dark" || mode === "light") { return mode; } if (typeof colors?.is_dark_mode === "boolean") { return colors.is_dark_mode ? "dark" : "light"; } return systemTheme(); } function systemTheme() { return window.matchMedia?.(MEDIA_QUERY).matches ? "dark" : "light"; } function applyResolvedTheme(theme, colors) { const seed = browserAccentValue(colors); const mode = currentMode(); const changed = lastAppliedTheme !== theme || lastAppliedAccent !== seed || lastAppliedMode !== mode; lastAppliedTheme = theme; lastAppliedAccent = seed; lastAppliedMode = mode; const root = document.documentElement; root.dataset.tlfTheme = theme; root.dataset.tlfThemeMode = mode; root.dataset.tlfThemeSource = seed !== undefined ? "browser" : "fallback"; root.style.colorScheme = theme; applySeedColor(seed); if (changed) { const state = { theme, mode, source: seed !== undefined ? "browser" : "fallback" }; changeHandlers.forEach((handler) => { try { handler(state); } catch (error) { console.warn("[Previewer] Theme change handler failed", error); } }); } } function applySeedColor(seed) { const style = document.documentElement.style; if (!style?.setProperty || !style.removeProperty) { return; } if (seed === undefined) { style.removeProperty("--primary"); return; } const { r, g, b } = skColorToRgb(seed); style.setProperty("--primary", `rgb(${r}, ${g}, ${b})`); } function browserAccentValue(colors) { return [colors?.user_color, colors?.accent_color, colors?.primary_color, colors?.background_color] .map(seedColorValue) .find((value) => value !== undefined); } function seedColorValue(color) { if (typeof color?.value !== "number" || ((color.value >>> 24) & 255) === 0) { return undefined; } const value = normalizeColorValue(color.value); return isUsableSeed(value) ? value : undefined; } function isUsableSeed(value) { const { r, g, b } = skColorToRgb(value); const brightest = Math.max(r, g, b); const chroma = brightest - Math.min(r, g, b); return chroma > 12 || (brightest > 32 && brightest < 224); } function skColorToRgb(value) { const unsignedValue = value >>> 0; return { r: (unsignedValue >> 16) & 255, g: (unsignedValue >> 8) & 255, b: unsignedValue & 255 }; } async function readBrowserThemeColors() { const directColors = await (readBrowserThemeColorsDirectly() || Promise.resolve(undefined)); if (directColors) { return directColors; } console.info("[Previewer] chrome.tabThemeColor is unavailable or empty in this page context; requesting background theme colors."); return readBrowserThemeColorsFromBackground(); } function readBrowserThemeColorsDirectly() { const api = tabThemeColorApi(); if (!api?.getColors && !api?.getUserColor) { return undefined; } return (async () => { const userColors = api.getUserColor ? await readColorsFromApi(api.getUserColor.bind(api)) : undefined; if (userColors && hasUsableColor(userColors)) { return userColors; } return api.getColors ? readColorsFromApi(api.getColors.bind(api)) : userColors; })(); } function readColorsFromApi(reader) { return new Promise((resolve) => { let settled = false; const finish = (colors) => { if (settled) { return; } settled = true; resolve(normalizeColors(colors) || undefined); }; try { reader(finish); } catch { finish(undefined); } }); } function readBrowserThemeColorsFromBackground() { const runtime = chromeRuntimeApi(); if (!runtime?.sendMessage) { console.warn("[Previewer] chrome.runtime.sendMessage is unavailable; falling back to default orange theme."); return Promise.resolve(undefined); } const sendMessage = runtime.sendMessage.bind(runtime); return new Promise((resolve) => { sendMessage({ type: "tlf-get-tab-theme-colors" }, (response) => { if (runtime.lastError?.message) { console.warn("[Previewer] Failed to request background theme colors; falling back to default orange theme.", runtime.lastError.message); resolve(undefined); return; } if (!response?.ok) { console.warn("[Previewer] Background theme color request failed; falling back to default orange theme.", response); resolve(undefined); return; } const colors = normalizeColors(response.themeColors) || undefined; if (!colors) { console.warn("[Previewer] Background returned no theme colors; falling back to default orange theme."); } resolve(colors); }); }); } function connectThemePort(onColors, onDisconnect) { const runtime = chromeRuntimeApi(); if (!runtime?.connect) { return undefined; } try { const port = runtime.connect({ name: PORT_NAME }); const onMessage = (message) => { if (message?.type !== "tlf-tab-theme-colors") { return; } const colors = normalizeColors(message.colors) || undefined; if (!colors) { console.warn("[Previewer] Background theme port returned no colors; falling back to default orange theme."); } onColors(colors); }; port.onMessage?.addListener(onMessage); const handleDisconnect = () => { port.onMessage?.removeListener?.(onMessage); port.onDisconnect?.removeListener?.(handleDisconnect); onDisconnect(); }; port.onDisconnect?.addListener(handleDisconnect); return { onMessage: port.onMessage, disconnect: () => { port.onMessage?.removeListener?.(onMessage); port.onDisconnect?.removeListener?.(handleDisconnect); port.disconnect?.(); } }; } catch (error) { console.warn("[Previewer] Failed to connect background theme port; falling back to one-shot theme lookup.", error); return undefined; } } function normalizeColors(colors) { if (!colors) { return undefined; } return { ...colors, user_color: normalizeColor(colors.user_color), primary_color: normalizeColor(colors.primary_color), accent_color: normalizeColor(colors.accent_color), background_color: normalizeColor(colors.background_color), text_color: normalizeColor(colors.text_color) }; } function normalizeColor(color) { if (!color) { return undefined; } return { value: normalizeColorValue(color.value) }; } function hasUsableColor(colors) { return browserAccentValue(colors) !== undefined; } function normalizeColorValue(value) { return value === CHROMIUM_UNSET_USER_COLOR ? TABBIT_DEFAULT_ORANGE : value; } function tabThemeColorApi() { return globalThis.chrome?.tabThemeColor; } function chromeRuntimeApi() { return globalThis.chrome?.runtime; } function readStorage(key) { try { return window.localStorage.getItem(key); } catch { return null; } } function writeStorage(key, value) { try { window.localStorage.setItem(key, value); } catch { return; } } function removeStorage(key) { try { window.localStorage.removeItem(key); } catch { return; } } })(Theme = TabLocalFiles.Theme || (TabLocalFiles.Theme = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { function formatShortcut(spec) { if (shortcutPlatform() === "mac") { return `${spec.alt ? "⌥" : ""}${spec.shift ? "⇧" : ""}${spec.mod ? "⌘" : ""}${spec.key}`; } const parts = []; if (spec.mod) { parts.push("Ctrl"); } if (spec.alt) { parts.push("Alt"); } if (spec.shift) { parts.push("Shift"); } parts.push(spec.key); return parts.join("+"); } TabLocalFiles.formatShortcut = formatShortcut; function shortcutPlatform() { const navigatorInfo = globalThis.navigator; const platform = (navigatorInfo?.userAgentData?.platform || navigatorInfo?.platform || "").toLowerCase(); if (platform.includes("mac")) { return "mac"; } if (platform.includes("win")) { return "windows"; } return "other"; } TabLocalFiles.shortcutPlatform = shortcutPlatform; })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var PerformanceMetrics; (function (PerformanceMetrics) { const MAX_SAMPLES = 300; const samples = []; let markSeed = 0; function record(sample) { samples.push(sample); if (samples.length > MAX_SAMPLES) { samples.shift(); } } function safeDetails(details) { const safe = {}; for (const [key, value] of Object.entries(details || {})) { if (typeof value === "number" && Number.isFinite(value)) { safe[key] = value; } } return safe; } function timing() { return typeof performance === "undefined" ? undefined : performance; } function now() { const api = timing(); return typeof api?.now === "function" ? api.now() : Date.now(); } function begin(name, details) { const startedAt = now(); const markId = `tlf:${name}:${(markSeed += 1)}`; timing()?.mark?.(`${markId}:start`); return (moreDetails) => { const durationMs = now() - startedAt; const api = timing(); try { api?.mark?.(`${markId}:end`); api?.measure?.(name, `${markId}:start`, `${markId}:end`); } catch { } api?.clearMarks?.(`${markId}:start`); api?.clearMarks?.(`${markId}:end`); record({ name, durationMs, timestamp: Date.now(), details: { ...safeDetails(details), ...safeDetails(moreDetails) } }); }; } PerformanceMetrics.begin = begin; function observe(name, details) { record({ name, durationMs: 0, timestamp: Date.now(), details: safeDetails(details) }); } PerformanceMetrics.observe = observe; function memory() { const info = timing()?.memory; if (!info || typeof info.usedJSHeapSize !== "number") { return undefined; } return { usedJsHeapBytes: info.usedJSHeapSize, totalJsHeapBytes: info.totalJSHeapSize ?? 0, jsHeapLimitBytes: info.jsHeapSizeLimit ?? 0 }; } function report() { const grouped = new Map(); for (const sample of samples) { const list = grouped.get(sample.name) || []; list.push(sample.durationMs); grouped.set(sample.name, list); } const byName = {}; for (const [name, durations] of grouped) { const sorted = [...durations].sort((a, b) => a - b); const middle = Math.floor(sorted.length / 2); byName[name] = { count: sorted.length, medianMs: sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle], maxMs: sorted[sorted.length - 1] }; } return { samples: [...samples], byName, memory: memory() }; } PerformanceMetrics.report = report; function reset() { samples.length = 0; } PerformanceMetrics.reset = reset; function install() { if (typeof window === "undefined") { return; } window.__tlfPerformance = { report, reset }; } PerformanceMetrics.install = install; })(PerformanceMetrics = TabLocalFiles.PerformanceMetrics || (TabLocalFiles.PerformanceMetrics = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var VendorLoader; (function (VendorLoader) { const KATEX_SCRIPT_PATH = "vendor/katex/katex.min.js"; const KATEX_STYLE_PATH = "vendor/katex/katex.min.css"; const KATEX_STYLE_MARKER = "tlfKatexStyle"; let katexPromise = null; function isKatexReady() { return Boolean(window.katex); } VendorLoader.isKatexReady = isKatexReady; function ensureKatex() { if (isKatexReady()) { ensureKatexStylesheet(); return Promise.resolve(true); } katexPromise ?? (katexPromise = loadKatex()); return katexPromise; } VendorLoader.ensureKatex = ensureKatex; async function loadKatex() { ensureKatexStylesheet(); const scriptUrl = runtimeAssetUrl(KATEX_SCRIPT_PATH); if (!scriptUrl) { return false; } try { await import(scriptUrl); } catch (error) { console.warn("[Previewer] KaTeX failed to load; math stays as source text.", error); return false; } return isKatexReady(); } function ensureKatexStylesheet() { const target = document.head || document.documentElement; if (!target || target.querySelector(`link[data-${cssDataAttribute(KATEX_STYLE_MARKER)}]`)) { return; } const href = runtimeAssetUrl(KATEX_STYLE_PATH); if (!href) { return; } const link = document.createElement("link"); link.rel = "stylesheet"; link.href = href; link.dataset[KATEX_STYLE_MARKER] = "true"; target.append(link); } function cssDataAttribute(datasetKey) { return datasetKey.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); } function runtimeAssetUrl(path) { const runtimeUrl = globalThis.chrome?.runtime?.getURL?.(path); if (runtimeUrl) { return runtimeUrl; } try { return new URL(path, document.baseURI).toString(); } catch { return undefined; } } })(VendorLoader = TabLocalFiles.VendorLoader || (TabLocalFiles.VendorLoader = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { const TOOLBAR_ACTION_EVENT = "tlf:toolbar-action"; const DIRECTORY_SIDEBAR_WIDTH_STORAGE_KEY = "tab-local-files:directory-sidebar-width"; const DIRECTORY_SIDEBAR_MIN_WIDTH = 180; const DIRECTORY_SIDEBAR_MAX_WIDTH = 640; const DIRECTORY_POPUP_VIEWPORT_PADDING = 8; const DIRECTORY_POPUP_GAP = 8; const DIRECTORY_POPUP_MENU_SELECTOR = "[data-tlf-create-document-menu], [data-tlf-directory-favorites-menu], [data-tlf-directory-settings-menu], [data-tlf-file-header-info-menu], [data-tlf-file-path-overflow-menu], [data-tlf-file-header-overflow]"; const DIRECTORY_SIDEBAR_POPUP_PANEL_SELECTOR = ":scope > .tlf-create-document-menu-list, :scope > .tlf-directory-settings-panel, :scope > .tlf-context-overflow-panel"; const TOAST_VISIBLE_MS = 1800; const TOAST_EXIT_MS = 180; const TOAST_HEADER_GAP = 12; const TOAST_VIEWPORT_MARGIN = 16; let themeBindingCleanup; let toolbarEditStateBound = false; let directoryMenuDismissControlsBound = false; let currentDocumentMountId = 0; let currentDocumentCleanup; let toastHideTimer; let toastRemoveTimer; let suppressEditorModeAnalytics = false; let activePrintCleanup; function findEnabledFileType(decodedPath, definitions) { const normalizedPath = decodedPath.toLowerCase(); return definitions.find((definition) => { return (definition.enabled && (definition.pathMatcher?.(decodedPath) || definition.extensions.some((extension) => normalizedPath.endsWith(extension)))); }); } TabLocalFiles.findEnabledFileType = findEnabledFileType; function readRawTextFromDocument() { if (!document.body) { return document.documentElement?.textContent || ""; } const pre = document.body.querySelector("pre"); if (pre && document.body.children.length === 1) { return pre.textContent || ""; } return document.body.innerText || document.body.textContent || ""; } TabLocalFiles.readRawTextFromDocument = readRawTextFromDocument; function readRawHtmlFromDocument() { const html = document.documentElement?.outerHTML || ""; if (!html) { return ""; } const doctype = serializeDoctype(document.doctype); return doctype ? `${doctype}\n${html}` : html; } TabLocalFiles.readRawHtmlFromDocument = readRawHtmlFromDocument; function fileNameFromPath(pathname) { return pathname.split("/").filter(Boolean).pop() || TabLocalFiles.t("common.localFile"); } TabLocalFiles.fileNameFromPath = fileNameFromPath; function fileTitleFromName(fileName, definition) { const matchedExtension = definition.extensions.find((extension) => fileName.toLowerCase().endsWith(extension)); return matchedExtension ? fileName.slice(0, -matchedExtension.length) || fileName : fileName; } TabLocalFiles.fileTitleFromName = fileTitleFromName; function viewerChromeFromSearch(search) { try { const params = new URLSearchParams(search); const hideAll = isHiddenViewerChromeValue(params.get("tlf-chrome")); return { headerDisabled: hideAll || isHiddenViewerChromeValue(params.get("tlf-header")), sidebarDisabled: hideAll || isHiddenViewerChromeValue(params.get("tlf-sidebar")) }; } catch { return { headerDisabled: false, sidebarDisabled: false }; } } TabLocalFiles.viewerChromeFromSearch = viewerChromeFromSearch; function extensionViewerUrl(sourceUrl, sidebarSnapshot, chrome) { const viewerUrl = globalThis.chrome?.runtime?.getURL?.("viewer.html"); if (!viewerUrl) { return sourceUrl; } const url = new URL(viewerUrl); url.searchParams.set("file", sourceUrl); if (chrome?.headerDisabled) { url.searchParams.set("tlf-header", "hidden"); } if (chrome?.sidebarDisabled) { url.searchParams.set("tlf-sidebar", "hidden"); } else if (sidebarSnapshot) { url.searchParams.set("sidebar", JSON.stringify(sidebarSnapshot)); } return url.toString(); } TabLocalFiles.extensionViewerUrl = extensionViewerUrl; function isHiddenViewerChromeValue(value) { const normalizedValue = value?.trim().toLowerCase(); return normalizedValue === "0" || normalizedValue === "false" || normalizedValue === "hidden"; } function beginDocumentMount() { currentDocumentMountId += 1; runCurrentDocumentCleanup(); teardownEmbeddedContent(); resetPanelMode(); return currentDocumentMountId; } TabLocalFiles.beginDocumentMount = beginDocumentMount; function resetPanelMode() { const page = document.querySelector(".tlf-page"); if (page) { page.dataset.tlfMode = "preview"; } } function isCurrentDocumentMount(mountId) { return mountId !== undefined && mountId === currentDocumentMountId; } TabLocalFiles.isCurrentDocumentMount = isCurrentDocumentMount; function registerDocumentMountCleanup(mountId, cleanup) { if (!isCurrentDocumentMount(mountId)) { safeRunCleanup(cleanup); return; } const previousCleanup = currentDocumentCleanup; currentDocumentCleanup = () => { safeRunCleanup(previousCleanup); safeRunCleanup(cleanup); }; } TabLocalFiles.registerDocumentMountCleanup = registerDocumentMountCleanup; function runCurrentDocumentCleanup() { safeRunCleanup(currentDocumentCleanup); currentDocumentCleanup = undefined; } function safeRunCleanup(cleanup) { if (!cleanup) { return; } try { cleanup(); } catch (error) { console.warn("[Previewer] Document cleanup failed", error); } } function teardownEmbeddedContent() { document.querySelectorAll("iframe").forEach((frame) => { try { frame.srcdoc = ""; frame.src = "about:blank"; frame.removeAttribute("src"); } catch { } }); document.querySelectorAll("object, embed").forEach((embed) => { try { if ("data" in embed) { embed.data = "about:blank"; } embed.removeAttribute("data"); embed.removeAttribute("src"); embed.textContent = ""; } catch { } }); document.querySelectorAll("canvas").forEach((canvas) => { try { canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height); canvas.width = 0; canvas.height = 0; } catch { } }); } function renderDocumentLayout(context, rendered) { const definition = context.definition; const sidebarHtml = context.sidebarDisabled ? "" : rendered.sidebarHtml || context.navigationSidebarHtml || ""; const pageClassName = [ "tlf-page", sidebarHtml ? "" : "tlf-page-no-sidebar", context.headerDisabled ? "tlf-page-no-header" : "" ] .filter(Boolean) .join(" "); const workspaceClassName = context.navigationRootPath && sidebarHtml ? " tlf-workspace-page" : ""; return `
${sidebarHtml} ${renderDocumentMain(context, rendered)}
`; } TabLocalFiles.renderDocumentLayout = renderDocumentLayout; function renderDocumentMain(context, rendered) { const contentClassName = rendered.contentClassName ? ` ${escapeAttr(rendered.contentClassName)}` : ""; return `
${renderFileHeader(context, rendered)}
${rendered.bodyHtml}
`; } TabLocalFiles.renderDocumentMain = renderDocumentMain; function renderFileHeader(context, rendered) { const toolbar = rendered.toolbar || {}; const isDirectory = context.definition.id === "directory" || Boolean(toolbar.directory); const isLocalSource = isEditableLocalFileSourceUrl(context.sourceUrl); const canRename = !isDirectory && isLocalSource; const hasSidebar = !context.sidebarDisabled && Boolean(rendered.sidebarHtml || context.navigationSidebarHtml); const sidebarToggleLabel = hasSidebar ? TabLocalFiles.t("common.collapseExplorer") : TabLocalFiles.t("common.expandExplorer"); const headerDetails = renderToolbarStatuses(toolbar.status || []); const infoMenuHtml = renderFileHeaderInfoMenu(headerDetails, isDirectory, context); const favoriteToggleHtml = renderFavoriteToggle(context); const titleHtml = canRename ? ` ` : `${escapeHtml(context.fileName)}`; const renameStatusHtml = canRename ? '' : ""; const badgesHtml = (toolbar.badges || []).map((badge) => badge.html).join(""); const modeSegmentHtml = isDirectory ? "" : renderModeSegment(toolbar, context); const overflowMenuHtml = renderOverflowMenu(toolbar, context, isDirectory); const exportMenuHtml = isDirectory ? "" : renderExportMenu(toolbar.exportItems || [], context); const contextToolbarHtml = renderContextToolbar(toolbar, context); const hiddenAttributes = context.headerDisabled ? ' hidden aria-hidden="true"' : ""; return `
${renderFilePathBreadcrumb(parentDirectoryPathForHeader(context.decodedPath), context.navigationRootPath, isLocalSource)}
${titleHtml}
${renameStatusHtml}
${badgesHtml} ${infoMenuHtml} ${favoriteToggleHtml}
${modeSegmentHtml} ${exportMenuHtml} ${overflowMenuHtml}
${contextToolbarHtml}
`; } function parentDirectoryPathForHeader(decodedPath) { const trimmedPath = decodedPath.endsWith("/") ? decodedPath.slice(0, -1) : decodedPath; const slashIndex = trimmedPath.lastIndexOf("/"); return slashIndex >= 0 ? trimmedPath.slice(0, slashIndex + 1) : "/"; } function renderModeSegment(toolbar, context) { const isLocalSource = isEditableLocalFileSourceUrl(context.sourceUrl); const editableType = Boolean(toolbar.editable); const editBlocked = !editableType || !isLocalSource; const editTitle = !editableType ? TabLocalFiles.t("common.editUnsupportedType") : !isLocalSource ? TabLocalFiles.t("common.editUnavailableRemote") : TabLocalFiles.t("common.editModeHint"); const lockIcon = editBlocked ? `` : ""; return `
`; } function renderOverflowMenu(toolbar, context, isDirectory) { const isLocalSource = isEditableLocalFileSourceUrl(context.sourceUrl); const canReveal = isLocalFileSourceUrl(context.sourceUrl); const isRemoteSource = isRemoteHttpSourceUrl(context.sourceUrl); const items = []; if (!isDirectory) { items.push(commonToolbarAction("open-new-tab")); items.push(commonToolbarAction("print")); } const revealControl = commonToolbarAction("reveal", canReveal ? {} : { disabled: true }); items.push(canReveal ? revealControl : { ...revealControl, label: TabLocalFiles.t("common.revealUnavailableRemote") }); items.push(commonToolbarAction("copy-path")); if (isRemoteSource) { items.push(commonToolbarAction("download-source")); } if (!isDirectory && toolbar.editable && isLocalSource) { items.push(commonToolbarAction("duplicate-file", { hidden: true })); } items.push(...(toolbar.menu || [])); if (isLocalSource && context.decodedPath !== "/") { items.push(commonToolbarAction("trash")); } return `
${renderLucideIcon("more-horizontal")}
`; } function renderMenuItem(control) { if (control.type === "custom") { return control.html; } const action = control.action || control.id; const disabled = control.disabled ? " disabled" : ""; const hidden = control.hidden ? " hidden" : ""; const requiresFocus = control.requiresEditorFocus ? " data-tlf-requires-editor-focus" : ""; const elementId = control.elementId ? ` id="${escapeAttr(control.elementId)}"` : ""; const visibleLabel = control.labelText || control.label; return ` ${renderLucideIcon(control.icon)} ${escapeHtml(visibleLabel)} `; } function renderContextToolbar(toolbar, context) { const previewControls = renderToolbarControls(toolbar.contextPreview || [], context); const editLeadingControls = renderToolbarControls(toolbar.contextEditLeading || [], context); const historyControls = toolbar.editable && !toolbar.directory ? renderToolbarControls(editHistoryControls(toolbar), context) : ""; const editControls = renderToolbarControls(toolbar.contextEdit || [], context); const editOverflow = renderContextEditOverflow(toolbar.contextEditOverflow || [], context); const trailingControls = renderToolbarControls(toolbar.contextTrailing || [], context); const commonControls = toolbar.commonTools ? [ toolbar.saveBadge ? renderSaveComboControl(context) : renderToolbarControls([commonToolbarAction("save", { showLabel: true, labelText: TabLocalFiles.t("common.save"), disabled: true })], context), renderToolbarControls([ commonToolbarAction("revert", { disabled: true }), commonToolbarAction("reload"), commonToolbarAction("copy-all") ], context) ].join("") : ""; if (!previewControls && !editLeadingControls && !editControls && !editOverflow && !commonControls && !trailingControls) { return ""; } const previewGroup = previewControls ? `
${previewControls}
` : ""; const editGroup = editLeadingControls || historyControls || editControls || editOverflow ? `
${editLeadingControls}${historyControls}${editControls}${editOverflow}
` : ""; const commonGroup = commonControls ? `${previewGroup || editGroup ? '' : ""}
${commonControls}
` : ""; const trailingGroup = trailingControls ? `${previewGroup || editGroup || commonGroup ? '' : ""}
${trailingControls}
` : ""; const moreTools = TabLocalFiles.t("common.moreTools"); return `
${previewGroup} ${editGroup} ${commonGroup} ${trailingGroup}
`; } function renderExportMenu(items, context) { if (!items.some((item) => item.type === "custom" || !item.hidden)) { return ""; } const label = TabLocalFiles.t("common.export"); return `
${renderLucideIcon("download")} ${escapeHtml(label)} ${renderLucideIcon("chevron-down")}
`; } function editHistoryControls(toolbar) { const controls = [ toolbarAction("undo", shortcutLabel("common.undo", { mod: true, key: "Z" }), "undo-2", { disabled: true }), toolbarAction("redo", shortcutLabel("common.redo", { mod: true, shift: true, key: "Z" }), "redo-2", { disabled: true }) ]; if (toolbar.contextEdit?.length || toolbar.contextEditOverflow?.length) { controls.push(contextDivider("edit-history-divider")); } return controls; } const SAVE_STATE_DESCRIPTORS = [ { key: "readonly", icon: "lock", labelKey: "editor.readonly" }, { key: "clean", icon: "circle-check", labelKey: "editor.clean" }, { key: "external", icon: "refresh-cw", labelKey: "editor.external" }, { key: "dirty", icon: "save", labelKey: "common.save" }, { key: "autosave-pending", icon: "save", labelKey: "editor.autoSavePending" }, { key: "saving", icon: "loader-circle", labelKey: "editor.saving" }, { key: "saved", icon: "circle-check", labelKey: "editor.saved" }, { key: "error", icon: "circle-alert", labelKey: "editor.error" }, { key: "conflict", icon: "circle-alert", labelKey: "editor.conflict" } ]; function saveStateKey(state, autoSave, externalWrite = false) { if (externalWrite && (state === "clean" || state === "saved")) { return "external"; } return state === "dirty" && autoSave ? "autosave-pending" : state; } TabLocalFiles.saveStateKey = saveStateKey; let externalWriteActive = false; function setExternalWriteActive(active) { if (externalWriteActive === active) { return; } externalWriteActive = active; syncToolbarEditState(); } TabLocalFiles.setExternalWriteActive = setExternalWriteActive; function renderSaveComboControl(context) { const isLocal = isLocalFileSourceUrl(context.sourceUrl); const label = isLocal ? TabLocalFiles.t("common.save") : TabLocalFiles.t("common.editUnavailableRemote"); const icons = renderStateSlot(SAVE_STATE_DESCRIPTORS.map((descriptor) => ({ key: descriptor.key, html: renderLucideIcon(descriptor.icon) })), "clean", { className: "tlf-save-combo-icon", ariaHidden: true }); const labels = renderStateSlot(SAVE_STATE_DESCRIPTORS.map((descriptor) => ({ key: descriptor.key, html: escapeHtml(TabLocalFiles.t(descriptor.labelKey)) })), "clean", { elementId: "tlf-editor-state", className: "tlf-save-combo-state", live: true }); return ` `; } function syncSaveStateSlots(key) { document.querySelectorAll(".tlf-save-combo").forEach((button) => { button.dataset.tlfSaveState = key; button.querySelectorAll("[data-tlf-state-slot]").forEach((slot) => setStateSlot(slot, key)); }); } function renderContextEditOverflow(controls, context) { if (!controls.length) { return ""; } const inlineControls = renderToolbarControls(controls, context); const menuItems = controls.map((control) => renderMenuItem(toolbarControlForContext(control, context))).join(""); return `
${inlineControls}
${renderLucideIcon("chevrons-right")}
`; } function renderFileHeaderStatAttributes(stat) { if (!stat) { return ""; } const attributes = []; if (Number.isFinite(stat.size)) { attributes.push(`data-tlf-file-size="${escapeAttr(String(stat.size))}"`); } if (stat.birthtime) { attributes.push(`data-tlf-file-birthtime="${escapeAttr(stat.birthtime)}"`); } if (stat.mtime) { attributes.push(`data-tlf-file-mtime="${escapeAttr(stat.mtime)}"`); } return attributes.length ? ` ${attributes.join(" ")}` : ""; } function updateFileHeaderStatMetadata(stat) { if (!stat) { return; } const toolbar = document.querySelector("[data-tlf-file-toolbar]"); if (!toolbar) { return; } if (Number.isFinite(stat.size)) { toolbar.dataset.tlfFileSize = String(stat.size); updateFileHeaderInfoStat("size", TabLocalFiles.formatBytes(stat.size)); } if (stat.birthtime) { toolbar.dataset.tlfFileBirthtime = stat.birthtime; updateFileHeaderInfoStat("created", formatFileTimestamp(stat.birthtime)); } if (stat.mtime) { toolbar.dataset.tlfFileMtime = stat.mtime; updateFileHeaderInfoStat("modified", formatFileTimestamp(stat.mtime)); } } TabLocalFiles.updateFileHeaderStatMetadata = updateFileHeaderStatMetadata; async function refreshFileHeaderStat(context) { if (context.fileStat || !isLocalFileSourceUrl(context.sourceUrl) || context.decodedPath.endsWith("/")) { return; } const stat = await TabLocalFiles.statOriginalFile(context.decodedPath); if (!stat || !isCurrentDocumentMount(context.mountId)) { return; } context.fileStat = stat; updateFileHeaderStatMetadata(stat); } TabLocalFiles.refreshFileHeaderStat = refreshFileHeaderStat; function renderFileHeaderInfoMenu(detailsHtml, isDirectory, context) { if (!detailsHtml.trim() && isDirectory) { return ""; } const label = TabLocalFiles.t("common.fileInfo"); const statsHtml = renderFileHeaderInfoStats(context, isDirectory); const contentHtml = detailsHtml.trim() ? `
${detailsHtml}
` : ""; return `
${renderLucideIcon("info")}
`; } function renderFileHeaderInfoStats(context, isDirectory) { const stat = context.fileStat; if (isDirectory || (!stat && !isLocalFileSourceUrl(context.sourceUrl))) { return ""; } const sizeText = stat && Number.isFinite(stat.size) ? TabLocalFiles.formatBytes(stat.size) : TabLocalFiles.t("common.unknown"); const rows = [ renderFileHeaderInfoStatRow("size", TabLocalFiles.t("common.fileSize"), sizeText), ...(TabLocalFiles.ZipArchive.isVirtualPath(context.decodedPath) ? [] : [renderFileHeaderInfoStatRow("created", TabLocalFiles.t("common.fileCreated"), formatFileTimestamp(stat?.birthtime))]), renderFileHeaderInfoStatRow("modified", TabLocalFiles.t("common.fileModified"), formatFileTimestamp(stat?.mtime)) ]; return `
${rows.join("")}
`; } function renderFileHeaderInfoStatRow(field, label, value) { return `
${escapeHtml(label)}
${escapeHtml(value)}
`; } function formatFileTimestamp(value, now = new Date()) { if (!value) { return TabLocalFiles.t("common.unknown"); } const date = new Date(value); if (Number.isNaN(date.valueOf())) { return TabLocalFiles.t("common.unknown"); } const locale = TabLocalFiles.currentLocale(); const dayDifference = calendarDayDifference(now, date); if (dayDifference === 0 || dayDifference === 1) { const time = date.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit" }); return `${TabLocalFiles.t(dayDifference === 0 ? "common.today" : "common.yesterday")} ${time}`; } return date.toLocaleString(locale, { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "2-digit" }); } TabLocalFiles.formatFileTimestamp = formatFileTimestamp; function calendarDayDifference(later, earlier) { const laterMidnight = new Date(later.getFullYear(), later.getMonth(), later.getDate()).getTime(); const earlierMidnight = new Date(earlier.getFullYear(), earlier.getMonth(), earlier.getDate()).getTime(); return Math.round((laterMidnight - earlierMidnight) / 86400000); } function upsertFileInfoStat(field, label, value) { const stats = document.querySelector("[data-tlf-file-info-stats]"); if (!stats) { return; } const existing = stats.querySelector(`[data-tlf-file-info-stat="${field}"]`); if (existing) { existing.textContent = value; return; } stats.insertAdjacentHTML("beforeend", renderFileHeaderInfoStatRow(field, label, value)); } TabLocalFiles.upsertFileInfoStat = upsertFileInfoStat; function updateFileHeaderInfoStat(field, value) { const target = document.querySelector(`[data-tlf-file-info-stats] [data-tlf-file-info-stat="${field}"]`); if (target) { target.textContent = value; } } function renderToolbarStatuses(statusItems) { return statusItems.map(renderToolbarStatus).join(""); } function renderToolbarStatus(status) { const className = `tlf-file-header-detail${status.className ? ` ${escapeAttr(status.className)}` : ""}`; const tone = status.tone ? ` data-tone="${escapeAttr(status.tone)}"` : ""; const content = status.html !== undefined ? status.html : escapeHtml(status.text || ""); return `
${content}
`; } function renderToolbarControls(controls, context) { return controls.map((control) => renderToolbarControl(toolbarControlForContext(control, context))).join(""); } function toolbarControlForContext(control, context) { if (control.type !== "action" || isEditableLocalFileSourceUrl(context.sourceUrl)) { return control; } const action = control.action || control.id; if (action === "save" || action === "revert" || action.endsWith(".save")) { return { ...control, label: TabLocalFiles.t("common.editUnavailableRemote"), disabled: true }; } return control; } function renderToolbarControl(control) { if (control.type === "custom") { return control.html; } const elementId = control.elementId ? ` id="${escapeAttr(control.elementId)}"` : ""; const action = control.action || control.id; const pressed = control.pressed === undefined ? "" : ` aria-pressed="${String(control.pressed)}"`; const disabled = control.disabled ? " disabled" : ""; const hidden = control.hidden ? " hidden" : ""; const requiresFocus = control.requiresEditorFocus ? " data-tlf-requires-editor-focus" : ""; const variantClass = control.variant === "primary" ? " tlf-toolbar-button-primary" : ""; const visibleLabel = control.showLabel ? control.labelText || control.label : ""; const labeledClass = visibleLabel ? " tlf-toolbar-button-labeled" : ""; const className = `tlf-toolbar-button${variantClass}${labeledClass}${control.className ? ` ${escapeAttr(control.className)}` : ""}`; const tooltip = visibleLabel ? (visibleLabel === control.label ? "" : control.label) : control.label; const tooltipAttribute = tooltip ? ` title="${escapeAttr(tooltip)}"` : ""; const labelHtml = visibleLabel ? `${escapeHtml(visibleLabel)}` : ""; return ` ${renderLucideIcon(control.icon)}${labelHtml} `; } function toolbarAction(id, label, icon, options = {}) { return { type: "action", id, label, icon, ...options }; } TabLocalFiles.toolbarAction = toolbarAction; function toolbarCustom(id, html) { return { type: "custom", id, html }; } TabLocalFiles.toolbarCustom = toolbarCustom; function contextDivider(id) { return toolbarCustom(id, ''); } TabLocalFiles.contextDivider = contextDivider; function contextLabel(id, text) { return toolbarCustom(id, `${escapeHtml(text)}`); } TabLocalFiles.contextLabel = contextLabel; function renderStateSlot(entries, activeKey, options = {}) { const attributes = [ options.elementId ? ` id="${escapeAttr(options.elementId)}"` : "", ` class="tlf-state-slot${options.className ? ` ${escapeAttr(options.className)}` : ""}"`, " data-tlf-state-slot", options.live ? ' role="status" aria-live="polite"' : "", options.ariaLabel ? ` aria-label="${escapeAttr(options.ariaLabel)}"` : "", options.ariaHidden ? ' aria-hidden="true"' : "" ].join(""); const items = entries .map((entry) => `${entry.html}`) .join(""); return `${items}`; } TabLocalFiles.renderStateSlot = renderStateSlot; function setStateSlot(slot, activeKey) { slot?.querySelectorAll(".tlf-state-slot-item").forEach((item) => { item.toggleAttribute("data-active", item.dataset.state === activeKey); }); } TabLocalFiles.setStateSlot = setStateSlot; function renderDigitSlot(text, digits, options = {}) { const reserve = Math.max(1, Math.round(digits)); const attributes = [ options.elementId ? ` id="${escapeAttr(options.elementId)}"` : "", ` class="tlf-digit-slot${options.className ? ` ${escapeAttr(options.className)}` : ""}"`, options.name ? ` data-tlf-digit="${escapeAttr(options.name)}"` : "", ` style="--tlf-digit-reserve:${reserve}"` ].join(""); return `${escapeHtml(text)}`; } TabLocalFiles.renderDigitSlot = renderDigitSlot; function tHtml(key, values = {}) { const template = TabLocalFiles.messageTemplate(key); const pattern = /\{(\w+)\}/g; let html = ""; let index = 0; let match; while ((match = pattern.exec(template)) !== null) { html += escapeHtml(template.slice(index, match.index)) + (values[match[1]] ?? ""); index = match.index + match[0].length; } return html + escapeHtml(template.slice(index)); } TabLocalFiles.tHtml = tHtml; function setDigitSlot(root, name, text) { const slot = root?.querySelector(`[data-tlf-digit="${name}"]`); if (slot) { slot.textContent = text; } } TabLocalFiles.setDigitSlot = setDigitSlot; function segmentedToolbar(id, ariaLabel, options) { const buttons = options .map((option) => { const icon = option.icon ? renderLucideIcon(option.icon) : ""; return ``; }) .join(""); return toolbarCustom(id, `
${buttons}
`); } TabLocalFiles.segmentedToolbar = segmentedToolbar; function syncSegmentedToolbar(id, activeKey) { document.querySelectorAll(`[data-tlf-segment="${id}"]`).forEach((segment) => { segment.querySelectorAll("[data-tlf-toolbar-action]").forEach((button) => { button.setAttribute("aria-pressed", String(button.dataset.tlfToolbarAction === `${id}.${activeKey}`)); }); positionSegmentPill(segment, true); }); } TabLocalFiles.syncSegmentedToolbar = syncSegmentedToolbar; function positionSegmentPill(segment, animate) { const pill = segment.querySelector(":scope > .tlf-segment-pill"); const active = segment.querySelector(':scope > [aria-pressed="true"]'); if (!pill || !active || segment.offsetParent === null) { return; } const apply = () => { pill.style.transform = `translateX(${active.offsetLeft}px)`; pill.style.width = `${active.offsetWidth}px`; }; if (animate) { apply(); return; } const previousTransition = pill.style.transition; pill.style.transition = "none"; apply(); void pill.offsetWidth; pill.style.transition = previousTransition; } TabLocalFiles.positionSegmentPill = positionSegmentPill; function actualSizeToolbar(id, label, options = {}) { const className = `tlf-toolbar-button${options.className ? ` ${escapeAttr(options.className)}` : ""}`; const pressed = options.pressed === undefined ? "" : ` aria-pressed="${String(options.pressed)}"`; return toolbarCustom(id, ` `); } TabLocalFiles.actualSizeToolbar = actualSizeToolbar; function toolbarStatus(id, text, options = {}) { return { id, text, ...options }; } TabLocalFiles.toolbarStatus = toolbarStatus; function zoomStepScale(current, factor) { const next = current * factor; if ((current < 1 && next > 1) || (current > 1 && next < 1)) { return 1; } return next; } TabLocalFiles.zoomStepScale = zoomStepScale; function zoomPercentControl(id, elementId) { const label = TabLocalFiles.t("common.zoomPercent"); return toolbarCustom(id, ``); } TabLocalFiles.zoomPercentControl = zoomPercentControl; function bindZoomPercentInput(element, applyPercent) { const input = asZoomInputElement(element); if (!input || input.dataset.tlfZoomInputBound === "true") { return; } input.dataset.tlfZoomInputBound = "true"; const commit = () => { const lastValue = input.dataset.tlfZoomLastValue || "100%"; const parsed = Number.parseFloat(input.value.replace(/%|%/g, "").trim()); if (!Number.isFinite(parsed) || parsed <= 0) { input.value = lastValue; return; } if (`${Math.round(parsed)}%` === lastValue) { input.value = lastValue; return; } applyPercent(parsed); }; input.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); commit(); input.select(); } else if (event.key === "Escape") { event.preventDefault(); input.value = input.dataset.tlfZoomLastValue || "100%"; input.blur(); } }); input.addEventListener("blur", commit); input.addEventListener("focus", () => { input.select(); }); } TabLocalFiles.bindZoomPercentInput = bindZoomPercentInput; function syncZoomPercentDisplay(element, percent) { if (!element) { return; } const text = `${Math.round(percent)}%`; const input = asZoomInputElement(element); if (input) { input.dataset.tlfZoomLastValue = text; input.value = text; return; } element.textContent = text; } TabLocalFiles.syncZoomPercentDisplay = syncZoomPercentDisplay; function asZoomInputElement(element) { return element && element.tagName === "INPUT" ? element : undefined; } function directoryToolbar() { return { directory: true }; } TabLocalFiles.directoryToolbar = directoryToolbar; function editableTextToolbar(status = []) { return { status, editable: true, saveBadge: true, commonTools: true }; } TabLocalFiles.editableTextToolbar = editableTextToolbar; function rawPreviewToolbar(status = []) { return { status, menu: [toolbarAction("raw", TabLocalFiles.t("common.rawFile"), "external-link")] }; } TabLocalFiles.rawPreviewToolbar = rawPreviewToolbar; function filePreviewToolbar(status = []) { return { status }; } TabLocalFiles.filePreviewToolbar = filePreviewToolbar; function commonToolbarAction(id, options = {}) { const definitions = { save: { label: TabLocalFiles.t("common.save"), icon: "save" }, revert: { label: TabLocalFiles.t("common.revertToOpened"), icon: "rotate-cw-fading-clock" }, "copy-all": { label: TabLocalFiles.t("common.copyAllText"), icon: "copy" }, reload: { label: TabLocalFiles.t("common.reloadFromDisk"), icon: "refresh-cw" }, "open-new-tab": { label: TabLocalFiles.t("common.openInNewTab"), icon: "external-link" }, "download-source": { label: TabLocalFiles.t("common.downloadRemoteFile"), icon: "download" }, "copy-path": { label: TabLocalFiles.t("common.copyAbsolutePath"), icon: "copy" }, "duplicate-file": { label: TabLocalFiles.t("common.duplicateFile"), icon: "file-plus" }, trash: { label: TabLocalFiles.t("trash.action", { place: trashPlaceLabel() }), icon: "trash-2" }, print: { label: TabLocalFiles.t("common.print"), icon: "printer" }, reveal: { label: revealInFileManagerLabel(), icon: "folder-open" } }; const definition = definitions[id]; return toolbarAction(id, definition.label, definition.icon, options); } function trashPlaceLabel() { return TabLocalFiles.t(TabLocalFiles.shortcutPlatform() === "mac" ? "trash.placeTrash" : "trash.placeRecycleBin"); } TabLocalFiles.trashPlaceLabel = trashPlaceLabel; function revealInFileManagerLabel() { const platform = TabLocalFiles.shortcutPlatform(); if (platform === "mac") { return TabLocalFiles.t("common.revealInFinder"); } if (platform === "windows") { return TabLocalFiles.t("common.revealInExplorer"); } return TabLocalFiles.t("common.revealInFileManager"); } TabLocalFiles.revealInFileManagerLabel = revealInFileManagerLabel; function shortcutLabel(labelKey, spec) { return TabLocalFiles.t("common.shortcutHint", { label: TabLocalFiles.t(labelKey), shortcut: TabLocalFiles.formatShortcut(spec) }); } TabLocalFiles.shortcutLabel = shortcutLabel; function renderLucideIcon(name) { const paths = { "arrow-down-to-line": '', "arrow-down-a-z": '', bold: '', braces: '', code: '', eraser: '', filter: '', hash: '', "heading-1": '', "heading-2": '', "heading-3": '', "heading-4": '', "heading-5": '', "list-checks": '', "list-ordered": '', "list-tree": '', minus: '', paperclip: '', quote: '', "square-code": '', strikethrough: '', languages: '', text: '', "undo-2": '', "redo-2": '', table: '', "wrap-text": '', "chevron-down": '', "chevrons-down-up": '', "chevrons-up-down": '', "chevron-left": '', "chevron-right": '', "chevron-up": '', copy: '', download: '', check: '', "circle-check": '', "circle-help": '', "circle-alert": '', "chevrons-right": '', "external-link": '', lock: '', "loader-circle": '', "file-plus": '', highlighter: '', underline: '', hand: '', "fit-page": '', "fit-width": '', "flip-horizontal": '', "flip-vertical": '', "folder-open": '', "folder-plus": '', info: '', lightbulb: '', "message-square-warning": '', "triangle-alert": '', "table-properties": '', "octagon-alert": '', image: '', italic: '', "layout-grid": '', link: '', list: '', maximize: '', "more-horizontal": '', "panel-left": '', pencil: '', printer: '', "refresh-cw": '', "rotate-ccw": '', "rotate-cw-fading-clock": '', "rotate-cw": '', save: '', search: '', star: '', "trash-2": '', settings: '', "volume-2": '', x: '', zap: '', plus: '', "align-left": '', "zoom-in": '', "zoom-out": '' }; return ` `; } TabLocalFiles.renderLucideIcon = renderLucideIcon; function showToast(message, tone = "neutral") { if (!message || !document.body) { return; } const toast = ensureToastRegion(); if (toastHideTimer) { window.clearTimeout(toastHideTimer); } if (toastRemoveTimer) { window.clearTimeout(toastRemoveTimer); } positionToastRegion(toast); toast.hidden = false; toast.textContent = message; toast.dataset.tlfToastTone = tone; toast.dataset.tlfToastState = "visible"; toastHideTimer = window.setTimeout(() => { toast.dataset.tlfToastState = "leaving"; toastRemoveTimer = window.setTimeout(() => { toast.hidden = true; toast.textContent = ""; }, TOAST_EXIT_MS); }, TOAST_VISIBLE_MS); } TabLocalFiles.showToast = showToast; function positionToastRegion(toast) { const header = document.querySelector("[data-tlf-file-toolbar]:not([hidden])"); const headerBottom = header?.getBoundingClientRect?.().bottom || 0; const top = Math.max(TOAST_VIEWPORT_MARGIN, Math.round(headerBottom) + TOAST_HEADER_GAP); toast.style.top = `${top}px`; } function ensureToastRegion() { const existing = document.querySelector("[data-tlf-toast]"); if (existing) { return existing; } const toast = document.createElement("div"); toast.className = "tlf-toast-region"; toast.dataset.tlfToast = "true"; toast.dataset.tlfToastState = "leaving"; toast.setAttribute("role", "status"); toast.setAttribute("aria-live", "polite"); toast.setAttribute("aria-atomic", "true"); toast.hidden = true; document.body.append(toast); return toast; } function renderBreadcrumb(decodedPath, navigationRootPath) { if (!navigationRootPath || !isInsideNavigationRoot(decodedPath, navigationRootPath)) { return ""; } const rootSegments = navigationRootPath.split("/").filter(Boolean); const segments = decodedPath.split("/").filter(Boolean); const isDirectory = decodedPath.endsWith("/"); if (!rootSegments.length || segments.length < rootSegments.length) { return ""; } const rootLabel = decodePathSegment(rootSegments[rootSegments.length - 1]); const rootItem = decodedPath === navigationRootPath ? `${escapeHtml(rootLabel)}` : `${escapeHtml(rootLabel)}`; const childItems = segments.slice(rootSegments.length).map((segment, relativeIndex, relativeSegments) => { const absoluteIndex = rootSegments.length + relativeIndex; const isCurrent = relativeIndex === relativeSegments.length - 1; const segmentPath = `/${segments.slice(0, absoluteIndex + 1).join("/")}${isCurrent && !isDirectory ? "" : "/"}`; const label = decodePathSegment(segment); if (isCurrent) { return `${escapeHtml(label)}`; } return `${escapeHtml(label)}`; }); const items = [rootItem, ...childItems]; return ` `; } TabLocalFiles.renderBreadcrumb = renderBreadcrumb; function renderFilePathBreadcrumb(decodedPath, _navigationRootPath, allowNavigation = true) { const segments = decodedPath.split("/").filter(Boolean); const isDirectory = decodedPath.endsWith("/"); if (!segments.length) { return ` `; } const items = segments.map((segment, index) => { return renderFilePathSegment(segments, index, isDirectory, allowNavigation); }); const trailingSeparator = isDirectory ? '/' : ""; const collapsibleItems = allowNavigation ? renderCollapsibleFilePathItems(segments, isDirectory, true) : renderStaticCollapsedFilePathItems(segments, isDirectory); const collapsedHtml = collapsibleItems ? ` /${collapsibleItems}${trailingSeparator} ` : ""; return ` `; } function renderCollapsibleFilePathItems(segments, isDirectory, allowNavigation) { if (segments.length < 4) { return ""; } const hiddenItems = segments.slice(1, -2).map((_segment, relativeIndex) => { return renderFilePathSegment(segments, relativeIndex + 1, isDirectory, allowNavigation, ' role="menuitem"'); }); if (!hiddenItems.length) { return ""; } const visibleItems = [ renderFilePathSegment(segments, 0, isDirectory, allowNavigation), renderFilePathOverflowMenu(hiddenItems.join('/')), renderFilePathSegment(segments, segments.length - 2, isDirectory, allowNavigation), renderFilePathSegment(segments, segments.length - 1, isDirectory, allowNavigation) ]; return visibleItems.join('/'); } function renderStaticCollapsedFilePathItems(segments, isDirectory) { if (segments.length < 4) { return ""; } return [ renderFilePathSegment(segments, 0, isDirectory, false), ``, renderFilePathSegment(segments, segments.length - 2, isDirectory, false), renderFilePathSegment(segments, segments.length - 1, isDirectory, false) ].join('/'); } function renderFilePathOverflowMenu(hiddenItemsHtml) { const buttonLabel = TabLocalFiles.t("common.showCollapsedPath"); return `
${escapeHtml(TabLocalFiles.t("common.pathEllipsis"))}
`; } function renderFilePathSegment(segments, index, isDirectory, allowNavigation, attributes = "") { const isCurrent = index === segments.length - 1; const segmentPath = `/${segments.slice(0, index + 1).join("/")}${isCurrent && !isDirectory ? "" : "/"}`; const label = decodePathSegment(segments[index]); if (!allowNavigation) { const className = isCurrent ? "tlf-file-path-current" : "tlf-file-path-static"; return `${escapeHtml(label)}`; } const currentClass = isCurrent ? " tlf-file-path-current" : ""; return `${escapeHtml(label)}`; } function isInsideNavigationRoot(path, root) { return path === root || path.startsWith(root); } function pathToFileUrl(pathname) { return `file://${pathname.split("/").map(encodeURIComponent).join("/")}`; } function decodePathSegment(segment) { try { return decodeURIComponent(segment); } catch { return segment; } } function serializeDoctype(doctype) { if (!doctype) { return ""; } const publicId = doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : ""; const systemId = doctype.systemId ? `${doctype.publicId ? "" : " SYSTEM"} "${doctype.systemId}"` : ""; return ``; } function bindLayoutControls() { const toggleButtons = Array.from(document.querySelectorAll("[data-tlf-sidebar-expand]")); const themeButton = document.querySelector("[data-tlf-theme-toggle]"); const page = document.querySelector(".tlf-page"); bindThemeControls(themeButton); bindInstantTooltips(); bindSplitPaneControls(); bindToolbarControls(); bindSegmentPillControls(); TabLocalFiles.ToolbarOverflow.bind(currentDocumentMountId); bindGlobalShortcuts(); bindRenameControls(); bindFavoriteControls(); bindCreateDocumentControls(); bindDirectorySettingsControls(); bindDirectoryMenuDismissControls(); bindFilePathBreadcrumbControls(); bindDocnavPanels(); TabLocalFiles.Directory.bindTreeControls(); TabLocalFiles.DirectoryManage.bind(); TabLocalFiles.DirectoryWatch.start(); if (!page) { return; } bindSidebarResizeControls(page); const setSidebarCollapsed = (collapsed) => { page.classList.toggle("tlf-page-sidebar-collapsed", collapsed); const label = collapsed ? TabLocalFiles.t("common.expandExplorer") : TabLocalFiles.t("common.collapseExplorer"); toggleButtons.forEach((button) => { button.setAttribute("aria-expanded", String(!collapsed)); button.setAttribute("aria-label", label); button.title = label; }); }; toggleButtons.forEach((button) => { button.addEventListener("click", () => { if (button.getAttribute("aria-expanded") === "true") { setSidebarCollapsed(true); return; } void (async () => { if (await ensureDirectorySidebar(page)) { setSidebarCollapsed(false); } })(); }); }); } TabLocalFiles.bindLayoutControls = bindLayoutControls; async function ensureDirectorySidebar(page) { if (page.querySelector(".tlf-directory-sidebar")) { return true; } const toolbar = document.querySelector("[data-tlf-file-toolbar]"); const path = toolbar?.dataset.tlfPath; const sourceUrl = toolbar?.dataset.tlfSourceUrl || ""; if (!path || !isEditableLocalFileSourceUrl(sourceUrl)) { return false; } const directoryPath = directoryPathForFile(path) || "/"; const entries = await TabLocalFiles.listDirectoryEntries(directoryPath); if (!entries) { showToast(TabLocalFiles.t("directory.readFailed"), "error"); return false; } if (page.querySelector(".tlf-directory-sidebar")) { return true; } const snapshot = { rootName: fileNameFromPath(directoryPath.replace(/\/+$/, "") || "/"), rootPath: directoryPath, entries }; page.insertAdjacentHTML("afterbegin", TabLocalFiles.Directory.renderSnapshotSidebar(snapshot, path)); page.classList.remove("tlf-page-no-sidebar"); page.classList.add("tlf-workspace-page"); TabLocalFiles.Directory.bindTreeControls(); bindSidebarResizeControls(page); bindDirectorySettingsControls(); bindCreateDocumentControls(); bindDirectoryMenuDismissControls(); return true; } function renderDocnav(spec) { const idAttribute = spec.elementId ? ` id="${escapeAttr(spec.elementId)}"` : ""; const className = `tlf-docnav${spec.className ? ` ${escapeAttr(spec.className)}` : ""}`; const extraAttributes = spec.extraAttributes ? ` ${spec.extraAttributes}` : ""; const hiddenAttribute = spec.hidden ? " hidden" : ""; const reserveAttribute = spec.width.reserve ? ` data-tlf-docnav-reserve="${spec.width.reserve}"` : ""; const storageAttribute = spec.width.storageKey ? ` data-tlf-docnav-storage="${escapeAttr(spec.width.storageKey)}"` : ""; const resizeAriaLabel = spec.resizeAriaLabel || TabLocalFiles.t("directory.resizeAria"); const resizeTitle = spec.resizeTitle || TabLocalFiles.t("directory.resizeTitle"); return ` ${spec.bodyHtml} `; } TabLocalFiles.renderDocnav = renderDocnav; function bindDocnavPanels(scope = document) { scope.querySelectorAll("[data-tlf-docnav]").forEach((panel) => bindDocnavResize(panel)); } TabLocalFiles.bindDocnavPanels = bindDocnavPanels; function bindDocnavResize(panel) { const handle = panel.querySelector("[data-tlf-docnav-resize-handle]"); if (!handle) { return; } const min = Number(panel.dataset.tlfDocnavMin) || 160; const max = Math.max(min, Number(panel.dataset.tlfDocnavMax) || min); const defaultWidth = Number(panel.dataset.tlfDocnavDefault) || min; const reserve = Number(panel.dataset.tlfDocnavReserve) || 0; const storageKey = panel.dataset.tlfDocnavStorage || ""; const clampWidth = (width) => { const viewportWidth = Number(window.innerWidth) || max + reserve; const maxWidth = Math.max(min, Math.min(max, reserve ? viewportWidth - reserve : max)); const fallback = Number.isFinite(width) ? width : defaultWidth; return Math.round(Math.min(maxWidth, Math.max(min, fallback))); }; const applyWidth = (width, persist = true) => { const nextWidth = clampWidth(width); panel.style.setProperty("--tlf-docnav-width", `${nextWidth}px`); handle.setAttribute("aria-valuenow", String(nextWidth)); if (persist && storageKey) { writeLocalStorage(storageKey, String(nextWidth)); } }; if (storageKey) { const storedWidth = Number(readLocalStorage(storageKey)); if (Number.isFinite(storedWidth) && storedWidth > 0) { applyWidth(storedWidth, false); } } bindEdgePanelResize({ handle, boundFlag: "tlfDocnavResizeBound", measure: () => panel.getBoundingClientRect().width || defaultWidth, apply: (width) => applyWidth(width), defaultWidth, resizing: { element: panel, flag: "tlfDocnavResizing" } }); } function bindEdgePanelResize(spec) { const { handle } = spec; if (handle.dataset[spec.boundFlag] === "true") { return; } handle.dataset[spec.boundFlag] = "true"; const keyboardStep = spec.keyboardStep ?? 16; handle.addEventListener("pointerdown", (event) => { if (event.button !== 0) { return; } event.preventDefault(); const startX = event.clientX; const startWidth = spec.measure(); const previousCursor = document.body.style.cursor; const previousUserSelect = document.body.style.userSelect; if (spec.resizing) { spec.resizing.element.dataset[spec.resizing.flag] = "true"; } document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; handle.setPointerCapture?.(event.pointerId); const onPointerMove = (moveEvent) => { spec.apply(startWidth + moveEvent.clientX - startX); spec.onMove?.(); }; const stopResize = () => { window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", stopResize); window.removeEventListener("pointercancel", stopResize); if (handle.hasPointerCapture?.(event.pointerId)) { handle.releasePointerCapture(event.pointerId); } if (spec.resizing) { delete spec.resizing.element.dataset[spec.resizing.flag]; } document.body.style.cursor = previousCursor; document.body.style.userSelect = previousUserSelect; }; window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointerup", stopResize); window.addEventListener("pointercancel", stopResize); }); handle.addEventListener("keydown", (event) => { if (event.key !== "ArrowLeft" && event.key !== "ArrowRight" && event.key !== "Home") { return; } event.preventDefault(); const nextWidth = event.key === "Home" ? spec.defaultWidth : spec.measure() + (event.key === "ArrowLeft" ? -keyboardStep : keyboardStep); spec.apply(nextWidth); spec.onMove?.(); }); handle.addEventListener("dblclick", () => { spec.apply(spec.defaultWidth); spec.onMove?.(); }); } TabLocalFiles.bindEdgePanelResize = bindEdgePanelResize; function bindSidebarResizeControls(page) { const sidebar = page.querySelector(".tlf-directory-sidebar"); const handle = sidebar?.querySelector("[data-tlf-sidebar-resize-handle]"); if (!sidebar || !handle) { return; } applyStoredDirectorySidebarWidth(sidebar, handle); bindEdgePanelResize({ handle, boundFlag: "tlfSidebarResizeBound", measure: () => sidebar.getBoundingClientRect().width, apply: (width) => setDirectorySidebarWidth(sidebar, handle, width), defaultWidth: 236, onMove: () => syncOpenDirectoryPopupMenus(), resizing: { element: page, flag: "tlfSidebarResizing" } }); } function applyStoredDirectorySidebarWidth(sidebar, handle) { if (!TabLocalFiles.Directory.readSettings().rememberSidebarWidth) { removeLocalStorage(DIRECTORY_SIDEBAR_WIDTH_STORAGE_KEY); return; } const storedWidth = Number(readLocalStorage(DIRECTORY_SIDEBAR_WIDTH_STORAGE_KEY)); if (!Number.isFinite(storedWidth) || storedWidth <= 0) { return; } setDirectorySidebarWidth(sidebar, handle, storedWidth, false); } function setDirectorySidebarWidth(sidebar, handle, width, persist = true) { const nextWidth = clampDirectorySidebarWidth(width); const widthPx = `${nextWidth}px`; sidebar.style.width = widthPx; sidebar.style.minWidth = widthPx; sidebar.style.maxWidth = widthPx; handle.setAttribute("aria-valuenow", String(nextWidth)); if (persist && TabLocalFiles.Directory.readSettings().rememberSidebarWidth) { writeLocalStorage(DIRECTORY_SIDEBAR_WIDTH_STORAGE_KEY, String(nextWidth)); } } function clampDirectorySidebarWidth(width) { const viewportWidth = Number(window.innerWidth) || DIRECTORY_SIDEBAR_MAX_WIDTH + 320; const maxWidth = Math.max(DIRECTORY_SIDEBAR_MIN_WIDTH, Math.min(DIRECTORY_SIDEBAR_MAX_WIDTH, viewportWidth - 320)); return Math.round(Math.min(maxWidth, Math.max(DIRECTORY_SIDEBAR_MIN_WIDTH, width))); } let globalShortcutsBound = false; function bindGlobalShortcuts() { if (globalShortcutsBound) { return; } globalShortcutsBound = true; document.addEventListener("keydown", (event) => { const withModifier = event.metaKey || event.ctrlKey; if (withModifier && event.key.toLowerCase() === "e") { const toolbar = document.querySelector("[data-tlf-file-toolbar]"); if (!toolbar) { return; } const editing = isDocumentEditing(); const target = toolbar.querySelector(`[data-tlf-toolbar-action="${editing ? "mode-preview" : "mode-edit"}"]:not([disabled])`); if (target) { event.preventDefault(); target.click(); } return; } if (event.key === "F2" && !TabLocalFiles.TextEditor.isTextInputTarget(event.target)) { const trigger = document.querySelector("[data-tlf-rename-trigger]"); if (trigger && !trigger.hidden) { event.preventDefault(); trigger.click(); } } }); } const SPLIT_RATIO_STORAGE_PREFIX = "tab-local-files:split-ratio:"; const SPLIT_DEFAULT_RATIO = 0.42; const SPLIT_MIN_RATIO = 0.2; const SPLIT_MAX_RATIO = 0.8; const SPLIT_MIN_PANE_WIDTH = 220; const SPLIT_KEYBOARD_STEP = 0.02; function renderSplitHandle(paneId) { const label = TabLocalFiles.t("common.resizePanes"); return ``; } TabLocalFiles.renderSplitHandle = renderSplitHandle; function bindSplitPaneControls(scope = document) { scope.querySelectorAll("[data-tlf-split-handle]").forEach((handle) => { const container = handle.parentElement; const leadingPane = handle.previousElementSibling; if (!container || !leadingPane) { return; } const paneId = handle.dataset.tlfSplitHandle || ""; const storedRatio = readStoredSplitRatio(paneId); if (storedRatio !== undefined) { applySplitRatio(container, handle, storedRatio); } if (handle.dataset.tlfSplitBound === "true") { return; } handle.dataset.tlfSplitBound = "true"; handle.addEventListener("pointerdown", (event) => { if (event.button !== 0) { return; } event.preventDefault(); const startX = event.clientX; const startWidth = leadingPane.getBoundingClientRect().width; const previousCursor = document.body.style.cursor; const previousUserSelect = document.body.style.userSelect; container.dataset.tlfSplitResizing = "true"; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; handle.setPointerCapture?.(event.pointerId); const onPointerMove = (moveEvent) => { applySplitRatio(container, handle, (startWidth + moveEvent.clientX - startX) / splitContentWidth(container), paneId); }; const stopResize = () => { window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", stopResize); window.removeEventListener("pointercancel", stopResize); if (handle.hasPointerCapture?.(event.pointerId)) { handle.releasePointerCapture(event.pointerId); } delete container.dataset.tlfSplitResizing; document.body.style.cursor = previousCursor; document.body.style.userSelect = previousUserSelect; }; window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointerup", stopResize); window.addEventListener("pointercancel", stopResize); }); handle.addEventListener("keydown", (event) => { if (event.key !== "ArrowLeft" && event.key !== "ArrowRight" && event.key !== "Home") { return; } event.preventDefault(); const current = leadingPane.getBoundingClientRect().width / splitContentWidth(container); const nextRatio = event.key === "Home" ? SPLIT_DEFAULT_RATIO : current + (event.key === "ArrowLeft" ? -SPLIT_KEYBOARD_STEP : SPLIT_KEYBOARD_STEP); applySplitRatio(container, handle, nextRatio, paneId); }); handle.addEventListener("dblclick", () => { applySplitRatio(container, handle, SPLIT_DEFAULT_RATIO, paneId); }); }); } TabLocalFiles.bindSplitPaneControls = bindSplitPaneControls; function applySplitRatio(container, handle, ratio, persistPaneId) { const clamped = clampSplitRatio(container, ratio); container.style.setProperty("--tlf-split-ratio", `${(clamped * 100).toFixed(2)}%`); handle.setAttribute("aria-valuenow", String(Math.round(clamped * 100))); if (persistPaneId) { writeLocalStorage(`${SPLIT_RATIO_STORAGE_PREFIX}${persistPaneId}`, clamped.toFixed(4)); } } function splitContentWidth(container) { const style = window.getComputedStyle(container); const padding = (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0); return Math.max(1, container.clientWidth - padding); } function clampSplitRatio(container, ratio) { const fallback = Number.isFinite(ratio) ? Math.min(SPLIT_MAX_RATIO, Math.max(SPLIT_MIN_RATIO, ratio)) : SPLIT_DEFAULT_RATIO; const contentWidth = splitContentWidth(container); const minRatio = Math.max(SPLIT_MIN_RATIO, SPLIT_MIN_PANE_WIDTH / contentWidth); const maxRatio = Math.min(SPLIT_MAX_RATIO, 1 - SPLIT_MIN_PANE_WIDTH / contentWidth); return maxRatio > minRatio ? Math.min(maxRatio, Math.max(minRatio, fallback)) : fallback; } function readStoredSplitRatio(paneId) { if (!paneId) { return undefined; } const stored = Number(readLocalStorage(`${SPLIT_RATIO_STORAGE_PREFIX}${paneId}`)); return Number.isFinite(stored) && stored > 0 && stored < 1 ? stored : undefined; } const TOOLTIP_ELEMENT_ID = "tlf-instant-tooltip"; const TOOLTIP_TRIGGER_SELECTOR = "[data-tlf-tooltip], .tlf-toolbar-button[title], .tlf-mode-segment-button[title], .tlf-sidebar-expand-button[title]"; const TOOLTIP_VIEWPORT_MARGIN = 8; const TOOLTIP_TRIGGER_GAP = 6; let instantTooltipBound = false; let instantTooltipTrigger; let instantTooltipTitle; function bindInstantTooltips() { if (instantTooltipBound) { return; } instantTooltipBound = true; document.addEventListener("pointerover", (event) => { const trigger = instantTooltipTriggerFor(event.target); if (trigger) { showInstantTooltip(trigger); } else { hideInstantTooltip(); } }); document.addEventListener("pointerout", (event) => { if (instantTooltipTriggerFor(event.relatedTarget) !== instantTooltipTrigger) { hideInstantTooltip(); } }); document.addEventListener("pointerdown", hideInstantTooltip, true); document.addEventListener("focusin", (event) => { const trigger = instantTooltipTriggerFor(event.target); if (trigger) { showInstantTooltip(trigger); } }); document.addEventListener("focusout", hideInstantTooltip); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { hideInstantTooltip(); } }); document.addEventListener("scroll", hideInstantTooltip, { capture: true, passive: true }); window.addEventListener("blur", hideInstantTooltip); } TabLocalFiles.bindInstantTooltips = bindInstantTooltips; function instantTooltipTriggerFor(target) { if (!(target instanceof Element)) { return undefined; } const trigger = target.closest(TOOLTIP_TRIGGER_SELECTOR); return trigger && !trigger.hidden ? trigger : undefined; } function showInstantTooltip(trigger) { if (instantTooltipTrigger === trigger) { return; } hideInstantTooltip(); const text = (trigger.dataset.tlfTooltip || trigger.getAttribute("title") || "").trim(); const tooltip = text ? instantTooltipElement() : undefined; if (!tooltip) { return; } instantTooltipTrigger = trigger; const nativeTitle = trigger.getAttribute("title"); if (nativeTitle !== null) { instantTooltipTitle = nativeTitle; trigger.dataset.tlfTooltip = nativeTitle; trigger.removeAttribute("title"); } tooltip.textContent = text; tooltip.hidden = false; positionInstantTooltip(trigger, tooltip); } function hideInstantTooltip() { const trigger = instantTooltipTrigger; const parkedTitle = instantTooltipTitle; instantTooltipTrigger = undefined; instantTooltipTitle = undefined; if (trigger && parkedTitle !== undefined) { delete trigger.dataset.tlfTooltip; if (!trigger.hasAttribute("title")) { trigger.setAttribute("title", parkedTitle); } } const tooltip = document.getElementById(TOOLTIP_ELEMENT_ID); if (tooltip) { tooltip.hidden = true; } } function instantTooltipElement() { const existing = document.getElementById(TOOLTIP_ELEMENT_ID); if (existing) { return existing; } if (!document.body) { return undefined; } const tooltip = document.createElement("div"); tooltip.id = TOOLTIP_ELEMENT_ID; tooltip.className = "tlf-tooltip"; tooltip.setAttribute("aria-hidden", "true"); tooltip.hidden = true; document.body.appendChild(tooltip); return tooltip; } function positionInstantTooltip(trigger, tooltip) { const anchor = trigger.getBoundingClientRect(); const box = tooltip.getBoundingClientRect(); const below = anchor.bottom + TOOLTIP_TRIGGER_GAP; const fitsBelow = below + box.height <= window.innerHeight - TOOLTIP_VIEWPORT_MARGIN; const top = fitsBelow ? below : Math.max(TOOLTIP_VIEWPORT_MARGIN, anchor.top - TOOLTIP_TRIGGER_GAP - box.height); const maxLeft = Math.max(TOOLTIP_VIEWPORT_MARGIN, window.innerWidth - box.width - TOOLTIP_VIEWPORT_MARGIN); const left = Math.min(Math.max(TOOLTIP_VIEWPORT_MARGIN, anchor.left + (anchor.width - box.width) / 2), maxLeft); tooltip.style.top = `${Math.round(top)}px`; tooltip.style.left = `${Math.round(left)}px`; } function bindToolbarControls() { document.querySelectorAll("[data-tlf-file-toolbar]").forEach((toolbar) => { toolbar.addEventListener("click", (event) => { const button = event.target?.closest("[data-tlf-toolbar-action]"); if (!button || !toolbar.contains(button)) { return; } button.closest("[data-tlf-file-header-overflow], [data-tlf-export-menu]")?.removeAttribute("open"); const action = button.dataset.tlfToolbarAction || ""; handleToolbarAction(action, button, toolbar); }); toolbar.querySelectorAll("[data-tlf-context-toolbar]").forEach((contextToolbar) => { contextToolbar.addEventListener("mousedown", (event) => { const target = event.target; if (!target || target.closest("input, select, textarea, [contenteditable]")) { return; } event.preventDefault(); }); }); }); if (!toolbarEditStateBound) { document.addEventListener("tlf:editor-mode-change", syncToolbarEditState); document.addEventListener("tlf:editor-state-change", syncToolbarEditState); document.addEventListener("input", scheduleHistoryCommandStateSync); document.addEventListener("focusin", scheduleEditActionAvailabilitySync); document.addEventListener("focusout", scheduleEditActionAvailabilitySync); toolbarEditStateBound = true; } syncToolbarEditState(); } let historyCommandSyncScheduled = false; function scheduleHistoryCommandStateSync() { if (historyCommandSyncScheduled) { return; } historyCommandSyncScheduled = true; const run = () => { historyCommandSyncScheduled = false; syncHistoryCommandState(); }; if (typeof window.setTimeout === "function") { window.setTimeout(run, 0); } else { run(); } } function syncHistoryCommandState() { const buttons = document.querySelectorAll('[data-tlf-toolbar-action="undo"], [data-tlf-toolbar-action="redo"]'); if (!buttons.length) { return; } const surface = TabLocalFiles.TextEditor.getActiveEditingSurface(); if (surface && TabLocalFiles.TextEditor.editingSurfaceClaimsInput()) { buttons.forEach((button) => { button.disabled = !surface.canHistory(button.dataset.tlfToolbarAction === "undo" ? "undo" : "redo"); }); return; } const textarea = TabLocalFiles.TextEditor.findHistoryTextarea(); const editable = Boolean(textarea) && !textarea.readOnly && !textarea.disabled; buttons.forEach((button) => { const command = button.dataset.tlfToolbarAction === "undo" ? "undo" : "redo"; button.disabled = !editable || !historyCommandEnabled(command); }); } function historyCommandEnabled(command) { try { if (typeof document.queryCommandEnabled === "function") { return document.queryCommandEnabled(command); } } catch { } return true; } let editActionAvailabilitySyncScheduled = false; function scheduleEditActionAvailabilitySync() { if (editActionAvailabilitySyncScheduled) { return; } editActionAvailabilitySyncScheduled = true; const run = () => { editActionAvailabilitySyncScheduled = false; syncEditActionAvailability(); }; if (typeof window.setTimeout === "function") { window.setTimeout(run, 0); } else { run(); } } function syncEditActionAvailability() { const buttons = document.querySelectorAll("button[data-tlf-requires-editor-focus]"); if (!buttons.length) { return; } const textarea = TabLocalFiles.TextEditor.findHistoryTextarea(); const textareaFocused = Boolean(textarea && !textarea.readOnly && !textarea.disabled && document.activeElement === textarea); const surface = TabLocalFiles.TextEditor.getActiveEditingSurface(); if (surface) { const surfaceFocused = surface.hasFocus(); buttons.forEach((button) => { button.disabled = button.dataset.tlfSourceOnly === "true" ? !textareaFocused : !(surfaceFocused || textareaFocused); }); return; } buttons.forEach((button) => { button.disabled = !textareaFocused; }); } TabLocalFiles.syncEditActionAvailability = syncEditActionAvailability; function syncToolbarEditState() { const activeEditor = document.querySelector("[data-tlf-editor], [data-tlf-editable-session]"); const dirty = activeEditor?.dataset.tlfDirty === "true"; const autoSave = activeEditor?.dataset.tlfAutoSave === "true"; const editState = activeEditor?.dataset.tlfEditState; const revertable = activeEditor?.dataset.tlfRevertable === "true"; syncHistoryCommandState(); syncEditActionAvailability(); const localSource = isLocalFileSourceUrl(document.querySelector("[data-tlf-file-toolbar]")?.dataset.tlfSourceUrl || ""); const saving = editState === "saving"; const stateKey = saveStateKey(editState || "clean", autoSave, externalWriteActive); syncSaveStateSlots(stateKey); const saveNeeded = localSource && (autoSave ? editState === "error" || editState === "conflict" : dirty); document.querySelectorAll('[data-tlf-toolbar-action="save"]').forEach((button) => { if (!localSource) { return; } button.disabled = !saveNeeded || saving; button.classList.toggle("tlf-toolbar-button-primary", saveNeeded && !saving); button.title = saving ? TabLocalFiles.t("editor.saving") : saveNeeded ? shortcutLabel("common.save", { mod: true, key: "S" }) : stateKey === "external" ? TabLocalFiles.t("editor.externalHint") : autoSave && dirty ? TabLocalFiles.t("common.saveHandledByAutoSave") : TabLocalFiles.t("common.saveNothingToSave"); }); document.querySelectorAll('[data-tlf-toolbar-action="revert"]').forEach((button) => { if (!localSource) { return; } button.disabled = !revertable || saving; button.title = revertable ? TabLocalFiles.t("common.revertToOpened") : TabLocalFiles.t("common.revertUnavailable"); }); document.querySelectorAll('[data-tlf-toolbar-action="duplicate-file"]').forEach((button) => { button.hidden = editState !== "readonly" && editState !== "error"; }); } function setPanelMode(mode) { const page = document.querySelector(".tlf-page"); if (page) { const modeChanged = page.dataset.tlfMode !== mode; page.dataset.tlfMode = mode; const path = document.querySelector("[data-tlf-file-toolbar]")?.dataset.tlfPath; if (modeChanged && path) { writeStoredPanelMode(path, mode); } } document.querySelectorAll('[data-tlf-toolbar-action="mode-preview"]').forEach((button) => { button.setAttribute("aria-pressed", String(mode === "preview")); }); document.querySelectorAll('[data-tlf-toolbar-action="mode-edit"]').forEach((button) => { button.setAttribute("aria-pressed", String(mode === "edit")); }); window.requestAnimationFrame?.(() => { document.querySelectorAll(".tlf-mode-segment").forEach((segment) => { positionSegmentPill(segment, true); }); }); document.querySelectorAll(".tlf-segment").forEach((segment) => { positionSegmentPill(segment, false); }); TabLocalFiles.ToolbarOverflow.refit(); } TabLocalFiles.setPanelMode = setPanelMode; function documentEditMode() { return document.querySelector(".tlf-page")?.dataset.tlfMode === "edit" ? "edit" : "preview"; } TabLocalFiles.documentEditMode = documentEditMode; function isTextCompositionEvent(event) { if (!event) { return false; } if (event.isComposing || event.keyCode === 229) { return true; } return typeof event.getModifierState === "function" && event.getModifierState("AltGraph"); } TabLocalFiles.isTextCompositionEvent = isTextCompositionEvent; function isDocumentEditing() { return documentEditMode() === "edit"; } TabLocalFiles.isDocumentEditing = isDocumentEditing; const PANEL_MODE_STORAGE_PREFIX = "tab-local-files:panel-mode:"; function readStoredPanelMode(path) { try { const stored = window.sessionStorage.getItem(`${PANEL_MODE_STORAGE_PREFIX}${encodeURIComponent(path)}`); return stored === "edit" || stored === "preview" ? stored : undefined; } catch { return undefined; } } function writeStoredPanelMode(path, mode) { try { window.sessionStorage.setItem(`${PANEL_MODE_STORAGE_PREFIX}${encodeURIComponent(path)}`, mode); } catch { } } TabLocalFiles.writeStoredPanelMode = writeStoredPanelMode; function applyStoredPanelMode(context) { if (readStoredPanelMode(context.decodedPath) !== "edit") { return; } const editButton = document.querySelector('[data-tlf-file-toolbar] [data-tlf-toolbar-action="mode-edit"]:not([disabled])'); if (!editButton) { return; } suppressEditorModeAnalytics = true; try { editButton.click(); } finally { suppressEditorModeAnalytics = false; } } TabLocalFiles.applyStoredPanelMode = applyStoredPanelMode; function bindDirectorySettingsControls() { document.querySelectorAll("[data-tlf-directory-sort]").forEach((button) => { if (button.dataset.tlfDirectorySortBound === "true") { return; } button.dataset.tlfDirectorySortBound = "true"; button.addEventListener("click", () => { const sortKey = button.dataset.tlfDirectorySort; if (!sortKey) { return; } TabLocalFiles.Directory.writeSettings({ sortKey }); document.querySelectorAll("[data-tlf-directory-sort]").forEach((option) => { const active = option.dataset.tlfDirectorySort === sortKey; option.setAttribute("aria-checked", String(active)); option.classList.toggle("tlf-directory-sort-option-active", active); }); TabLocalFiles.Directory.applySettingsToSidebars(); TabLocalFiles.Directory.applySettingsToPreview(); }); }); document.querySelectorAll("[data-tlf-directory-sort-direction]").forEach((button) => { if (button.dataset.tlfDirectoryDirectionBound === "true") { return; } button.dataset.tlfDirectoryDirectionBound = "true"; button.addEventListener("click", () => { const sortDirection = button.dataset.tlfDirectorySortDirection; if (sortDirection !== "asc" && sortDirection !== "desc") { return; } TabLocalFiles.Directory.writeSettings({ sortDirection }); document.querySelectorAll("[data-tlf-directory-sort-direction]").forEach((option) => { const active = option.dataset.tlfDirectorySortDirection === sortDirection; option.setAttribute("aria-checked", String(active)); option.classList.toggle("tlf-directory-sort-option-active", active); }); TabLocalFiles.Directory.applySettingsToSidebars(); TabLocalFiles.Directory.applySettingsToPreview(); }); }); document.querySelectorAll("[data-tlf-directory-view-mode]").forEach((button) => { if (button.dataset.tlfDirectoryViewModeBound === "true") { return; } button.dataset.tlfDirectoryViewModeBound = "true"; button.addEventListener("click", () => { const viewMode = button.dataset.tlfDirectoryViewMode; if (viewMode !== "list" && viewMode !== "grid") { return; } TabLocalFiles.Directory.writeSettings({ viewMode }); document.querySelectorAll("[data-tlf-directory-view-mode]").forEach((option) => { const active = option.dataset.tlfDirectoryViewMode === viewMode; option.setAttribute("aria-checked", String(active)); option.classList.toggle("tlf-directory-sort-option-active", active); }); TabLocalFiles.Directory.applySettingsToPreview(); }); }); document.querySelectorAll("[data-tlf-directory-setting]").forEach((input) => { if (input.dataset.tlfDirectorySettingBound === "true") { return; } input.dataset.tlfDirectorySettingBound = "true"; input.addEventListener("change", () => { const setting = input.dataset.tlfDirectorySetting; if (!setting) { return; } const settings = TabLocalFiles.Directory.writeSettings({ [setting]: input.checked }); syncDirectorySettingInputs(settings); if (setting === "rememberSidebarWidth" && !input.checked) { removeLocalStorage(DIRECTORY_SIDEBAR_WIDTH_STORAGE_KEY); } TabLocalFiles.Directory.applySettingsToSidebars(); TabLocalFiles.Directory.applySettingsToPreview(); }); }); document.querySelectorAll("[data-tlf-open-settings-page]").forEach((button) => { if (button.dataset.tlfOpenSettingsBound === "true") { return; } button.dataset.tlfOpenSettingsBound = "true"; button.addEventListener("click", () => { openSettingsPage(); }); }); } function openSettingsPage() { const runtime = globalThis.chrome?.runtime; const sendMessage = runtime?.sendMessage; if (!sendMessage) { return; } try { sendMessage.call(runtime, { type: "tlf-open-settings-page" }); } catch { } } function bindDirectoryMenuDismissControls() { document.querySelectorAll(DIRECTORY_POPUP_MENU_SELECTOR).forEach((menu) => { if (menu.dataset.tlfDirectoryPopupBound === "true") { return; } menu.dataset.tlfDirectoryPopupBound = "true"; menu.addEventListener("toggle", () => { if (menu.open) { closeDirectoryPopupMenus(menu); syncDirectoryPopupMenuPosition(menu); } else { resetDirectoryPopupMenuPosition(menu); } }); }); if (directoryMenuDismissControlsBound) { return; } directoryMenuDismissControlsBound = true; document.addEventListener("pointerdown", (event) => { closeDirectoryPopupMenusOutside(event.target); }); document.addEventListener("focusin", (event) => { closeDirectoryPopupMenusOutside(event.target); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeDirectoryPopupMenus(); } }); window.addEventListener("blur", () => { closeDirectoryPopupMenus(); }); window.addEventListener("resize", syncOpenDirectoryPopupMenus); } function bindFilePathBreadcrumbControls() { const breadcrumbs = Array.from(document.querySelectorAll("[data-tlf-file-path-breadcrumb]")); if (!breadcrumbs.length) { return; } let animationFrame = 0; const scheduleSync = () => { if (animationFrame) { return; } animationFrame = window.requestAnimationFrame(() => { animationFrame = 0; breadcrumbs.forEach(syncFilePathBreadcrumb); }); }; const resizeObserver = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(scheduleSync); breadcrumbs.forEach((breadcrumb) => { resizeObserver?.observe(breadcrumb); }); window.addEventListener("resize", scheduleSync); scheduleSync(); registerDocumentMountCleanup(currentDocumentMountId, () => { if (animationFrame) { window.cancelAnimationFrame(animationFrame); } resizeObserver?.disconnect(); window.removeEventListener("resize", scheduleSync); }); } function bindSegmentPillControls() { const segments = Array.from(document.querySelectorAll(".tlf-mode-segment, .tlf-segment")); if (!segments.length) { return; } let animationFrame = 0; const scheduleSync = () => { if (animationFrame) { return; } animationFrame = window.requestAnimationFrame(() => { animationFrame = 0; segments.forEach((segment) => positionSegmentPill(segment, false)); }); }; const resizeObserver = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(scheduleSync); segments.forEach((segment) => { resizeObserver?.observe(segment); }); window.addEventListener("resize", scheduleSync); scheduleSync(); registerDocumentMountCleanup(currentDocumentMountId, () => { if (animationFrame) { window.cancelAnimationFrame(animationFrame); } resizeObserver?.disconnect(); window.removeEventListener("resize", scheduleSync); }); } function syncFilePathBreadcrumb(breadcrumb) { const fullPath = breadcrumb.querySelector("[data-tlf-file-path-full]"); const measurePath = breadcrumb.querySelector("[data-tlf-file-path-measure]"); const collapsedPath = breadcrumb.querySelector("[data-tlf-file-path-collapsed]"); if (!fullPath || !collapsedPath) { delete breadcrumb.dataset.tlfPathCollapsed; return; } delete breadcrumb.dataset.tlfPathCollapsed; const availableWidth = Math.ceil(breadcrumb.getBoundingClientRect().width); const measuredPath = measurePath || fullPath; const fullPathWidth = Math.ceil(measuredPath.getBoundingClientRect().width || measuredPath.scrollWidth); const shouldCollapse = availableWidth > 0 && fullPathWidth > availableWidth + 1; if (shouldCollapse) { breadcrumb.dataset.tlfPathCollapsed = "true"; } else { collapsedPath.querySelectorAll("[data-tlf-file-path-overflow-menu]").forEach((menu) => { menu.open = false; }); } } function closeDirectoryPopupMenus(exceptMenu) { document.querySelectorAll(DIRECTORY_POPUP_MENU_SELECTOR).forEach((menu) => { if (menu !== exceptMenu && !(exceptMenu && menu.contains(exceptMenu))) { menu.open = false; resetDirectoryPopupMenuPosition(menu); } }); } function closeDirectoryPopupMenusOutside(target) { document.querySelectorAll(DIRECTORY_POPUP_MENU_SELECTOR).forEach((menu) => { if (menu.open && !isEventTargetWithin(menu, target)) { menu.open = false; resetDirectoryPopupMenuPosition(menu); } }); } function isEventTargetWithin(element, target) { return typeof Node !== "undefined" && target instanceof Node && element.contains(target); } function syncOpenDirectoryPopupMenus() { document.querySelectorAll(DIRECTORY_POPUP_MENU_SELECTOR).forEach((menu) => { if (menu.open) { syncDirectoryPopupMenuPosition(menu); } }); } function syncDirectoryPopupMenuPosition(menu) { const panel = directorySidebarPopupPanel(menu); const trigger = menu.querySelector("summary"); if (!panel || !trigger) { resetDirectoryPopupMenuPosition(menu); return; } const triggerRect = trigger.getBoundingClientRect(); const panelRect = panel.getBoundingClientRect(); const panelWidth = Math.ceil(panelRect.width || panel.offsetWidth || panel.scrollWidth); const panelHeight = Math.ceil(panelRect.height || panel.offsetHeight || panel.scrollHeight); if (!panelWidth || !panelHeight) { resetDirectoryPopupMenuPosition(menu); return; } const viewportWidth = Number(window.innerWidth) || document.documentElement.clientWidth || panelWidth + DIRECTORY_POPUP_VIEWPORT_PADDING * 2; const viewportHeight = Number(window.innerHeight) || document.documentElement.clientHeight || panelHeight + DIRECTORY_POPUP_VIEWPORT_PADDING * 2; const maxLeft = Math.max(DIRECTORY_POPUP_VIEWPORT_PADDING, viewportWidth - panelWidth - DIRECTORY_POPUP_VIEWPORT_PADDING); const maxTop = Math.max(DIRECTORY_POPUP_VIEWPORT_PADDING, viewportHeight - panelHeight - DIRECTORY_POPUP_VIEWPORT_PADDING); const preferredLeft = menu.dataset.tlfPopupAlign === "start" ? triggerRect.left : triggerRect.right - panelWidth; const preferredTop = triggerRect.bottom + DIRECTORY_POPUP_GAP; const flippedTop = triggerRect.top - panelHeight - DIRECTORY_POPUP_GAP; const top = preferredTop > maxTop && flippedTop >= DIRECTORY_POPUP_VIEWPORT_PADDING ? flippedTop : clampNumber(preferredTop, DIRECTORY_POPUP_VIEWPORT_PADDING, maxTop); const left = clampNumber(preferredLeft, DIRECTORY_POPUP_VIEWPORT_PADDING, maxLeft); menu.dataset.tlfPopupPositioned = "true"; panel.style.setProperty("--tlf-directory-popup-left", `${left}px`); panel.style.setProperty("--tlf-directory-popup-top", `${top}px`); } function resetDirectoryPopupMenuPosition(menu) { const panel = directorySidebarPopupPanel(menu); delete menu.dataset.tlfPopupPositioned; panel?.style.removeProperty("--tlf-directory-popup-left"); panel?.style.removeProperty("--tlf-directory-popup-top"); } function directorySidebarPopupPanel(menu) { return menu.querySelector(DIRECTORY_SIDEBAR_POPUP_PANEL_SELECTOR); } function clampNumber(value, min, max) { return Math.min(max, Math.max(min, value)); } function syncDirectorySettingInputs(settings = TabLocalFiles.Directory.readSettings()) { document.querySelectorAll("[data-tlf-directory-setting]").forEach((input) => { const setting = input.dataset.tlfDirectorySetting; if (!setting) { return; } input.checked = Boolean(settings[setting]); const row = input.closest(".tlf-directory-setting-row"); row?.setAttribute("aria-checked", input.checked ? "true" : "false"); }); } function refreshDirectorySettingControls() { const settings = TabLocalFiles.Directory.readSettings(); syncDirectorySettingInputs(settings); document.querySelectorAll("[data-tlf-directory-sort]").forEach((option) => { const active = option.dataset.tlfDirectorySort === settings.sortKey; option.setAttribute("aria-checked", String(active)); option.classList.toggle("tlf-directory-sort-option-active", active); }); document.querySelectorAll("[data-tlf-directory-sort-direction]").forEach((option) => { const active = option.dataset.tlfDirectorySortDirection === settings.sortDirection; option.setAttribute("aria-checked", String(active)); option.classList.toggle("tlf-directory-sort-option-active", active); }); document.querySelectorAll("[data-tlf-directory-view-mode]").forEach((option) => { const active = option.dataset.tlfDirectoryViewMode === settings.viewMode; option.setAttribute("aria-checked", String(active)); option.classList.toggle("tlf-directory-sort-option-active", active); }); } TabLocalFiles.refreshDirectorySettingControls = refreshDirectorySettingControls; function renderFavoriteToggle(context) { if (!canBookmarkSource(context.sourceUrl, context.decodedPath)) { return ""; } const label = TabLocalFiles.t("favorites.add"); return ``; } function canBookmarkSource(sourceUrl, decodedPath) { return isLocalFileSourceUrl(sourceUrl) && !TabLocalFiles.ZipArchive.isVirtualPath(decodedPath); } function bookmarkEntryFromToolbar(toolbar) { const sourceUrl = toolbar.dataset.tlfSourceUrl || ""; const path = toolbar.dataset.tlfPath || ""; const name = toolbar.dataset.tlfFileName || ""; const fileType = toolbar.dataset.tlfFileType || ""; if (!sourceUrl || !path || !name || !canBookmarkSource(sourceUrl, path)) { return undefined; } const kind = fileType === "directory" ? "directory" : "file"; return { url: sourceUrl, path, name, kind, icon: kind === "directory" ? "folder" : TabLocalFiles.fileIconNameForPath(path), fileType: fileType || undefined }; } TabLocalFiles.bookmarkEntryFromToolbar = bookmarkEntryFromToolbar; function bindFavoriteControls() { document.querySelectorAll("[data-tlf-file-toolbar]").forEach((toolbar) => { if (toolbar.dataset.tlfBookmarkBound === "true") { return; } toolbar.dataset.tlfBookmarkBound = "true"; const entry = bookmarkEntryFromToolbar(toolbar); const button = toolbar.querySelector("[data-tlf-favorite-toggle]"); if (!entry) { button?.remove(); return; } void TabLocalFiles.FileBookmarks.recordRecent(entry); if (!button) { return; } const setPressed = (favorite) => { const label = TabLocalFiles.t(favorite ? "favorites.remove" : "favorites.add"); button.setAttribute("aria-pressed", String(favorite)); button.setAttribute("aria-label", label); button.title = label; }; void TabLocalFiles.FileBookmarks.isFavorite(entry.url).then(setPressed); button.addEventListener("click", () => { button.disabled = true; void TabLocalFiles.FileBookmarks.toggleFavorite(entry) .then((favorite) => { setPressed(favorite); showToast(TabLocalFiles.t(favorite ? "favorites.added" : "favorites.removed"), "success"); }) .finally(() => { button.disabled = false; }); }); }); } function bindRenameControls() { document.querySelectorAll("[data-tlf-file-toolbar]").forEach((toolbar) => { const trigger = toolbar.querySelector("[data-tlf-rename-trigger]"); const form = toolbar.querySelector("[data-tlf-rename-form]"); const input = toolbar.querySelector("[data-tlf-rename-input]"); const status = toolbar.querySelector("[data-tlf-rename-status]"); const path = toolbar.dataset.tlfPath; const currentFileName = toolbar.dataset.tlfFileName || trigger?.textContent || ""; if (!trigger || !form || !input || !path || !currentFileName) { return; } let submitting = false; let cancelRequested = false; const setRenaming = (renaming) => { toolbar.dataset.tlfRenaming = String(renaming); trigger.hidden = renaming; form.hidden = !renaming; }; const reset = () => { cancelRequested = false; submitting = false; input.disabled = false; input.value = currentFileName; setRenaming(false); }; const beginRename = () => { cancelRequested = false; clearRenameStatus(status); input.value = currentFileName; input.disabled = false; setRenaming(true); input.focus(); input.select(); }; const cancelRename = () => { cancelRequested = true; clearRenameStatus(status); reset(); }; const submitRename = async () => { if (submitting || cancelRequested) { return; } const nextName = input.value.trim(); if (!nextName || nextName === currentFileName) { reset(); return; } submitting = true; input.disabled = true; const canRename = await TabLocalFiles.EditSession.requestNavigation(); if (!canRename) { submitting = false; input.disabled = false; clearRenameStatus(status); input.focus(); input.select(); return; } const invalidReason = validateRenameTarget(toolbar, currentFileName, nextName); if (invalidReason) { submitting = false; input.disabled = false; setRenameStatus(status, invalidReason, "error"); input.focus(); input.select(); return; } const previousExtension = TabLocalFiles.extensionFromFileName(currentFileName); const nextExtension = TabLocalFiles.extensionFromFileName(nextName); if (previousExtension !== nextExtension) { const confirmed = await TabLocalFiles.EditSession.confirmExtensionChange(nextName, nextExtension); if (!confirmed) { submitting = false; input.disabled = false; clearRenameStatus(status); input.focus(); input.select(); return; } } setRenameStatus(status, TabLocalFiles.t("rename.inProgress")); const result = await TabLocalFiles.renameOriginalFile(path, nextName); if (result.ok && result.fileUrl) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "rename", result: "success" }); setRenameStatus(status, TabLocalFiles.t("rename.success"), "success"); window.location.href = extensionViewerUrl(result.fileUrl, sidebarSnapshotWithRenamedFile(toolbar, path, currentFileName, result)); return; } submitting = false; input.disabled = false; TabLocalFiles.recordCurrentFileOperationResult({ operation: "rename", result: "failed", failureStage: "rename" }); setRenameStatus(status, TabLocalFiles.t("rename.failed", { error: result.error || TabLocalFiles.t("common.unknownError") }), "error"); input.focus(); input.select(); }; trigger.addEventListener("click", beginRename); form.addEventListener("submit", (event) => { event.preventDefault(); void submitRename(); }); input.addEventListener("keydown", (event) => { if (event.key === "Escape") { event.preventDefault(); cancelRename(); } }); input.addEventListener("blur", () => { window.setTimeout(() => { void submitRename(); }, 0); }); }); } function hasControlCharacters(value) { for (let index = 0; index < value.length; index += 1) { const code = value.charCodeAt(index); if (code < 32 || code === 127) { return true; } } return false; } function validateRenameTarget(toolbar, currentFileName, nextName) { if (/[\\/:*?"<>|]/.test(nextName) || hasControlCharacters(nextName) || nextName === "." || nextName === "..") { return TabLocalFiles.t("rename.invalidName"); } const entries = readDirectorySnapshotFromElement(toolbar)?.entries || []; const normalizedNext = nextName.toLowerCase(); const duplicated = entries.some((entry) => entry.name.replace(/\/+$/, "").toLowerCase() === normalizedNext && entry.name !== currentFileName); return duplicated ? TabLocalFiles.t("rename.duplicateName") : undefined; } function setRenameStatus(status, message, state = "neutral") { if (!status) { return; } status.hidden = !message; status.textContent = message; status.dataset.tlfRenameStatus = state; } function clearRenameStatus(status) { setRenameStatus(status, ""); } function handleToolbarAction(action, button, toolbar) { if (action === "print") { if (dispatchToolbarAction(action, button, toolbar)) { printDocumentBody(); } return; } if (action === "mode-edit" || action === "mode-preview") { const editing = action === "mode-edit"; if (editing && !isEditableLocalFileSourceUrl(toolbar.dataset.tlfSourceUrl || "")) { disableToolbarButton(button, TabLocalFiles.t("common.editUnavailableRemote")); return; } if (dispatchToolbarAction(action, button, toolbar)) { if (!TabLocalFiles.TextEditor.setModeFirst(editing)) { setPanelMode(editing ? "edit" : "preview"); } } if (!suppressEditorModeAnalytics) { recordCurrentEditorModeChange(editing ? "edit" : "preview"); } return; } if (action === "save") { if (!isEditableLocalFileSourceUrl(toolbar.dataset.tlfSourceUrl || "")) { disableToolbarButton(button, TabLocalFiles.t("common.editUnavailableRemote")); return; } if (dispatchToolbarAction(action, button, toolbar)) { void TabLocalFiles.TextEditor.saveFirst(); } return; } if (action === "revert") { if (!isLocalFileSourceUrl(toolbar.dataset.tlfSourceUrl || "")) { disableToolbarButton(button, TabLocalFiles.t("common.editUnavailableRemote")); return; } if (dispatchToolbarAction(action, button, toolbar)) { void TabLocalFiles.TextEditor.revertFirst(); } return; } if (action === "undo" || action === "redo") { if (dispatchToolbarAction(action, button, toolbar)) { TabLocalFiles.TextEditor.runHistoryCommandFirst(action); syncHistoryCommandState(); } return; } if (action === "copy-all") { if (dispatchToolbarAction(action, button, toolbar)) { void TabLocalFiles.TextEditor.copyFirst(); } return; } if (action === "reload") { if (dispatchToolbarAction(action, button, toolbar)) { void reloadEditorSourceFromDisk(); } return; } if (action === "copy-path") { void copyToolbarPath(toolbar); return; } if (action === "duplicate-file") { void duplicateToolbarFile(button, toolbar); return; } if (action === "trash") { const path = toolbar.dataset.tlfPath; if (path) { void TabLocalFiles.DirectoryManage.trashOpenDocument(path, toolbar.dataset.tlfFileType === "directory"); } return; } if (action === "open-new-tab") { const sourceUrl = toolbar.dataset.tlfSourceUrl; if (sourceUrl) { openSourceInNewTab(sourceUrl); } return; } if (action === "download-source") { void downloadToolbarSource(button, toolbar); return; } if (action === "raw") { const sourceUrl = toolbar.dataset.tlfSourceUrl; if (sourceUrl) { window.location.href = browserOpenUrl(sourceUrl); } return; } if (action === "reveal") { if (!isLocalFileSourceUrl(toolbar.dataset.tlfSourceUrl || "")) { disableToolbarButton(button, TabLocalFiles.t("common.revealUnavailableRemote")); return; } void openToolbarPath(toolbar); return; } dispatchToolbarAction(action, button, toolbar); } async function reloadEditorSourceFromDisk() { const root = document.querySelector("[data-tlf-editor]"); const toolbar = document.querySelector("[data-tlf-file-toolbar]"); const sourceUrl = toolbar?.dataset.tlfSourceUrl; if (!root?.id || !sourceUrl) { return; } if (!(await TabLocalFiles.EditSession.requestNavigation())) { return; } const source = await TabLocalFiles.readCurrentFileAsText(sourceUrl); if (source === undefined) { showToast(TabLocalFiles.t("common.reloadFailed"), "error"); return; } TabLocalFiles.TextEditor.rebaseFromDisk(root.id, source); showToast(TabLocalFiles.t("common.reloadedFromDisk"), "success"); } async function copyToolbarPath(toolbar) { const path = toolbar.dataset.tlfPath || toolbar.dataset.tlfSourceUrl || ""; if (!path) { return; } try { await copyPlainTextToClipboard(path); showToast(TabLocalFiles.t("common.copiedPath"), "success"); } catch { showToast(TabLocalFiles.t("common.copyFailed"), "error"); } } async function copyPlainTextToClipboard(value) { if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(value); return; } catch { } } const fallback = document.createElement("textarea"); fallback.value = value; fallback.readOnly = true; fallback.setAttribute("aria-hidden", "true"); fallback.style.position = "fixed"; fallback.style.inset = "0"; fallback.style.opacity = "0"; fallback.style.pointerEvents = "none"; document.body.append(fallback); fallback.focus(); fallback.select(); const copied = document.execCommand("copy"); fallback.remove(); if (!copied) { throw new Error("Copy failed"); } } TabLocalFiles.copyPlainTextToClipboard = copyPlainTextToClipboard; async function duplicateToolbarFile(button, toolbar) { const path = toolbar.dataset.tlfPath; const sourceUrl = toolbar.dataset.tlfSourceUrl; if (!path || !sourceUrl || !isEditableLocalFileSourceUrl(sourceUrl)) { return; } if (!(await TabLocalFiles.EditSession.requestNavigation())) { return; } button.disabled = true; try { const text = await TabLocalFiles.readCurrentFileAsText(sourceUrl); if (text === undefined) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "duplicate", result: "failed", failureStage: "read" }); showToast(TabLocalFiles.t("duplicate.readFailed"), "error"); return; } const directoryPath = directoryPathForFile(path) || "/"; const entries = (await TabLocalFiles.listDirectoryEntries(directoryPath)) || []; const existingNames = new Set(entries.map((entry) => entry.name.toLowerCase())); const copyName = duplicateFileNameFor(toolbar.dataset.tlfFileName || fileNameFromPath(path), existingNames); const result = await TabLocalFiles.createTextDocument(directoryPath, copyName, text); if (!result.ok || !result.fileUrl || !result.path) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "duplicate", result: "failed", failureStage: "create" }); showToast(TabLocalFiles.t("duplicate.failed", { error: result.error || TabLocalFiles.t("common.unknownError") }), "error"); return; } writeStoredPanelMode(result.path, "edit"); TabLocalFiles.recordCurrentFileOperationResult({ operation: "duplicate", result: "success" }); showToast(TabLocalFiles.t("duplicate.success", { fileName: copyName }), "success"); window.location.href = extensionViewerUrl(result.fileUrl, readDirectorySnapshotFromElement(toolbar)); } finally { button.disabled = false; } } function duplicateFileNameFor(fileName, existingNames) { const extension = fileName.match(/\.[^.]+$/)?.[0] || ""; const baseName = extension ? fileName.slice(0, -extension.length) : fileName; for (let index = 1; index <= 500; index += 1) { const candidate = index === 1 ? `${TabLocalFiles.t("duplicate.namePattern", { name: baseName })}${extension}` : `${TabLocalFiles.t("duplicate.namePatternIndexed", { name: baseName, index })}${extension}`; if (!existingNames.has(candidate.toLowerCase())) { return candidate; } } return `${TabLocalFiles.t("duplicate.namePattern", { name: baseName })}${extension}`; } async function downloadToolbarSource(button, toolbar) { const sourceUrl = toolbar.dataset.tlfSourceUrl || ""; if (!isRemoteHttpSourceUrl(sourceUrl)) { disableToolbarButton(button, TabLocalFiles.t("common.downloadUnavailable")); return; } recordDownloadSourceAnalytics("click", "download_source_click", toolbar); const previousLabel = button.title || TabLocalFiles.t("common.downloadRemoteFile"); button.disabled = true; button.title = TabLocalFiles.t("common.downloadingRemoteFile"); button.setAttribute("aria-label", TabLocalFiles.t("common.downloadingRemoteFile")); try { const mode = await TabLocalFiles.saveRemoteSourceFile(sourceUrl, toolbar.dataset.tlfFileName || TabLocalFiles.t("common.localFile")); recordDownloadSourceAnalytics("task", "download_source_success", toolbar, { save_mode: mode }); showToast(mode === "picker" ? TabLocalFiles.t("common.downloadedRemoteFile") : TabLocalFiles.t("common.exportedToDownloads"), "success"); } catch (error) { const cancelled = error instanceof DOMException && error.name === "AbortError"; recordDownloadSourceAnalytics("task", cancelled ? "download_source_cancelled" : "download_source_failed", toolbar, { error_name: error instanceof Error ? error.name : typeof error }); showToast(cancelled ? TabLocalFiles.t("common.cancelledDownload") : TabLocalFiles.t("common.downloadRemoteFailed"), cancelled ? "neutral" : "error"); } finally { button.disabled = false; button.title = previousLabel; button.setAttribute("aria-label", previousLabel); } } function recordDownloadSourceAnalytics(type, eventName, toolbar, details = {}) { void recordCurrentFileAnalyticsEvent({ type, name: eventName, value: "download-source", triggerMethod: "manual_click", subModuleName: "toolbar", fields: details }); } function recordCurrentFileAnalyticsEvent(input) { const toolbar = document.querySelector("[data-tlf-file-toolbar]"); return TabLocalFiles.recordFileAnalyticsEvent({ ...input, sourceUrl: toolbar?.dataset.tlfSourceUrl || "", fileName: toolbar?.dataset.tlfFileName || "", fileType: toolbar?.dataset.tlfFileType || "unknown" }); } TabLocalFiles.recordCurrentFileAnalyticsEvent = recordCurrentFileAnalyticsEvent; function recordCurrentEditorModeChange(mode) { void recordCurrentFileAnalyticsEvent({ type: "click", name: "editor_mode_change", value: "editor-mode", triggerMethod: "manual_click", subModuleName: "editor", fields: { mode } }); } TabLocalFiles.recordCurrentEditorModeChange = recordCurrentEditorModeChange; function classifyFileSaveFailure(error) { const message = (typeof error === "string" ? error : error && typeof error === "object" && "message" in error && typeof error.message === "string" ? error.message : "").toLowerCase(); if (message.includes("requires a user gesture")) { return "user_gesture_required"; } if (message.includes("invalid or expired workspace token")) { return "workspace_token_invalid"; } if (message.includes("modified outside the workspace")) { return "external_conflict"; } if (message.includes("size exceeds limit")) { return "size_limit"; } if (message.includes("read-only") || message.includes("not allowed") || message.includes("blocked") || message.includes("requires a writable workspace") || message.includes("must stay inside the workspace")) { return "write_not_allowed"; } if (message.includes("failed to ")) { return "filesystem_error"; } return "unknown"; } TabLocalFiles.classifyFileSaveFailure = classifyFileSaveFailure; function createCurrentFileSaveResultRecorder() { let lastAutomaticFailureRevision; return { record(input, revision) { if (input.automatic && input.result === "failed" && revision !== undefined) { if (lastAutomaticFailureRevision === revision) { return; } lastAutomaticFailureRevision = revision; } else if (input.result === "success") { lastAutomaticFailureRevision = undefined; } TabLocalFiles.recordCurrentFileSaveResult(input); } }; } TabLocalFiles.createCurrentFileSaveResultRecorder = createCurrentFileSaveResultRecorder; function recordCurrentFileSaveResult(input) { void recordCurrentFileAnalyticsEvent({ type: "task", name: "file_save_result", value: "file-save", triggerMethod: input.automatic ? "auto" : "manual_click", subModuleName: "editor", fields: { result: input.result, save_mode: input.saveMode, automatic: input.automatic, conflict_detected: input.conflictDetected, ...(input.error ? { error_name: input.error instanceof Error ? input.error.name : "UnknownError", ...(input.result === "failed" ? { failure_reason: classifyFileSaveFailure(input.error) } : {}) } : {}) } }); } TabLocalFiles.recordCurrentFileSaveResult = recordCurrentFileSaveResult; function recordCurrentFileOperationResult(input) { void recordCurrentFileAnalyticsEvent({ type: "task", name: "file_operation_result", value: "file-operation", triggerMethod: "manual_click", subModuleName: "workspace", fields: { operation: input.operation, result: input.result, ...(input.documentType ? { document_type: input.documentType } : {}), ...(input.failureStage ? { failure_stage: input.failureStage } : {}) } }); } TabLocalFiles.recordCurrentFileOperationResult = recordCurrentFileOperationResult; function recordCurrentFileExportResult(input) { void recordCurrentFileAnalyticsEvent({ type: "task", name: "file_export_result", value: "file-export", triggerMethod: "manual_click", subModuleName: "toolbar", fields: { format: input.format, result: input.result, ...(input.failureReason ? { failure_reason: input.failureReason } : {}), ...(input.error ? { error_name: input.error instanceof Error ? input.error.name : "UnknownError" } : {}) } }); } TabLocalFiles.recordCurrentFileExportResult = recordCurrentFileExportResult; function recordFileTranslateResult(context, input) { void recordRenderContextAnalyticsEvent(context, { type: "task", name: "file_translate_result", value: `${input.kind}-translate`, triggerMethod: "manual_click", subModuleName: "translation", fields: { result: input.result, ...(input.failureReason ? { failure_reason: input.failureReason } : {}), output: input.output, lang: input.targetCode, block_count: input.blockCount, mode: input.mode } }); } TabLocalFiles.recordFileTranslateResult = recordFileTranslateResult; function translateTransportFailureReason(error) { return error instanceof Error && error.message === "timeout" ? "timeout" : "unavailable"; } TabLocalFiles.translateTransportFailureReason = translateTransportFailureReason; function recordRenderContextAnalyticsEvent(context, input) { return TabLocalFiles.recordFileAnalyticsEvent({ ...input, sourceUrl: context.sourceUrl, fileName: context.fileName, fileType: context.definition.id }); } TabLocalFiles.recordRenderContextAnalyticsEvent = recordRenderContextAnalyticsEvent; function printDocumentBody() { startBrowserPrint(); } TabLocalFiles.printDocumentBody = printDocumentBody; function printPreparedDocument(root, cleanup) { if (activePrintCleanup) { cleanup?.(); return; } root.classList.add("tlf-print-document"); document.body?.appendChild(root); document.documentElement.dataset.tlfPrintTarget = "prepared"; startBrowserPrint(() => { root.remove(); cleanup?.(); }); } TabLocalFiles.printPreparedDocument = printPreparedDocument; async function printImageBlobs(blobs, altTexts = []) { if (!blobs.length) { throw new Error("No printable images were provided"); } const root = document.createElement("section"); const objectUrls = []; const cleanup = () => { root.remove(); objectUrls.forEach((url) => URL.revokeObjectURL(url)); }; try { for (let index = 0; index < blobs.length; index += 1) { const page = document.createElement("figure"); page.className = "tlf-print-page"; const image = document.createElement("img"); image.className = "tlf-print-page-image"; image.alt = altTexts[index] || TabLocalFiles.t("print.pageAlt", { page: index + 1 }); const objectUrl = URL.createObjectURL(blobs[index]); objectUrls.push(objectUrl); page.appendChild(image); root.appendChild(page); if (typeof image.decode === "function") { image.src = objectUrl; await image.decode(); } else { await new Promise((resolve, reject) => { image.addEventListener("load", () => resolve(), { once: true }); image.addEventListener("error", () => reject(new Error("Unable to load a printable image")), { once: true }); image.src = objectUrl; }); } } printPreparedDocument(root, cleanup); } catch (error) { cleanup(); throw error; } } TabLocalFiles.printImageBlobs = printImageBlobs; function downloadTextFile(fileName, content, mimeType) { const blob = new Blob([content], { type: `${mimeType};charset=utf-8` }); const objectUrl = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = objectUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0); } TabLocalFiles.downloadTextFile = downloadTextFile; function canvasToPngBlob(canvas) { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error("Unable to export canvas")); } }, "image/png"); }); } TabLocalFiles.canvasToPngBlob = canvasToPngBlob; function startBrowserPrint(cleanup) { if (activePrintCleanup) { cleanup?.(); return; } document.documentElement.dataset.tlfPrintBody = "true"; let cleaned = false; const clearPrintMode = () => { if (cleaned) { return; } cleaned = true; delete document.documentElement.dataset.tlfPrintBody; delete document.documentElement.dataset.tlfPrintTarget; window.removeEventListener("afterprint", clearPrintMode); activePrintCleanup = undefined; cleanup?.(); }; activePrintCleanup = clearPrintMode; window.addEventListener("afterprint", clearPrintMode, { once: true }); void whenPrintLayoutSettled().then(() => { try { window.print(); } catch (error) { clearPrintMode(); throw error; } }); } async function withPrintLayout(work) { if (activePrintCleanup) { throw new Error("A print is already in progress"); } document.documentElement.dataset.tlfPrintBody = "true"; try { await whenPrintLayoutSettled(); return await work(); } finally { delete document.documentElement.dataset.tlfPrintBody; } } TabLocalFiles.withPrintLayout = withPrintLayout; function whenPrintLayoutSettled() { const FONT_WAIT_MS = 2000; const PAINT_WAIT_MS = 300; const fontsReady = document.fonts?.ready ? Promise.race([document.fonts.ready, delay(FONT_WAIT_MS)]) : Promise.resolve(); return fontsReady.then(() => new Promise((resolve) => { if (typeof window.requestAnimationFrame !== "function") { window.setTimeout(resolve, 0); return; } const settled = new Promise((paint) => { window.requestAnimationFrame(() => window.requestAnimationFrame(() => paint())); }); void Promise.race([settled, delay(PAINT_WAIT_MS)]).then(() => resolve()); })); } TabLocalFiles.whenPrintLayoutSettled = whenPrintLayoutSettled; function delay(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function clickFirst(selector) { const target = document.querySelector(selector); target?.click(); return Boolean(target); } function disableToolbarButton(button, label) { button.disabled = true; button.title = label; button.setAttribute("aria-label", label); } function openSourceInNewTab(sourceUrl) { const opened = window.open(browserOpenUrl(sourceUrl), "_blank", "noopener,noreferrer"); if (opened) { opened.opener = null; } } function browserOpenUrl(sourceUrl) { if (TabLocalFiles.isArchiveEntrySourceUrl(sourceUrl)) { return extensionViewerUrl(sourceUrl); } return isEditableLocalFileSourceUrl(sourceUrl) ? TabLocalFiles.rawFileUrl(sourceUrl) : sourceUrl; } async function openToolbarPath(toolbar) { const path = toolbar.dataset.tlfPath; if (!path) { return; } await TabLocalFiles.openPathInFileManager(path); } function isLocalFileSourceUrl(sourceUrl) { try { return new URL(sourceUrl).protocol === "file:"; } catch { return false; } } TabLocalFiles.isLocalFileSourceUrl = isLocalFileSourceUrl; function isEditableLocalFileSourceUrl(sourceUrl) { return isLocalFileSourceUrl(sourceUrl) && !TabLocalFiles.isArchiveEntrySourceUrl(sourceUrl); } TabLocalFiles.isEditableLocalFileSourceUrl = isEditableLocalFileSourceUrl; function isRemoteHttpSourceUrl(sourceUrl) { try { const protocol = new URL(sourceUrl).protocol; return protocol === "http:" || protocol === "https:"; } catch { return false; } } function triggerToolbarAction(action) { const toolbar = document.querySelector("[data-tlf-file-toolbar]"); const button = toolbar?.querySelector(`[data-tlf-toolbar-action="${action}"]`); if (!button || button.disabled) { return false; } button.click(); return true; } TabLocalFiles.triggerToolbarAction = triggerToolbarAction; function dispatchToolbarAction(action, button, toolbar) { return toolbar.dispatchEvent(new CustomEvent(TOOLBAR_ACTION_EVENT, { bubbles: true, cancelable: true, detail: { action, button, toolbar } })); } function onToolbarAction(action, handler) { const toolbar = document.querySelector("[data-tlf-file-toolbar]"); if (!toolbar) { return () => undefined; } const listener = (event) => { const detail = event.detail; if (!detail || detail.action !== action) { return; } event.preventDefault(); handler(detail); }; toolbar.addEventListener(TOOLBAR_ACTION_EVENT, listener); return () => toolbar.removeEventListener(TOOLBAR_ACTION_EVENT, listener); } TabLocalFiles.onToolbarAction = onToolbarAction; function bindCreateDocumentControls() { bindCreateDocumentMenuControls(); } function bindCreateDocumentMenuControls() { document.querySelectorAll("[data-tlf-create-document-type]").forEach((button) => { if (button.dataset.tlfCreateDocumentMenuBound === "true") { return; } button.dataset.tlfCreateDocumentMenuBound = "true"; button.addEventListener("click", () => { void submitCreateDocumentFromMenu(button); }); }); } async function submitCreateDocumentFromMenu(button) { const typeId = button.dataset.tlfCreateDocumentType || "markdown"; const directoryPath = button.dataset.tlfCreateDocumentDirectory || undefined; const menu = button.closest("[data-tlf-create-document-menu]"); const status = menu?.querySelector("[data-tlf-create-document-menu-status]"); const existingNames = visibleDirectoryFileNames(button); const fileName = TabLocalFiles.suggestedUniqueDocumentFileName(typeId, existingNames); button.disabled = true; setCreateDocumentMenuStatus(status || undefined, TabLocalFiles.t("create.inProgress", { fileName })); try { const result = await createSidebarTextDocument(directoryPath, typeId, existingNames); if (!result.ok || !result.fileUrl) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "create", result: "failed", documentType: typeId, failureStage: "create" }); setCreateDocumentMenuStatus(status || undefined, TabLocalFiles.t("create.failed", { error: result.error || TabLocalFiles.t("common.unknownError") }), "error"); return; } if (result.path) { writeStoredPanelMode(result.path, "edit"); } TabLocalFiles.recordCurrentFileOperationResult({ operation: "create", result: "success", documentType: typeId }); setCreateDocumentMenuStatus(status || undefined, TabLocalFiles.t("create.success"), "success"); if (menu) { menu.open = false; } window.location.href = extensionViewerUrl(result.fileUrl, sidebarSnapshotWithCreatedFile(button, result)); } catch (error) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "create", result: "failed", documentType: typeId, failureStage: "create" }); setCreateDocumentMenuStatus(status || undefined, TabLocalFiles.t("create.failed", { error: error instanceof Error ? error.message : String(error) }), "error"); } finally { button.disabled = false; } } async function createSidebarTextDocument(directoryPath, typeId, existingNames) { if (!directoryPath) { return { ok: false, error: TabLocalFiles.t("create.missingDirectory") }; } if (!TabLocalFiles.TabLocalWorkspace.isAvailable()) { return { ok: false, error: TabLocalFiles.t("create.workspaceUnavailable") }; } let client; try { client = await TabLocalFiles.TabLocalWorkspace.createClient(pathToFileUrl(directoryPath), { writable: true }); if (!client.info.writable) { throw new Error(TabLocalFiles.t("create.directoryNotWritable")); } const entries = await client.listDirectory(TabLocalFiles.TabLocalWorkspace.ROOT_PATH); const latestNames = entries.filter((entry) => entry.kind === "file").map((entry) => entry.name); const fileName = TabLocalFiles.suggestedUniqueDocumentFileName(typeId, latestNames); await client.createFile(fileName); try { await client.writeTextFile(fileName, TabLocalFiles.initialDocumentText(typeId, fileName), { overwrite: true }); } catch (error) { await client.deleteEntry(fileName).catch(() => undefined); throw error; } const path = directoryPathForCreate(directoryPath) + fileName; return { ok: true, path, fileUrl: pathToFileUrl(path), fileName }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } finally { if (client) { void client.release().catch(() => undefined); } } } function fileNameFromCreateResult(result, fallback) { return result.path ? fileNameFromPath(result.path) : fallback; } function directoryPathForCreate(directoryPath) { return directoryPath.endsWith("/") ? directoryPath : `${directoryPath}/`; } function sidebarSnapshotWithRenamedFile(toolbar, previousPath, previousName, result) { if (!result.fileUrl) { return undefined; } const snapshot = readDirectorySnapshotFromElement(toolbar); if (!snapshot) { return undefined; } const nextPath = result.path || filePathFromFileUrl(result.fileUrl) || ""; const nextName = nextPath ? fileNameFromPath(nextPath) : previousName; const previousFileUrl = pathToFileUrl(previousPath); let matchedEntry = false; const entries = snapshot.entries.map((entry) => { if (!directoryEntryMatchesRenamedFile(entry, snapshot.rootPath, previousPath, previousFileUrl, previousName)) { return entry; } matchedEntry = true; return { ...entry, name: nextName, href: result.fileUrl || entry.href, hidden: nextName.startsWith(".") }; }); return matchedEntry ? { ...snapshot, entries } : snapshot; } function directoryEntryMatchesRenamedFile(entry, rootPath, previousPath, previousFileUrl, previousName) { if (entry.href === previousFileUrl || filePathFromFileUrl(entry.href) === previousPath) { return true; } return directoryPathForFile(previousPath) === directoryPathForCreate(rootPath) && entry.name === previousName; } function directoryPathForFile(path) { const slashIndex = path.lastIndexOf("/"); return slashIndex >= 0 ? path.slice(0, slashIndex + 1) : ""; } function filePathFromFileUrl(fileUrl) { try { const url = new URL(fileUrl); return url.protocol === "file:" ? decodeURIComponent(url.pathname) : undefined; } catch { return undefined; } } function sidebarSnapshotWithCreatedFile(button, result) { if (!result.fileUrl || !result.fileName) { return undefined; } const snapshot = readDirectorySnapshotFromElement(button); if (!snapshot) { return undefined; } const entries = snapshot.entries.some((entry) => entry.href === result.fileUrl || entry.name === result.fileName) ? snapshot.entries : [ ...snapshot.entries, { name: result.fileName, href: result.fileUrl, kind: "file", hidden: result.fileName.startsWith(".") } ]; return { ...snapshot, entries }; } function visibleDirectoryFileNames(button) { const snapshot = readDirectorySnapshotFromElement(button); return (snapshot?.entries || []) .filter((entry) => entry.kind === "file" && entry.name) .map((entry) => entry.name); } function readDirectorySnapshotFromElement(element) { const closestSnapshotElement = typeof element.closest === "function" ? element.closest("[data-tlf-directory-snapshot]") : null; const snapshotElement = closestSnapshotElement || document.querySelector("[data-tlf-directory-snapshot]"); const rawSnapshot = snapshotElement?.dataset.tlfDirectorySnapshot; if (!rawSnapshot) { return undefined; } try { const snapshot = JSON.parse(rawSnapshot); return snapshot.rootPath && Array.isArray(snapshot.entries) ? snapshot : undefined; } catch { return undefined; } } function setCreateDocumentMenuStatus(status, message, state = "neutral") { if (!status) { return; } status.textContent = message; status.dataset.tlfCreateStatus = state; } function bindThemeControls(themeButton) { themeBindingCleanup?.(); themeBindingCleanup = undefined; const followTheme = (state) => { if (themeButton) { syncThemeButton(themeButton, state.theme, state.mode); } TabLocalFiles.refreshFileFavicon(); }; const unsubscribe = TabLocalFiles.Theme.onChange(followTheme); const onThemeButtonClick = () => TabLocalFiles.Theme.toggleMode(); themeButton?.addEventListener("click", onThemeButtonClick); TabLocalFiles.Theme.start(); followTheme(TabLocalFiles.Theme.currentState()); themeBindingCleanup = () => { unsubscribe(); themeButton?.removeEventListener("click", onThemeButtonClick); TabLocalFiles.Theme.stop(); }; } function syncThemeButton(button, theme, mode) { button.textContent = theme === "light" ? "☀" : "☾"; const themeLabel = TabLocalFiles.t(theme === "dark" ? "theme.dark" : "theme.light"); const nextThemeLabel = TabLocalFiles.t(theme === "dark" ? "theme.light" : "theme.dark"); const fixedThemeLabel = TabLocalFiles.t(mode === "dark" ? "theme.dark" : "theme.light"); button.title = mode === "browser" ? TabLocalFiles.t("theme.followingBrowser", { theme: themeLabel, nextTheme: nextThemeLabel }) : TabLocalFiles.t("theme.fixed", { theme: fixedThemeLabel }); button.setAttribute("aria-label", button.title); } function readLocalStorage(key) { try { return window.localStorage.getItem(key); } catch { return null; } } function writeLocalStorage(key, value) { try { window.localStorage.setItem(key, value); } catch { return; } } function removeLocalStorage(key) { try { window.localStorage.removeItem(key); } catch { return; } } function renderOutline(items, maxLevel, options) { const visibleItems = items.filter((item) => item.level <= maxLevel); if (options?.collapsible) { const nodes = renderCollapsibleOutline(visibleItems, options.collapsedIds); return nodes ? `` : emptyOutlineHtml(); } const links = visibleItems .map((item) => ` ${escapeHtml(item.text)} `) .join(""); return links ? `` : emptyOutlineHtml(); } TabLocalFiles.renderOutline = renderOutline; function renderCollapsibleOutline(items, collapsedIds) { let index = 0; const renderNodes = (parentLevel) => { const parts = []; while (index < items.length && items[index].level > parentLevel) { const item = items[index]; index += 1; const childrenHtml = renderNodes(item.level); const collapsed = Boolean(childrenHtml) && Boolean(collapsedIds?.has(item.id)); const toggleHtml = childrenHtml ? `` : ""; parts.push(`
${childrenHtml ? `
${childrenHtml}
` : ""}
`); } return parts.join(""); }; return renderNodes(0); } function emptyOutlineHtml() { return `

${escapeHtml(TabLocalFiles.t("outline.empty"))}

`; } function escapeHtml(value) { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } TabLocalFiles.escapeHtml = escapeHtml; function escapeAttr(value) { return escapeHtml(value).replace(/`/g, "`"); } TabLocalFiles.escapeAttr = escapeAttr; })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ToolbarOverflow; (function (ToolbarOverflow) { const PINNED_SELECTOR = ".tlf-save-combo"; const SEGMENT_SELECTOR = ".tlf-segment, .tlf-mode-segment"; function fit(adapter) { adapter.reset(); adapter.setOverflowVisible(false); if (adapter.fits()) { return 0; } adapter.setOverflowVisible(true); let folded = 0; for (const unit of adapter.units()) { adapter.fold(unit); folded += 1; if (adapter.fits()) { break; } } return folded; } ToolbarOverflow.fit = fit; function foldUnits(toolbar, isShown) { const clusters = []; Array.from(toolbar.children).forEach((group) => { if (!group.classList.contains("tlf-context-group") || !isShown(group)) { return; } let cluster = []; Array.from(group.children).forEach((child) => { if (child.classList.contains("tlf-context-divider") && cluster.length) { clusters.push(cluster); cluster = []; } if (!child.matches(PINNED_SELECTOR)) { cluster.push(child); } }); clusters.push(cluster); }); return clusters.filter((cluster) => cluster.some((node) => !node.classList.contains("tlf-context-divider"))).reverse(); } ToolbarOverflow.foldUnits = foldUnits; const bound = new Set(); function bind(mountId) { document.querySelectorAll("[data-tlf-context-toolbar]").forEach((toolbar) => { const overflow = toolbar.querySelector(":scope > [data-tlf-context-overflow]"); const panel = overflow?.querySelector("[data-tlf-context-overflow-panel]"); if (!overflow || !panel || toolbar.dataset.tlfOverflowBound === "true") { return; } toolbar.dataset.tlfOverflowBound = "true"; const folded = []; let frame = 0; let observer; const run = () => { if (!toolbar.isConnected || toolbar.clientWidth === 0) { return; } fit({ units: () => foldUnits(toolbar, isShown), fits: () => rowFits(toolbar), fold: (nodes) => { folded.push(foldCluster(nodes, panel)); syncDividers(toolbar); }, reset: () => { while (folded.length) { unfoldCluster(folded.pop()); } panel.replaceChildren(); syncDividers(toolbar); }, setOverflowVisible: (visible) => { overflow.hidden = !visible; if (!visible) { overflow.open = false; } } }); observer?.takeRecords(); }; const schedule = () => { if (frame) { return; } frame = window.requestAnimationFrame(() => { frame = 0; run(); }); }; const resizeObserver = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(schedule); resizeObserver?.observe(toolbar); observer = typeof MutationObserver === "undefined" ? undefined : new MutationObserver(schedule); observer?.observe(toolbar, { childList: true, subtree: true, attributes: true, attributeFilter: ["hidden"] }); void document.fonts?.ready.then(schedule); const onToggle = () => { if (overflow.open) { panel.querySelectorAll(SEGMENT_SELECTOR).forEach((segment) => TabLocalFiles.positionSegmentPill(segment, false)); } }; overflow.addEventListener("toggle", onToggle); const entry = { schedule, run }; bound.add(entry); run(); TabLocalFiles.registerDocumentMountCleanup(mountId, () => { if (frame) { window.cancelAnimationFrame(frame); } resizeObserver?.disconnect(); observer?.disconnect(); overflow.removeEventListener("toggle", onToggle); bound.delete(entry); }); }); } ToolbarOverflow.bind = bind; function refit() { bound.forEach((entry) => entry.run()); } ToolbarOverflow.refit = refit; function isShown(group) { return window.getComputedStyle(group).display !== "none"; } function rowFits(toolbar) { const style = window.getComputedStyle(toolbar); const available = toolbar.clientWidth - (Number.parseFloat(style.paddingLeft) || 0) - (Number.parseFloat(style.paddingRight) || 0); let left = Infinity; let right = -Infinity; Array.from(toolbar.children).forEach((child) => { const rect = child.getBoundingClientRect(); if (rect.width > 0) { left = Math.min(left, rect.left); right = Math.max(right, rect.right); } }); return right <= left || right - left <= available + 0.5; } function foldCluster(nodes, panel) { const group = nodes[0].parentElement; const placeholder = document.createComment("tlf-context-overflow"); group.insertBefore(placeholder, nodes[0]); const context = group.dataset.tlfContext; const key = context || (group.classList.contains("tlf-context-common") ? "common" : "trailing"); let section = panel.querySelector(`:scope > [data-tlf-overflow-section="${key}"]`); if (!section) { section = document.createElement("div"); section.className = "tlf-context-overflow-section"; section.dataset.tlfOverflowSection = key; if (context) { section.dataset.tlfContext = context; } panel.prepend(section); } const wrapper = document.createElement("div"); wrapper.className = "tlf-context-overflow-cluster"; wrapper.append(...nodes); section.prepend(wrapper); return { placeholder, wrapper }; } function unfoldCluster(cluster) { const parent = cluster.placeholder.parentNode; if (parent) { Array.from(cluster.wrapper.childNodes).forEach((node) => parent.insertBefore(node, cluster.placeholder)); } cluster.placeholder.remove(); cluster.wrapper.remove(); } function syncDividers(toolbar) { const hasControls = (node, step) => { for (let current = node; current; current = step(current)) { if (current.classList.contains("tlf-context-group") && isShown(current) && current.children.length > 0) { return true; } } return false; }; toolbar.querySelectorAll(":scope > .tlf-context-divider").forEach((divider) => { divider.hidden = !(hasControls(divider.previousElementSibling, (node) => node.previousElementSibling) && hasControls(divider.nextElementSibling, (node) => node.nextElementSibling)); }); } })(ToolbarOverflow = TabLocalFiles.ToolbarOverflow || (TabLocalFiles.ToolbarOverflow = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var CdpSession; (function (CdpSession) { const DEFAULT_TIMEOUT_MS = 60000; function chromeApi() { return globalThis.chrome; } function isAvailable() { const api = chromeApi(); return Boolean(api?.tabCDPBridge && api.tabs?.getCurrent); } CdpSession.isAvailable = isAvailable; function currentTabId(api) { return new Promise((resolve, reject) => { const getCurrent = api.tabs?.getCurrent; if (!getCurrent) { reject(new Error("Unable to identify the containing tab")); return; } getCurrent((tab) => { const error = api.runtime?.lastError?.message; if (error) { reject(new Error(error)); return; } if (!Number.isInteger(tab?.id) || (tab?.id ?? -1) < 0) { reject(new Error("Unable to identify the containing tab")); return; } resolve(tab.id); }); }); } function connect(api, bridge, tabId) { return new Promise((resolve, reject) => { bridge.connect({ tabId }, (result) => { const error = api.runtime?.lastError?.message; if (error || !result?.connectionId) { reject(new Error(error || "Unable to attach to the tab")); return; } resolve(result.connectionId); }); }); } async function openForCurrentTab(options = {}) { const api = chromeApi(); const bridge = api?.tabCDPBridge; if (!api || !bridge) { throw new Error("The DevTools bridge is not available"); } const tabId = await currentTabId(api); const connectionId = await connect(api, bridge, tabId); const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; const pending = new Map(); let nextId = 1; let closed = false; const settle = (id) => { const entry = pending.get(id); if (entry) { window.clearTimeout(entry.timer); pending.delete(id); } return entry; }; const failAll = (error) => { for (const id of Array.from(pending.keys())) { settle(id)?.reject(error); } }; const onMessage = (id, message) => { if (id !== connectionId || typeof message.id !== "number") { return; } const entry = settle(message.id); if (!entry) { return; } if (message.error) { entry.reject(new Error(message.error.message || `CDP error ${message.error.code ?? ""}`.trim())); } else { entry.resolve(message.result ?? {}); } }; const release = (disconnectBridge) => { if (closed) { return; } closed = true; bridge.onMessage.removeListener(onMessage); bridge.onDisconnect.removeListener(onDisconnect); if (disconnectBridge) { try { bridge.disconnect(connectionId); } catch { } } }; const onDisconnect = (id, reason) => { if (id !== connectionId) { return; } release(false); failAll(new Error(reason || "The DevTools connection was closed")); }; bridge.onMessage.addListener(onMessage); bridge.onDisconnect.addListener(onDisconnect); return { send(method, params) { if (closed) { return Promise.reject(new Error("The DevTools session is closed")); } const id = nextId; nextId += 1; return new Promise((resolve, reject) => { const timer = window.setTimeout(() => { settle(id); reject(new Error(`${method} timed out after ${timeoutMs}ms`)); }, timeoutMs); pending.set(id, { resolve: resolve, reject, timer }); try { bridge.send(connectionId, params ? { id, method, params } : { id, method }); } catch (error) { settle(id); reject(error instanceof Error ? error : new Error(String(error))); } }); }, close() { release(true); failAll(new Error("The DevTools session is closed")); } }; } CdpSession.openForCurrentTab = openForCurrentTab; })(CdpSession = TabLocalFiles.CdpSession || (TabLocalFiles.CdpSession = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var PageExport; (function (PageExport) { const A4_WIDTH_IN = 8.27; const A4_HEIGHT_IN = 11.69; const PAGE_MARGIN_IN = 0.6; const DEFAULT_MAX_SLICE_PX = 8192; const DEFAULT_MAX_IMAGE_PX = 32767; const MAX_CAPTURE_SCALE = 2; const DEFAULT_LAYOUT_HEIGHT = 1200; PageExport.PDF_PRINT_PARAMS = { printBackground: true, preferCSSPageSize: false, generateDocumentOutline: true, paperWidth: A4_WIDTH_IN, paperHeight: A4_HEIGHT_IN, marginTop: PAGE_MARGIN_IN, marginBottom: PAGE_MARGIN_IN, marginLeft: PAGE_MARGIN_IN, marginRight: PAGE_MARGIN_IN }; async function printToPdf(session) { return TabLocalFiles.withPrintLayout(async () => { const result = await session.send("Page.printToPDF", { ...PageExport.PDF_PRINT_PARAMS }); return decodeBase64(result.data); }); } PageExport.printToPdf = printToPdf; function planCaptureSlices(input) { const maxSlicePx = input.maxSlicePx ?? DEFAULT_MAX_SLICE_PX; const maxImagePx = input.maxImagePx ?? DEFAULT_MAX_IMAGE_PX; const width = Math.ceil(input.width); const height = Math.ceil(input.height); if (width <= 0 || height <= 0) { return undefined; } let scale = Math.min(Math.max(input.deviceScaleFactor, 1), MAX_CAPTURE_SCALE); scale = Math.min(scale, maxImagePx / height, maxImagePx / width); if (scale < 1) { return undefined; } const sliceHeight = Math.max(1, Math.floor(maxSlicePx / scale)); const slices = []; for (let y = 0; y < height; y += sliceHeight) { slices.push({ y, height: Math.min(sliceHeight, height - y) }); } return { scale, sliceHeight, slices }; } PageExport.planCaptureSlices = planCaptureSlices; async function captureLongImage(session, options) { return TabLocalFiles.withPrintLayout(async () => { document.documentElement.dataset.tlfPrintTarget = "image"; await session.send("Emulation.setEmulatedMedia", { media: "print" }); try { await session.send("Emulation.setDeviceMetricsOverride", { width: Math.round(options.layoutWidth), height: Math.round(options.layoutHeight ?? DEFAULT_LAYOUT_HEIGHT), deviceScaleFactor: 0, mobile: false }); await TabLocalFiles.whenPrintLayoutSettled(); const size = options.measure(); const plan = planCaptureSlices({ width: size.width, height: size.height, deviceScaleFactor: options.deviceScaleFactor, maxSlicePx: options.maxSlicePx, maxImagePx: options.maxImagePx }); if (!plan) { return undefined; } const width = Math.ceil(size.width); const clipScale = plan.scale / Math.max(options.deviceScaleFactor, 1); const slices = []; for (const slice of plan.slices) { const shot = await session.send("Page.captureScreenshot", { format: "png", captureBeyondViewport: true, fromSurface: true, clip: { x: 0, y: slice.y, width, height: slice.height, scale: clipScale } }); slices.push({ y: slice.y, height: slice.height, data: shot.data }); } return { width, height: Math.ceil(size.height), scale: plan.scale, slices }; } finally { delete document.documentElement.dataset.tlfPrintTarget; await restoreEmulation(session); } }); } PageExport.captureLongImage = captureLongImage; async function restoreEmulation(session) { try { await session.send("Emulation.clearDeviceMetricsOverride"); } finally { await session.send("Emulation.setEmulatedMedia", { media: "" }); } } function decodeBase64(data) { const binary = atob(data); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes; } PageExport.decodeBase64 = decodeBase64; })(PageExport = TabLocalFiles.PageExport || (TabLocalFiles.PageExport = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var DocumentTitle; (function (DocumentTitle) { const HTML_ENTITIES = { amp: "&", apos: "'", gt: ">", lt: "<", nbsp: " ", quot: '"' }; let titled; let preferenceBound = false; function resolve(candidate, fallback) { if (!TabLocalFiles.Preferences.useDocumentTitle()) { return fallback; } return candidate?.trim() || fallback; } DocumentTitle.resolve = resolve; function apply(context, candidate, fallback) { if (!TabLocalFiles.isCurrentDocumentMount(context.mountId)) { return; } titled = { context, candidate, fallback }; bindPreference(); document.title = resolve(candidate, fallback); } DocumentTitle.apply = apply; function bindPreference() { if (preferenceBound) { return; } preferenceBound = true; TabLocalFiles.Preferences.onChange(() => { const current = titled; if (current && TabLocalFiles.isCurrentDocumentMount(current.context.mountId)) { document.title = resolve(current.candidate, current.fallback); } }); } function fromMarkdownSource(source) { return fromFrontmatter(source) || firstHeading(bodyAfterFrontmatter(source)) || fromLeadingHeading(source); } DocumentTitle.fromMarkdownSource = fromMarkdownSource; function fromLeadingHeading(source) { const lines = bodyAfterFrontmatter(source).split(/\r?\n/); const index = lines.findIndex((candidate) => candidate.trim()); if (index < 0) { return undefined; } const line = lines[index]; const atx = line.match(/^ {0,3}#{1,2}[ \t]+(.*?)[ \t]*$/); if (atx) { return plainText(atx[1].replace(/[ \t]+#+[ \t]*$/, "")) || undefined; } const underlined = /^ {0,3}(?:=+|-+)[ \t]*$/.test(lines[index + 1] || ""); return underlined ? plainText(line) || undefined : undefined; } DocumentTitle.fromLeadingHeading = fromLeadingHeading; function fromFrontmatter(source) { const lines = frontmatterLines(source); if (!lines) { return undefined; } for (const line of lines) { const match = line.match(/^title[ \t]*:[ \t]*(.*)$/i); if (!match) { continue; } const value = unquote(match[1].trim()); return !value || /^[|>][-+]?$/.test(value) ? undefined : value; } return undefined; } DocumentTitle.fromFrontmatter = fromFrontmatter; function frontmatterLines(source) { if (!/^---[ \t]*\r?\n/.test(source)) { return undefined; } const lines = source.split(/\r?\n/); for (let index = 1; index < lines.length; index += 1) { if (/^(?:---|\.\.\.)[ \t]*$/.test(lines[index])) { return lines.slice(1, index); } } return undefined; } function bodyAfterFrontmatter(source) { const lines = frontmatterLines(source); return lines ? source.split(/\r?\n/).slice(lines.length + 2).join("\n") : source; } function firstHeading(body) { const lines = body.split(/\r?\n/); let fence; for (let index = 0; index < lines.length; index += 1) { const line = lines[index]; const fenceMatch = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/); if (fence) { if (fenceMatch && fenceMatch[1][0] === fence.marker && fenceMatch[1].length >= fence.length) { fence = undefined; } continue; } if (fenceMatch) { fence = { marker: fenceMatch[1][0], length: fenceMatch[1].length }; continue; } const atx = line.match(/^ {0,3}#[ \t]+(.*?)[ \t]*$/); if (atx) { const text = plainText(atx[1].replace(/[ \t]+#+[ \t]*$/, "")); if (text) { return text; } continue; } if (/^ {0,3}#/.test(line)) { continue; } if (line.trim() && /^ {0,3}=+[ \t]*$/.test(lines[index + 1] || "")) { const text = plainText(line); if (text) { return text; } } } return undefined; } function plainText(text) { return text .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1") .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") .replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1") .replace(/(\*\*|__|~~|==|[*`])/g, "") .replace(/(? 1 && value.endsWith(quote) ? value.slice(1, -1) : value; } function fromHtmlSource(source) { const match = headMarkup(source).match(/]*>([\s\S]*?)<\/title\s*>/i); if (!match) { return undefined; } return decodeHtmlEntities(match[1]).replace(/\s+/g, " ").trim() || undefined; } DocumentTitle.fromHtmlSource = fromHtmlSource; function headMarkup(source) { const headEnd = source.search(/<\/head\s*>/i); if (headEnd >= 0) { return source.slice(0, headEnd); } const bodyStart = source.search(/= 0 ? source.slice(0, bodyStart) : source; } function decodeHtmlEntities(value) { return value.replace(/&(#[Xx]?[0-9A-Fa-f]+|[A-Za-z]+);/g, (entity, body) => { if (body[0] !== "#") { return HTML_ENTITIES[body.toLowerCase()] ?? entity; } const hex = body[1] === "x" || body[1] === "X"; const code = parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10); return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : entity; }); } })(DocumentTitle = TabLocalFiles.DocumentTitle || (TabLocalFiles.DocumentTitle = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var LargeFile; (function (LargeFile) { const MB = 1024 * 1024; LargeFile.TEXT_FULL_BYTES = 8 * MB; LargeFile.SUBTITLE_FULL_BYTES = 2 * MB; LargeFile.TEXT_PREVIEW_BYTES = 2 * MB; LargeFile.MARKDOWN_FULL_BYTES = 2 * MB; LargeFile.MARKDOWN_INLINE_EDITOR_BYTES = 1 * MB; LargeFile.HTML_FULL_BYTES = 10 * MB; LargeFile.PDF_FULL_BYTES = 50 * MB; LargeFile.OFFICE_FULL_BYTES = 50 * MB; LargeFile.SVG_FULL_BYTES = 20 * MB; LargeFile.ARCHIVE_FULL_BYTES = 200 * MB; LargeFile.DIRECTORY_ENTRY_LIMIT = 2000; function previewRefreshDelayMs(sourceLength) { if (sourceLength >= 1 * MB) { return 900; } if (sourceLength >= 256 * 1024) { return 400; } return 120; } LargeFile.previewRefreshDelayMs = previewRefreshDelayMs; function decisionFor(definitionId, stat) { if (definitionId === "subtitle") { if (!stat || !Number.isFinite(stat.size) || stat.size < 0) { return { mode: "preview", maxBytes: LargeFile.TEXT_PREVIEW_BYTES }; } return stat.size > LargeFile.SUBTITLE_FULL_BYTES ? { mode: "preview", maxBytes: LargeFile.TEXT_PREVIEW_BYTES } : { mode: "full" }; } if (!stat || !Number.isFinite(stat.size) || stat.size <= 0) { return { mode: "full" }; } if (definitionId === "markdown") { return stat.size > LargeFile.MARKDOWN_FULL_BYTES ? { mode: "preview", maxBytes: LargeFile.TEXT_PREVIEW_BYTES } : { mode: "full" }; } if (definitionId === "code-text") { return stat.size > LargeFile.TEXT_FULL_BYTES ? { mode: "preview", maxBytes: LargeFile.TEXT_PREVIEW_BYTES } : { mode: "full" }; } if (definitionId === "html") { return stat.size > LargeFile.HTML_FULL_BYTES ? { mode: "preview", maxBytes: LargeFile.TEXT_PREVIEW_BYTES } : { mode: "full" }; } if (definitionId === "pdf") { return stat.size > LargeFile.PDF_FULL_BYTES ? { mode: "blocked" } : { mode: "full" }; } if (definitionId === "office") { return stat.size > LargeFile.OFFICE_FULL_BYTES ? { mode: "blocked" } : { mode: "full" }; } if (definitionId === "svg") { return stat.size > LargeFile.SVG_FULL_BYTES ? { mode: "blocked" } : { mode: "full" }; } return { mode: "full" }; } LargeFile.decisionFor = decisionFor; function isPreview(context) { return context.largeFileDecision?.mode === "preview"; } LargeFile.isPreview = isPreview; function markdownForcesSourceVariant(context) { const size = context.fileStat?.size; return typeof size === "number" && Number.isFinite(size) && size > LargeFile.MARKDOWN_INLINE_EDITOR_BYTES; } LargeFile.markdownForcesSourceVariant = markdownForcesSourceVariant; function isBlocked(context) { return context.largeFileDecision?.mode === "blocked"; } LargeFile.isBlocked = isBlocked; function toolbarStatus(context, kind) { if (isBlocked(context)) { return [TabLocalFiles.toolbarStatus("tlf-large-file-meta", TabLocalFiles.t("large.blockedMeta", { kind, size: fileSizeLabel(context) }), { tone: "neutral" })]; } const previewSize = TabLocalFiles.formatBytes(context.largeFileDecision?.maxBytes || LargeFile.TEXT_PREVIEW_BYTES); return [ TabLocalFiles.toolbarStatus("tlf-large-file-meta", TabLocalFiles.t("large.previewMeta", { kind, size: fileSizeLabel(context) }), { tone: "neutral" }), TabLocalFiles.toolbarStatus("tlf-large-file-detail", TabLocalFiles.t("large.previewDescription", { size: fileSizeLabel(context), previewSize }), { tone: "neutral" }) ]; } LargeFile.toolbarStatus = toolbarStatus; function renderTextPreview(context, kind, source) { const chunkBytes = context.largeFileDecision?.maxBytes || LargeFile.TEXT_PREVIEW_BYTES; const initialEnd = source ? Math.min(chunkBytes, context.fileStat?.size || chunkBytes) : 0; const totalBytes = context.fileStat?.size || initialEnd; const body = source ? TabLocalFiles.escapeHtml(source) : ""; return `
${body}
${TabLocalFiles.escapeHtml(TabLocalFiles.t("large.virtualStatus", { start: TabLocalFiles.formatBytes(0), end: TabLocalFiles.formatBytes(initialEnd), total: TabLocalFiles.formatBytes(totalBytes) }))}
`; } LargeFile.renderTextPreview = renderTextPreview; function renderBlocked(context, kind) { return `

${TabLocalFiles.escapeHtml(TabLocalFiles.t("large.blockedTitle"))}

${TabLocalFiles.escapeHtml(TabLocalFiles.t("large.blockedDescription", { kind, size: fileSizeLabel(context) }))}

`; } LargeFile.renderBlocked = renderBlocked; function fileSizeLabel(context) { return context.fileStat ? TabLocalFiles.formatBytes(context.fileStat.size) : TabLocalFiles.t("common.unknown"); } LargeFile.fileSizeLabel = fileSizeLabel; })(LargeFile = TabLocalFiles.LargeFile || (TabLocalFiles.LargeFile = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var LargeTextViewer; (function (LargeTextViewer) { const VIEWER_SELECTOR = "[data-tlf-large-text-viewer]"; const TEXT_SELECTOR = "[data-tlf-large-text-scroll]"; const CONTENT_SELECTOR = "[data-tlf-large-text-content]"; const CHUNK_SELECTOR = "[data-tlf-large-text-chunk]"; const TOP_SPACER_SELECTOR = "[data-tlf-large-text-top-spacer]"; const STATUS_SELECTOR = "[data-tlf-large-text-status]"; const DEFAULT_MAX_RENDERED_CHUNKS = 4; const SCROLL_THRESHOLD_PX = 480; function bind(context) { const viewer = document.querySelector(VIEWER_SELECTOR); if (!viewer || viewer.dataset.tlfLargeTextBound === "true") { return; } const textBox = viewer.querySelector(TEXT_SELECTOR); const content = viewer.querySelector(CONTENT_SELECTOR); const topSpacer = viewer.querySelector(TOP_SPACER_SELECTOR); const status = viewer.querySelector(STATUS_SELECTOR); if (!textBox || !content || !topSpacer || !status) { return; } const textElement = textBox; const contentElement = content; const topSpacerElement = topSpacer; const statusElement = status; viewer.dataset.tlfLargeTextBound = "true"; const totalBytes = positiveNumber(viewer.dataset.tlfTotalBytes) || context.fileStat?.size || 0; const chunkBytes = positiveNumber(viewer.dataset.tlfChunkBytes) || TabLocalFiles.LargeFile.TEXT_PREVIEW_BYTES; const maxRenderedChunks = positiveNumber(viewer.dataset.tlfMaxRenderedChunks) || DEFAULT_MAX_RENDERED_CHUNKS; const initialEnd = Math.min(positiveNumber(viewer.dataset.tlfInitialEnd) || 0, totalBytes || Number.MAX_SAFE_INTEGER); const initialChunk = contentElement.querySelector(CHUNK_SELECTOR); const chunks = initialChunk && initialEnd > 0 ? [createRenderedChunk(0, initialEnd, initialChunk)] : []; let topSpacerHeight = 0; let loading; let disposed = false; let resizeObserver; const selectRenderedText = (event) => { if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "a" || TabLocalFiles.TextEditor.isTextInputTarget(event.target)) { return; } const selection = window.getSelection(); if (!selection) { return; } event.preventDefault(); const range = document.createRange(); range.selectNodeContents(contentElement); selection.removeAllRanges(); selection.addRange(range); }; document.addEventListener("keydown", selectRenderedText); const cleanup = () => { disposed = true; window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", syncStatusFrame); document.removeEventListener("keydown", selectRenderedText); resizeObserver?.disconnect(); }; if (context.mountId !== undefined) { TabLocalFiles.registerDocumentMountCleanup(context.mountId, cleanup); } const measureAndUpdate = () => { syncStatusFrame(); updateStatus(); if (shouldLoadAfter()) { void loadAfter(); } }; const onScroll = () => { if (loading || disposed) { return; } if (shouldLoadAfter()) { void loadAfter(); return; } if (shouldLoadBefore()) { void loadBefore(); } }; async function loadAfter() { const highestOffset = currentHighestOffset(); if (!totalBytes || highestOffset >= totalBytes) { updateStatus(); return; } await loadChunk(highestOffset, Math.min(chunkBytes, totalBytes - highestOffset), "after"); } async function loadBefore() { const lowestOffset = currentLowestOffset(); if (lowestOffset <= 0) { updateStatus(); return; } const offset = Math.max(0, lowestOffset - chunkBytes); await loadChunk(offset, lowestOffset - offset, "before"); } async function loadChunk(offset, size, direction) { if (loading || size <= 0 || disposed) { return; } loading = direction; statusElement.textContent = TabLocalFiles.t("large.virtualLoading"); try { const chunk = await TabLocalFiles.readCurrentFileChunkAsText(context.sourceUrl, offset, size); if (!chunk || disposed) { statusElement.textContent = TabLocalFiles.t("large.virtualError"); return; } const element = renderChunk(chunk.text, chunk.offset, chunk.endOffset); if (direction === "before") { const previousTopHeight = topSpacerHeight; contentElement.prepend(element); chunks.unshift(createRenderedChunk(chunk.offset, chunk.endOffset, element)); await nextFrame(); setTopSpacer(Math.max(0, previousTopHeight - measuredHeight(chunks[0]))); } else { contentElement.append(element); chunks.push(createRenderedChunk(chunk.offset, chunk.endOffset, element)); await nextFrame(); } trimChunks(direction, maxRenderedChunks); updateStatus(); } catch { statusElement.textContent = TabLocalFiles.t("large.virtualError"); } finally { loading = undefined; if (!disposed && (direction === "after" ? shouldLoadAfter() : shouldLoadBefore())) { window.setTimeout(onScroll, 0); } } } function trimChunks(direction, maxChunks) { while (chunks.length > maxChunks) { const removed = direction === "before" ? chunks.pop() : chunks.shift(); if (!removed) { return; } const height = measuredHeight(removed); removed.element.remove(); if (direction === "after") { setTopSpacer(topSpacerHeight + height); } } } function updateStatus() { const start = currentLowestOffset(); const end = currentHighestOffset(); statusElement.textContent = TabLocalFiles.t("large.virtualStatus", { start: TabLocalFiles.formatBytes(start), end: TabLocalFiles.formatBytes(end), total: TabLocalFiles.formatBytes(totalBytes) }); } function syncStatusFrame() { const rect = textElement.getBoundingClientRect(); statusElement.style.setProperty("--tlf-large-file-status-left", `${Math.max(12, rect.left)}px`); statusElement.style.setProperty("--tlf-large-file-status-width", `${Math.max(160, rect.width)}px`); } function currentLowestOffset() { return chunks[0]?.offset || 0; } function currentHighestOffset() { return chunks[chunks.length - 1]?.endOffset || 0; } function setTopSpacer(height) { topSpacerHeight = Math.max(0, height); topSpacerElement.style.height = `${Math.round(topSpacerHeight)}px`; } function shouldLoadAfter() { if (!totalBytes || currentHighestOffset() >= totalBytes) { return false; } return textElement.getBoundingClientRect().bottom <= viewportHeight() + SCROLL_THRESHOLD_PX; } function shouldLoadBefore() { if (currentLowestOffset() <= 0) { return false; } return topSpacerElement.getBoundingClientRect().bottom >= -SCROLL_THRESHOLD_PX; } if (typeof ResizeObserver !== "undefined") { resizeObserver = new ResizeObserver(syncStatusFrame); resizeObserver.observe(textElement); } window.addEventListener("resize", syncStatusFrame, { passive: true }); window.addEventListener("scroll", onScroll, { passive: true }); void nextFrame().then(measureAndUpdate); } LargeTextViewer.bind = bind; function createRenderedChunk(offset, endOffset, element) { return { offset, endOffset, element }; } function renderChunk(text, offset, endOffset) { const element = document.createElement("span"); element.className = "tlf-large-file-chunk"; element.dataset.tlfLargeTextChunk = "true"; element.dataset.tlfOffset = String(offset); element.dataset.tlfEndOffset = String(endOffset); element.textContent = text; return element; } function measuredHeight(chunk) { return Math.max(1, chunk.element.getBoundingClientRect().height || chunk.element.offsetHeight || 1); } function positiveNumber(value) { const numberValue = Number(value); return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : undefined; } function nextFrame() { return new Promise((resolve) => { const requestFrame = window.requestAnimationFrame || ((callback) => window.setTimeout(callback, 0)); requestFrame(() => resolve()); }); } function viewportHeight() { return window.innerHeight || document.documentElement.clientHeight || 0; } })(LargeTextViewer = TabLocalFiles.LargeTextViewer || (TabLocalFiles.LargeTextViewer = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var VirtualTable; (function (VirtualTable) { VirtualTable.ROW_HEIGHT = 36; const OVERSCAN_ROWS = 12; function computeWindow(scrollTop, viewportHeight, rowCount, rowHeight = VirtualTable.ROW_HEIGHT, overscan = OVERSCAN_ROWS) { if (rowCount <= 0 || rowHeight <= 0) { return { start: 0, end: 0 }; } const first = Math.floor(Math.max(0, scrollTop) / rowHeight); const visible = Math.ceil(Math.max(0, viewportHeight) / rowHeight) + 1; const start = Math.min(rowCount, Math.max(0, first - overscan)); const end = Math.min(rowCount, first + visible + overscan); return { start, end: Math.max(start, end) }; } VirtualTable.computeWindow = computeWindow; function renderWindowHtml(window, rowCount, columnCount, renderRow, rowHeight = VirtualTable.ROW_HEIGHT) { const spacer = (rows) => rows > 0 ? `` : ""; let html = spacer(window.start); for (let index = window.start; index < window.end; index += 1) { html += renderRow(index); } return html + spacer(rowCount - window.end); } VirtualTable.renderWindowHtml = renderWindowHtml; function bind(options) { const { wrap, body, rowCount, columnCount, renderRow } = options; const rowHeight = options.rowHeight || VirtualTable.ROW_HEIGHT; let rendered; let frame = 0; let disposed = false; let printing = false; const headHeight = () => wrap.querySelector("thead")?.getBoundingClientRect().height || rowHeight; const render = () => { frame = 0; if (disposed) { return; } const next = printing ? { start: 0, end: rowCount } : computeWindow(wrap.scrollTop - headHeight(), wrap.clientHeight, rowCount, rowHeight); if (rendered && rendered.start === next.start && rendered.end === next.end) { return; } rendered = next; body.innerHTML = renderWindowHtml(next, rowCount, columnCount, renderRow, rowHeight); }; const schedule = () => { if (!frame && !disposed) { frame = window.requestAnimationFrame(render); } }; const beforePrint = () => { printing = true; render(); }; const afterPrint = () => { printing = false; render(); }; window.addEventListener("beforeprint", beforePrint); window.addEventListener("afterprint", afterPrint); wrap.addEventListener("scroll", schedule, { passive: true }); const resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(schedule) : undefined; resizeObserver?.observe(wrap); render(); return () => { disposed = true; if (frame) { window.cancelAnimationFrame(frame); } window.removeEventListener("beforeprint", beforePrint); window.removeEventListener("afterprint", afterPrint); wrap.removeEventListener("scroll", schedule); resizeObserver?.disconnect(); }; } VirtualTable.bind = bind; })(VirtualTable = TabLocalFiles.VirtualTable || (TabLocalFiles.VirtualTable = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var EditSession; (function (EditSession) { const DRAFT_STORAGE_PREFIX = "tab-local-files:draft:"; let activeSession; let nextToken = 0; let beforeUnloadBound = false; function register(registration) { ensureBeforeUnloadGuard(); const session = { ...registration, dirty: false, token: ++nextToken }; activeSession = session; return { setDirty: (dirty) => { if (activeSession?.token === session.token) { activeSession.dirty = dirty; } }, dispose: () => { if (activeSession?.token === session.token) { activeSession = undefined; } } }; } EditSession.register = register; async function requestNavigation() { const session = activeSession; if (!session?.dirty) { return true; } if (session.autoSave && session.flush) { return session.flush(); } const choice = await chooseBeforeNavigation(session.fileName); if (choice === "cancel") { return false; } if (choice === "save") { return session.save(); } session.discard(); session.dirty = false; return true; } EditSession.requestNavigation = requestNavigation; function restoreDraft(sourcePath, baseline) { try { const raw = window.localStorage.getItem(draftKey(sourcePath)); if (!raw) { return undefined; } const draft = JSON.parse(raw); return typeof draft.text === "string" && draft.text !== baseline ? draft.text : undefined; } catch { return undefined; } } EditSession.restoreDraft = restoreDraft; const quotaWarnedPaths = new Set(); const failureLoggedPaths = new Set(); function persistDraft(sourcePath, text) { try { const draft = { text, updatedAt: Date.now() }; window.localStorage.setItem(draftKey(sourcePath), JSON.stringify(draft)); quotaWarnedPaths.delete(sourcePath); failureLoggedPaths.delete(sourcePath); return "stored"; } catch (error) { const outOfSpace = isQuotaExceeded(error); if (!failureLoggedPaths.has(sourcePath)) { failureLoggedPaths.add(sourcePath); console.warn("[Previewer] Recovery draft was not stored.", error); } if (outOfSpace && !quotaWarnedPaths.has(sourcePath)) { quotaWarnedPaths.add(sourcePath); TabLocalFiles.showToast(TabLocalFiles.t("editor.draftOutOfSpace"), "error"); } return outOfSpace ? "quota-exceeded" : "unavailable"; } } EditSession.persistDraft = persistDraft; function isQuotaExceeded(error) { if (!error || typeof error !== "object") { return false; } const { name, code } = error; return (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED" || code === 22 || code === 1014); } function clearDraft(sourcePath) { try { window.localStorage.removeItem(draftKey(sourcePath)); } catch { } } EditSession.clearDraft = clearDraft; function confirmExtensionChange(fileName, nextExtension) { const extensionLabel = nextExtension || TabLocalFiles.t("rename.extensionNone"); return showDecisionDialog({ title: TabLocalFiles.t("rename.extensionChangeTitle"), message: TabLocalFiles.t("rename.extensionChangeDescription", { fileName, extension: extensionLabel }), actions: [ { value: true, label: TabLocalFiles.t("rename.extensionChangeConfirm"), primary: true }, { value: false, label: TabLocalFiles.t("common.cancel") } ], cancelValue: false, fallback: () => window.confirm(TabLocalFiles.t("rename.extensionChangeDescription", { fileName, extension: extensionLabel })) }); } EditSession.confirmExtensionChange = confirmExtensionChange; function confirmRevert(fileName) { return showDecisionDialog({ title: TabLocalFiles.t("editor.revertTitle"), message: TabLocalFiles.t("editor.revertDescription", { fileName }), actions: [ { value: true, label: TabLocalFiles.t("editor.revertConfirm"), primary: true }, { value: false, label: TabLocalFiles.t("common.cancel") } ], cancelValue: false, fallback: () => window.confirm(TabLocalFiles.t("editor.revertDescription", { fileName })) }); } EditSession.confirmRevert = confirmRevert; function confirmPdfAnnotationOverwrite(fileName) { const message = TabLocalFiles.t("pdf.saveAnnotationsConfirmDescription", { fileName }); return showDecisionDialog({ title: TabLocalFiles.t("pdf.saveAnnotationsConfirmTitle"), message, actions: [ { value: true, label: TabLocalFiles.t("pdf.saveAnnotationsConfirm"), primary: true }, { value: false, label: TabLocalFiles.t("common.cancel") } ], cancelValue: false, fallback: () => window.confirm(message) }); } EditSession.confirmPdfAnnotationOverwrite = confirmPdfAnnotationOverwrite; function confirmMoveToTrash(name, isDirectory, place) { const message = TabLocalFiles.t(isDirectory ? "trash.confirmFolder" : "trash.confirmFile", { name, place }); return showDecisionDialog({ title: TabLocalFiles.t("trash.confirmTitle", { place }), message, actions: [ { value: true, label: TabLocalFiles.t("trash.action", { place }), primary: true }, { value: false, label: TabLocalFiles.t("common.cancel") } ], cancelValue: false, fallback: () => window.confirm(message) }); } EditSession.confirmMoveToTrash = confirmMoveToTrash; function abandonActive() { const session = activeSession; if (!session) { return; } session.dirty = false; clearDraft(session.sourcePath); } EditSession.abandonActive = abandonActive; function confirmOverwrite(fileName) { return showDecisionDialog({ title: TabLocalFiles.t("editor.conflictTitle"), message: TabLocalFiles.t("editor.conflictDescription", { fileName }), actions: [ { value: true, label: TabLocalFiles.t("editor.overwrite"), primary: true }, { value: false, label: TabLocalFiles.t("common.cancel") } ], cancelValue: false, fallback: () => window.confirm(TabLocalFiles.t("editor.conflictDescription", { fileName })) }); } EditSession.confirmOverwrite = confirmOverwrite; function ensureBeforeUnloadGuard() { if (beforeUnloadBound) { return; } beforeUnloadBound = true; window.addEventListener("beforeunload", (event) => { if (!activeSession?.dirty) { return; } event.preventDefault(); event.returnValue = TabLocalFiles.t("editor.beforeUnload"); }); } function draftKey(sourcePath) { return `${DRAFT_STORAGE_PREFIX}${encodeURIComponent(sourcePath)}`; } function chooseBeforeNavigation(fileName) { return showDecisionDialog({ title: TabLocalFiles.t("editor.unsavedTitle"), message: TabLocalFiles.t("editor.unsavedDescription", { fileName }), actions: [ { value: "save", label: TabLocalFiles.t("common.save"), primary: true }, { value: "discard", label: TabLocalFiles.t("editor.discard") }, { value: "cancel", label: TabLocalFiles.t("common.cancel") } ], cancelValue: "cancel", fallback: () => { if (window.confirm(TabLocalFiles.t("editor.saveBeforeLeave", { fileName }))) { return "save"; } return window.confirm(TabLocalFiles.t("editor.discardBeforeLeave", { fileName })) ? "discard" : "cancel"; } }); } function showDecisionDialog(options) { if (typeof HTMLDialogElement === "undefined" || !document.body) { return Promise.resolve(options.fallback()); } return new Promise((resolve) => { const dialog = document.createElement("dialog"); dialog.className = "tlf-edit-dialog"; dialog.setAttribute("aria-labelledby", "tlf-edit-dialog-title"); const title = document.createElement("h2"); title.id = "tlf-edit-dialog-title"; title.className = "tlf-edit-dialog-title"; title.textContent = options.title; const message = document.createElement("p"); message.className = "tlf-edit-dialog-message"; message.textContent = options.message; const actions = document.createElement("div"); actions.className = "tlf-edit-dialog-actions"; let settled = false; const settle = (value) => { if (settled) { return; } settled = true; dialog.close(); dialog.remove(); resolve(value); }; options.actions.forEach((action) => { const button = document.createElement("button"); button.type = "button"; button.className = `tlf-editor-button${action.primary ? " tlf-editor-button-primary" : ""}`; button.textContent = action.label; button.addEventListener("click", () => settle(action.value)); actions.append(button); }); dialog.addEventListener("cancel", (event) => { event.preventDefault(); settle(options.cancelValue); }); dialog.append(title, message, actions); document.body.append(dialog); try { dialog.showModal(); } catch { dialog.remove(); resolve(options.fallback()); } }); } })(EditSession = TabLocalFiles.EditSession || (TabLocalFiles.EditSession = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var TextEditor; (function (TextEditor) { const EDITOR_MODE_CHANGE_EVENT = "tlf:editor-mode-change"; const EDITOR_STATE_CHANGE_EVENT = "tlf:editor-state-change"; const INDENT = " "; const AUTO_SAVE_DELAY_MS = 1500; function applyUndoableEdit(textarea, edit) { if (!applyEditThroughEditingCommand(textarea, edit)) { textarea.setRangeText(edit.replacement, edit.rangeStart, edit.rangeEnd, "select"); textarea.selectionStart = edit.selectionStart; textarea.selectionEnd = edit.selectionEnd; textarea.dispatchEvent(new Event("input", { bubbles: true })); return; } textarea.selectionStart = edit.selectionStart; textarea.selectionEnd = edit.selectionEnd; } TextEditor.applyUndoableEdit = applyUndoableEdit; function applyEditThroughEditingCommand(textarea, edit) { if (typeof document === "undefined" || typeof document.execCommand !== "function") { return false; } try { textarea.focus({ preventScroll: true }); if (document.activeElement !== textarea) { return false; } textarea.selectionStart = edit.rangeStart; textarea.selectionEnd = edit.rangeEnd; if (!edit.replacement && edit.rangeStart === edit.rangeEnd) { return true; } return edit.replacement ? document.execCommand("insertText", false, edit.replacement) : document.execCommand("delete"); } catch { return false; } } let activeEditingSurface; function setActiveEditingSurface(delegate) { activeEditingSurface = delegate; } TextEditor.setActiveEditingSurface = setActiveEditingSurface; function getActiveEditingSurface() { return activeEditingSurface; } TextEditor.getActiveEditingSurface = getActiveEditingSurface; function editingSurfaceClaimsInput() { return Boolean(activeEditingSurface && (activeEditingSurface.exclusive || activeEditingSurface.hasFocus())); } TextEditor.editingSurfaceClaimsInput = editingSurfaceClaimsInput; function externalTextDecision(diskText, editorValue, baselineValue) { if (diskText === editorValue) { return diskText === baselineValue ? "noop" : "adopt-clean"; } return editorValue === baselineValue ? "apply" : "keep-user"; } TextEditor.externalTextDecision = externalTextDecision; const autoSaveBarriers = new Set(); function registerAutoSaveBarrier(barrier) { autoSaveBarriers.add(barrier); return () => { autoSaveBarriers.delete(barrier); }; } TextEditor.registerAutoSaveBarrier = registerAutoSaveBarrier; function runAutoSaveBarriers() { autoSaveBarriers.forEach((barrier) => { try { barrier(); } catch (error) { console.warn("[Previewer] Auto-save barrier failed", error); } }); } function render(options) { return `
${TabLocalFiles.renderSplitHandle("editor")}
${options.previewHtml}
`; } TextEditor.render = render; function bind(options) { const root = document.getElementById(options.rootId); if (!root) { return; } const preview = root.querySelector("[data-tlf-editor-preview]"); const textarea = root.querySelector("[data-tlf-editor-textarea]"); const textareaLabel = root.querySelector("[data-tlf-editor-label]"); const lineNumbers = root.querySelector("[data-tlf-editor-line-numbers]"); const highlightElement = root.querySelector("[data-tlf-editor-highlight]"); if (!preview || !textarea || !textareaLabel) { return; } let originalText = options.initialText; let openedText = options.initialText; let editing = false; let previewTimer; let draftTimer; let editState = "clean"; let sessionHandle; let autoSaveController; let allowTabExit = false; const autoSaveEnabled = options.autoSave === true && options.saveMode === "original" && Boolean(options.sourcePath); root.dataset.tlfAutoSave = String(autoSaveEnabled); const recoveredDraft = options.sourcePath ? TabLocalFiles.EditSession.restoreDraft(options.sourcePath, originalText) : undefined; if (recoveredDraft !== undefined) { textarea.value = recoveredDraft; } const changes = TabLocalFiles.ExternalChanges.createLedger(); const chatDocument = TabLocalFiles.ChatEditDocument.create(textarea.value); let composing = false; let chatSaveInProgress = false; let strictChatSaves = false; let lastChatEdit; const chatActions = root.querySelector("[data-tlf-chat-actions]"); const quoteButton = root.querySelector("[data-tlf-chat-quote]"); quoteButton?.addEventListener("click", () => document.dispatchEvent(new CustomEvent("tlf:quote-document", { detail: options.rootId }))); const undoChatButton = root.querySelector("[data-tlf-chat-undo]"); undoChatButton?.addEventListener("click", () => { if (!lastChatEdit) return; void root.__tlfEditor?.undoChat(lastChatEdit.contextId, lastChatEdit.operationId).then((result) => { if (!result.success) TabLocalFiles.showToast(TabLocalFiles.t("common.chatEditConflict"), "error"); }); }); if (chatActions) chatActions.hidden = !(options.sourcePath && options.saveMode === "original"); root.addEventListener("compositionstart", () => { composing = true; }); root.addEventListener("compositionend", () => { composing = false; }); let changesText = textarea.value; let externalTextPending = false; const sourceHighlight = options.highlightLanguage && highlightElement ? attachSourceHighlight({ textarea, highlight: highlightElement, language: options.highlightLanguage, mountId: options.mountId }) : undefined; const renderPreview = (value) => { preview.innerHTML = options.renderPreview(value); options.afterPreviewRender?.(value); TabLocalFiles.ChangeMarks.refresh(options.rootId); }; const setEditState = (state, message) => { const previousState = editState; editState = state; root.dataset.tlfEditState = state; if (state === "error" && previousState !== "error" && message) { TabLocalFiles.showToast(message, "error"); } root.dispatchEvent(new CustomEvent(EDITOR_STATE_CHANGE_EVENT, { bubbles: true, detail: { rootId: options.rootId, state, dirty: root.dataset.tlfDirty === "true" } })); }; const syncLineNumbers = () => { if (!lineNumbers) { return; } const count = Math.max(1, TabLocalFiles.lineCount(textarea.value)); lineNumbers.textContent = Array.from({ length: count }, (_, index) => String(index + 1)).join("\n"); }; const canRevert = () => Boolean(options.sourcePath) && textarea.value !== openedText; const syncDirtyStatus = (nextCleanState = "clean") => { chatDocument.observe(textarea.value); if (textarea.value !== changesText) { const previous = changesText; changesText = textarea.value; if (externalTextPending) { changes.applyExternal(previous, changesText); } else { changes.applyLocal(previous, changesText); } } externalTextPending = false; const dirty = textarea.value !== originalText; root.dataset.tlfDirty = String(dirty); root.dataset.tlfRevertable = String(canRevert()); sessionHandle?.setDirty(dirty); setEditState(options.saveMode === "original" && !options.sourcePath ? "readonly" : dirty ? "dirty" : nextCleanState, dirty && autoSaveEnabled ? TabLocalFiles.t("editor.autoSavePending") : undefined); syncLineNumbers(); if (dirty && options.sourcePath) { if (draftTimer !== undefined) { window.clearTimeout(draftTimer); } draftTimer = window.setTimeout(() => { TabLocalFiles.EditSession.persistDraft(options.sourcePath || "", textarea.value); draftTimer = undefined; }, 240); } else if (options.sourcePath) { if (draftTimer !== undefined) { window.clearTimeout(draftTimer); draftTimer = undefined; } TabLocalFiles.EditSession.clearDraft(options.sourcePath); } }; const schedulePreview = () => { if (previewTimer) { window.clearTimeout(previewTimer); } previewTimer = window.setTimeout(() => { renderPreview(textarea.value); }, TabLocalFiles.LargeFile.previewRefreshDelayMs(textarea.value.length)); }; const setEditing = (nextEditing) => { editing = nextEditing; TabLocalFiles.setPanelMode(editing ? "edit" : "preview"); preview.hidden = false; textareaLabel.hidden = !editing; root.dispatchEvent(new CustomEvent(EDITOR_MODE_CHANGE_EVENT, { bubbles: true, detail: { rootId: options.rootId, mode: editing ? "edit" : "preview" } })); if (!editing) { renderPreview(textarea.value); } else { renderPreview(textarea.value); textarea.focus(); } }; textarea.addEventListener("input", () => { syncDirtyStatus(); schedulePreview(); if (root.dataset.tlfDirty === "true") { autoSaveController?.schedule(); } else { autoSaveController?.cancel(); } }); textarea.addEventListener("scroll", () => { if (lineNumbers) { lineNumbers.scrollTop = textarea.scrollTop; } }); const requestMode = (nextEditing) => { if (nextEditing === editing) { return; } if (!nextEditing && autoSaveController) { void autoSaveController.flush().finally(() => setEditing(false)); return; } setEditing(nextEditing); }; const copyAllText = async () => { try { await copyTextToClipboard(textarea.value, root); TabLocalFiles.showToast(TabLocalFiles.t("common.copiedToClipboard"), "success"); } catch { TabLocalFiles.showToast(TabLocalFiles.t("common.copyFailed"), "error"); } }; const saveResultRecorder = TabLocalFiles.createCurrentFileSaveResultRecorder(); const saveAs = async () => { try { const mode = options.onSaveAs ? await options.onSaveAs(textarea.value) : await TabLocalFiles.saveTextAsFile(textarea.value, options.fileName, options.mimeType); TabLocalFiles.showToast(mode === "picker" ? TabLocalFiles.t("common.savedAs") : TabLocalFiles.t("common.exportedToDownloads"), "success"); saveResultRecorder.record({ result: "success", saveMode: mode, automatic: false, conflictDetected: false }); } catch (error) { const cancelled = error instanceof DOMException && error.name === "AbortError"; if (!cancelled) { console.error("[Previewer] Save As failed.", error); TabLocalFiles.showToast(TabLocalFiles.t("common.saveAsFailed"), "error"); } saveResultRecorder.record({ result: cancelled ? "cancelled" : "failed", saveMode: "save-as", automatic: false, conflictDetected: false, error }); } }; let disposed = false; const saveSnapshot = async (value, automatic, onSaved, canWrite) => { let conflictDetected = false; let expectedMtime; try { if (options.saveMode === "original" && !options.sourcePath) { setEditState("readonly"); saveResultRecorder.record({ result: "failed", saveMode: "workspace", automatic, conflictDetected: false, error: new Error("Missing source path") }, value); return false; } if (options.saveMode === "original" && options.sourcePath) { const diskStat = await TabLocalFiles.statOriginalFile(options.sourcePath); const parsedMtime = diskStat?.mtime ? Date.parse(diskStat.mtime) : Number.NaN; expectedMtime = Number.isFinite(parsedMtime) ? parsedMtime : undefined; if ((options.requireMtime || strictChatSaves) && expectedMtime === undefined) { throw new Error("Unable to verify file modification time"); } const diskText = await TabLocalFiles.readTextFromOriginalFile(options.sourcePath); if ((options.requireMtime || strictChatSaves) && diskText === undefined) { throw new Error("Unable to verify file contents"); } if (diskText !== undefined && diskText !== originalText) { conflictDetected = true; setEditState("conflict"); if (options.requireMtime || strictChatSaves) { saveResultRecorder.record({ result: "failed", saveMode: "workspace", automatic, conflictDetected: true, error: new Error("File was modified outside the workspace") }, value); return false; } if (!(await TabLocalFiles.EditSession.confirmOverwrite(options.fileName))) { saveResultRecorder.record({ result: "cancelled", saveMode: "workspace", automatic, conflictDetected: true }); return false; } } } if (canWrite && !canWrite()) { conflictDetected = true; setEditState("conflict"); saveResultRecorder.record({ result: "failed", saveMode: "workspace", automatic, conflictDetected: true, error: new Error("Editor content changed before save") }, value); return false; } setEditState("saving"); const mode = options.onSave ? await options.onSave(value) : options.saveMode === "original" && options.sourcePath ? await TabLocalFiles.saveTextToOriginalFile(options.sourcePath, value, options.fileName, options.mimeType, expectedMtime) : await TabLocalFiles.saveTextAsFile(value, options.fileName, options.mimeType); originalText = value; onSaved?.(); if (options.sourcePath && textarea.value === value) { if (draftTimer !== undefined) { window.clearTimeout(draftTimer); draftTimer = undefined; } TabLocalFiles.EditSession.clearDraft(options.sourcePath); } syncDirtyStatus("saved"); if (textarea.value === value) { setEditState("saved", automatic && mode === "workspace" ? TabLocalFiles.t("editor.autoSaved") : mode === "workspace" ? TabLocalFiles.t("common.savedToOriginal") : mode === "picker" ? TabLocalFiles.t("common.saved") : TabLocalFiles.t("common.exportedToDownloads")); } saveResultRecorder.record({ result: "success", saveMode: mode, automatic, conflictDetected }, value); return true; } catch (error) { const cancelled = error instanceof DOMException && error.name === "AbortError"; if (!cancelled) { console.error("[Previewer] Save failed.", error); setEditState("error", options.saveMode === "original" ? TabLocalFiles.t("common.saveFailedWorkspace") : TabLocalFiles.t("common.exportFailed")); } saveResultRecorder.record({ result: cancelled ? "cancelled" : "failed", saveMode: options.saveMode === "original" ? "workspace" : "export", automatic, conflictDetected, error }, value); return false; } }; if (autoSaveEnabled) { autoSaveController = createAutoSaveController({ delayMs: AUTO_SAVE_DELAY_MS, getText: () => textarea.value, isDirty: (value) => value !== originalText, save: (value) => saveSnapshot(value, true), onPending: () => { if (textarea.value !== originalText) { setEditState("dirty", TabLocalFiles.t("editor.autoSavePending")); } } }); } let manualSaveInProgress = false; const runManualSave = (operation) => { if (manualSaveInProgress) { return Promise.resolve(false); } manualSaveInProgress = true; return operation().finally(() => { manualSaveInProgress = false; }); }; const save = () => autoSaveController ? autoSaveController.flush() : runManualSave(() => saveSnapshot(textarea.value, false)); const saveReplacement = (value, expectedCurrentValue) => { const previousValue = textarea.value; const requiredValue = expectedCurrentValue ?? previousValue; return runManualSave(() => saveSnapshot(value, false, () => { if (disposed || textarea.value !== previousValue) { return; } textarea.setRangeText(value, 0, textarea.value.length, "select"); openedText = value; sourceHighlight?.refresh(); renderPreview(value); }, () => !disposed && textarea.value === requiredValue)); }; const applyChatToSurface = (edits, value, dryRun) => { if (composing) throw new Error("COMPOSING"); const surface = activeEditingSurface; if (dryRun) { return surface?.rootId === options.rootId && surface.applyChatEdits ? surface.applyChatEdits(edits, value, true) : value; } const { selectionStart, selectionEnd, scrollTop, scrollLeft } = textarea; externalTextPending = true; try { if (surface?.rootId === options.rootId && surface.applyChatEdits) { value = surface.applyChatEdits(edits, value); textarea.value = value; } else { const previousFocus = document.activeElement; for (let i = edits.length - 1; i >= 0; i--) { const edit = edits[i]; applyUndoableEdit(textarea, { replacement: edit.replacement, rangeStart: edit.start, rangeEnd: edit.end, selectionStart, selectionEnd }); } if (previousFocus !== textarea) previousFocus?.focus?.({ preventScroll: true }); } let start = selectionStart; let end = selectionEnd; for (const edit of edits) { const delta = edit.replacement.length - (edit.end - edit.start); if (edit.end <= selectionStart) start += delta; if (edit.end <= selectionEnd) end += delta; } textarea.setSelectionRange(Math.max(0, start), Math.max(0, end)); sourceHighlight?.refresh(); syncDirtyStatus(); renderPreview(value); textarea.scrollTop = scrollTop; textarea.scrollLeft = scrollLeft; return value; } finally { externalTextPending = false; } }; const runChatEdit = async (operation) => { if (disposed || !root.isConnected) return { success: false, error: "TARGET_UNAVAILABLE" }; if (!options.sourcePath || options.saveMode !== "original") return { success: false, error: "TARGET_READ_ONLY" }; if (chatSaveInProgress) return { success: false, error: "EDIT_BUSY" }; if (composing) return { success: false, error: "COMPOSING" }; strictChatSaves = true; runAutoSaveBarriers(); chatDocument.observe(textarea.value); chatSaveInProgress = true; try { const result = operation(); if (!result.success) return result; if (undoChatButton) undoChatButton.hidden = result.status === "undone"; const saved = await save(); return { ...result, saved }; } finally { chatSaveInProgress = false; } }; root.addEventListener("keydown", (event) => { if (TabLocalFiles.isTextCompositionEvent(event)) { return; } if (event.target === textarea && event.key === "Escape") { allowTabExit = true; return; } if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") { allowTabExit = false; event.preventDefault(); void save(); return; } if (event.key === "Tab" && event.target === textarea) { if (allowTabExit) { allowTabExit = false; return; } event.preventDefault(); applyIndent(textarea, event.shiftKey); return; } if (event.target === textarea) { allowTabExit = false; } if (editing && !editingSurfaceClaimsInput() && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "b") { event.preventDefault(); wrapSelection("**", "**", TabLocalFiles.t("markdown.bold")); } else if (editing && !editingSurfaceClaimsInput() && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "i") { event.preventDefault(); wrapSelection("_", "_", TabLocalFiles.t("markdown.italic")); } }); textarea.addEventListener("blur", () => { allowTabExit = false; }); const selectEditorContent = (event) => { if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "a") { return; } if (event.target === textarea) { return; } if (isTextInputTarget(event.target)) { return; } event.preventDefault(); if (editing) { textarea.focus(); textarea.select(); return; } const selection = window.getSelection(); if (!selection) { return; } const range = document.createRange(); range.selectNodeContents(preview); selection.removeAllRanges(); selection.addRange(range); }; document.addEventListener("keydown", selectEditorContent); const wrapSelection = (before, after, placeholder) => { setEditing(true); const start = textarea.selectionStart; const end = textarea.selectionEnd; const selected = textarea.value.slice(start, end) || placeholder; applyUndoableEdit(textarea, { replacement: `${before}${selected}${after}`, rangeStart: start, rangeEnd: end, selectionStart: start + before.length, selectionEnd: start + before.length + selected.length }); textarea.focus(); }; const discard = () => { if (options.sourcePath) { if (draftTimer !== undefined) { window.clearTimeout(draftTimer); draftTimer = undefined; } TabLocalFiles.EditSession.clearDraft(options.sourcePath); } textarea.value = originalText; renderPreview(originalText); syncDirtyStatus(); }; const revert = async () => { if (!canRevert()) { return false; } if (!(await TabLocalFiles.EditSession.confirmRevert(options.fileName))) { return false; } applyUndoableEdit(textarea, { replacement: openedText, rangeStart: 0, rangeEnd: textarea.value.length, selectionStart: 0, selectionEnd: openedText.length }); sourceHighlight?.refresh(); renderPreview(textarea.value); const reverted = autoSaveController ? await autoSaveController.flush() : true; if (reverted) { TabLocalFiles.showToast(TabLocalFiles.t("common.reverted"), "success"); } return reverted; }; sessionHandle = options.sourcePath ? TabLocalFiles.EditSession.register({ fileName: options.fileName, sourcePath: options.sourcePath, autoSave: autoSaveEnabled, flush: autoSaveController?.flush, save, discard }) : undefined; const flushOnVisibilityChange = () => { if (document.visibilityState === "hidden") { void autoSaveController?.flush(); } }; const flushOnPageHide = () => { void autoSaveController?.flush(); }; if (autoSaveController) { document.addEventListener("visibilitychange", flushOnVisibilityChange); window.addEventListener("pagehide", flushOnPageHide); } const dispose = () => { if (disposed) { return; } disposed = true; chatDocument.dispose(); if (previewTimer !== undefined) { window.clearTimeout(previewTimer); } if (draftTimer !== undefined) { window.clearTimeout(draftTimer); } sourceHighlight?.dispose(); autoSaveController?.dispose(); document.removeEventListener("visibilitychange", flushOnVisibilityChange); document.removeEventListener("keydown", selectEditorContent); window.removeEventListener("pagehide", flushOnPageHide); sessionHandle?.dispose(); delete root.__tlfEditor; }; if (options.mountId !== undefined) { TabLocalFiles.registerDocumentMountCleanup(options.mountId, dispose); } root.__tlfEditor = { getText: () => textarea.value, isDirty: () => textarea.value !== originalText, canRevert, revert, setText: (value, markDirty = false, rebaseSession = false) => { if (markDirty) { applyUndoableEdit(textarea, { replacement: value, rangeStart: 0, rangeEnd: textarea.value.length, selectionStart: 0, selectionEnd: value.length }); return; } textarea.setRangeText(value, 0, textarea.value.length, "select"); originalText = value; if (rebaseSession) { openedText = value; } if (options.sourcePath) { if (draftTimer !== undefined) { window.clearTimeout(draftTimer); draftTimer = undefined; } TabLocalFiles.EditSession.clearDraft(options.sourcePath); } sourceHighlight?.refresh(); renderPreview(value); syncDirtyStatus(); }, absorbExternalText: (value) => { const decision = externalTextDecision(value, textarea.value, originalText); if (decision === "noop") { return decision; } if (strictChatSaves && decision === "keep-user") { autoSaveController?.cancel(); setEditState("conflict"); return decision; } if (decision === "adopt-clean" || decision === "keep-user") { originalText = value; syncDirtyStatus(); return decision; } const { selectionStart, selectionEnd, scrollTop, scrollLeft } = textarea; textarea.setRangeText(value, 0, textarea.value.length, "preserve"); originalText = value; openedText = value; if (options.sourcePath) { if (draftTimer !== undefined) { window.clearTimeout(draftTimer); draftTimer = undefined; } TabLocalFiles.EditSession.clearDraft(options.sourcePath); } sourceHighlight?.refresh(); externalTextPending = true; syncDirtyStatus(); renderPreview(value); try { textarea.setSelectionRange(Math.min(selectionStart, value.length), Math.min(selectionEnd, value.length)); } catch { } textarea.scrollTop = scrollTop; textarea.scrollLeft = scrollLeft; return decision; }, changes, copy: copyAllText, save, saveReplacement, flushSave: save, chatDocument, writable: Boolean(options.sourcePath) && options.saveMode === "original", enableChatEditing: () => { strictChatSaves = true; }, applyChat: (request) => runChatEdit(() => { const result = chatDocument.apply(request, applyChatToSurface); if (result.success && result.status === "applied") lastChatEdit = { contextId: request.contextId, operationId: request.operationId }; return result; }), undoChat: (contextId, operationId) => runChatEdit(() => chatDocument.undo(contextId, operationId, applyChatToSurface)), saveAs, wrapSelection, setMode: requestMode, refreshPreview: () => { renderPreview(textarea.value); }, transformSelection: (transform) => { setEditing(true); const edit = transform(textarea.value, textarea.selectionStart, textarea.selectionEnd); if (!edit) { return; } applyUndoableEdit(textarea, edit); textarea.focus(); }, dispose }; TabLocalFiles.ChangeMarks.bind({ rootId: options.rootId, root, textarea, changes, mountId: options.mountId }); renderPreview(textarea.value); syncDirtyStatus(); if (recoveredDraft !== undefined) { autoSaveController?.schedule(); setEditState("dirty", TabLocalFiles.t("editor.draftRestored")); TabLocalFiles.showToast(TabLocalFiles.t("editor.draftRestored"), "success"); } } TextEditor.bind = bind; function getText(rootId) { return editorFor(rootId)?.getText(); } TextEditor.getText = getText; function captureChatContext(rootId, start, end, surface, scope) { runAutoSaveBarriers(); const editor = editorFor(rootId); if (!editor?.writable) return undefined; editor.enableChatEditing(); editor.chatDocument.observe(editor.getText()); try { return editor.chatDocument.capture(start, end, surface, scope); } catch { return undefined; } } TextEditor.captureChatContext = captureChatContext; function readChatContext(rootId, contextId) { runAutoSaveBarriers(); const editor = editorFor(rootId); if (!editor) return { success: false, error: "TARGET_UNAVAILABLE" }; editor.chatDocument.observe(editor.getText()); return editor.chatDocument.read(contextId); } TextEditor.readChatContext = readChatContext; function applyChatEdit(rootId, request) { return editorFor(rootId)?.applyChat(request) ?? Promise.resolve({ success: false, error: "TARGET_UNAVAILABLE" }); } TextEditor.applyChatEdit = applyChatEdit; function undoChatEdit(rootId, contextId, operationId) { return editorFor(rootId)?.undoChat(contextId, operationId) ?? Promise.resolve({ success: false, error: "TARGET_UNAVAILABLE" }); } TextEditor.undoChatEdit = undoChatEdit; function isDirty(rootId) { return editorFor(rootId)?.isDirty() || false; } TextEditor.isDirty = isDirty; function externalChanges(rootId) { return editorFor(rootId)?.changes; } TextEditor.externalChanges = externalChanges; function setText(rootId, value, markDirty = false) { editorFor(rootId)?.setText(value, markDirty); } TextEditor.setText = setText; function applyUndoableEditorEdit(rootId, edit) { const root = document.getElementById(rootId); const textarea = root?.querySelector("[data-tlf-editor-textarea]"); const sourceLabel = root?.querySelector("[data-tlf-editor-label]"); if (!textarea || !sourceLabel) { return; } const sourceWasHidden = sourceLabel.hidden; const previousFocus = document.activeElement; if (sourceWasHidden) { sourceLabel.hidden = false; } try { applyUndoableEdit(textarea, edit); } finally { sourceLabel.hidden = sourceWasHidden; if (previousFocus && previousFocus !== textarea) { previousFocus.focus?.({ preventScroll: true }); } } } TextEditor.applyUndoableEditorEdit = applyUndoableEditorEdit; function rebaseFromDisk(rootId, value) { editorFor(rootId)?.setText(value, false, true); } TextEditor.rebaseFromDisk = rebaseFromDisk; function save(rootId) { return editorFor(rootId)?.save() || Promise.resolve(false); } TextEditor.save = save; function saveReplacement(rootId, value, expectedCurrentValue) { return editorFor(rootId)?.saveReplacement(value, expectedCurrentValue) || Promise.resolve(false); } TextEditor.saveReplacement = saveReplacement; function absorbExternalText(rootId, value) { return editorFor(rootId)?.absorbExternalText(value); } TextEditor.absorbExternalText = absorbExternalText; function revert(rootId) { return editorFor(rootId)?.revert() || Promise.resolve(false); } TextEditor.revert = revert; async function saveAsFirst() { const root = document.querySelector("[data-tlf-editor]"); if (!root?.id) { return; } await editorFor(root.id)?.saveAs(); } TextEditor.saveAsFirst = saveAsFirst; async function copyFirst() { const root = document.querySelector("[data-tlf-editor]"); if (!root?.id) { return; } await editorFor(root.id)?.copy(); } TextEditor.copyFirst = copyFirst; async function saveFirst() { const root = document.querySelector("[data-tlf-editor]"); if (!root?.id) { return; } await editorFor(root.id)?.save(); } TextEditor.saveFirst = saveFirst; async function revertFirst() { const root = document.querySelector("[data-tlf-editor]"); if (!root?.id) { return; } await editorFor(root.id)?.revert(); } TextEditor.revertFirst = revertFirst; function wrapSelection(rootId, before, after, placeholder) { editorFor(rootId)?.wrapSelection(before, after, placeholder); } TextEditor.wrapSelection = wrapSelection; function findHistoryTextarea() { return document.querySelector("[data-tlf-editor] [data-tlf-editor-textarea], [data-tlf-editable-session] textarea"); } TextEditor.findHistoryTextarea = findHistoryTextarea; function runHistoryCommandFirst(command) { if (activeEditingSurface && editingSurfaceClaimsInput()) { activeEditingSurface.runHistory(command); return; } const textarea = findHistoryTextarea(); if (!textarea || textarea.readOnly || textarea.disabled) { return; } textarea.focus(); document.execCommand(command); } TextEditor.runHistoryCommandFirst = runHistoryCommandFirst; function transformSelection(rootId, transform) { editorFor(rootId)?.transformSelection(transform); } TextEditor.transformSelection = transformSelection; function refreshPreview(rootId) { editorFor(rootId)?.refreshPreview(); } TextEditor.refreshPreview = refreshPreview; function setModeFirst(editing) { const root = document.querySelector("[data-tlf-editor]"); const editor = root?.id ? editorFor(root.id) : undefined; if (!editor) { return false; } editor.setMode(editing); return true; } TextEditor.setModeFirst = setModeFirst; function dispose(rootId) { editorFor(rootId)?.dispose(); } TextEditor.dispose = dispose; const HIGHLIGHT_DEBOUNCE_MS = 90; const HIGHLIGHT_MAX_CHARS = 200000; function attachSourceHighlight(options) { const { textarea, highlight, language } = options; const scroller = highlight.parentElement; let timer; let renderSequence = 0; let disposed = false; const overlayText = (value) => (value.endsWith("\n") || value === "" ? `${value} ` : value); const syncViewport = () => { if (!scroller) { return; } if (textarea.clientWidth > 0) { scroller.style.width = `${textarea.clientWidth}px`; } if (textarea.clientHeight > 0) { scroller.style.height = `${textarea.clientHeight}px`; } }; const syncScroll = () => { if (scroller) { syncViewport(); scroller.scrollTop = textarea.scrollTop; scroller.scrollLeft = textarea.scrollLeft; } }; const showPlainText = (value) => { highlight.textContent = overlayText(value); syncScroll(); }; const renderHighlight = (value, sequence) => { if (disposed || sequence !== renderSequence) { return; } const highlighter = window.speedHighlight; if (!highlighter?.highlightText || value.length > HIGHLIGHT_MAX_CHARS) { showPlainText(value); return; } void highlighter .highlightText(overlayText(value), language) .then((html) => { if (!disposed && sequence === renderSequence) { highlight.innerHTML = html; syncScroll(); } }) .catch(() => { if (!disposed && sequence === renderSequence) { showPlainText(value); } }); }; const refresh = () => { if (disposed) { return; } const value = textarea.value; const sequence = ++renderSequence; showPlainText(value); renderHighlight(value, sequence); }; const scheduleRefresh = () => { if (disposed) { return; } if (timer !== undefined) { window.clearTimeout(timer); } const value = textarea.value; const sequence = ++renderSequence; showPlainText(value); timer = window.setTimeout(() => { timer = undefined; renderHighlight(value, sequence); }, HIGHLIGHT_DEBOUNCE_MS); }; textarea.addEventListener("input", scheduleRefresh); textarea.addEventListener("scroll", syncScroll); const resizeObserver = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => syncViewport()); resizeObserver?.observe(textarea); const dispose = () => { if (disposed) { return; } disposed = true; if (timer !== undefined) { window.clearTimeout(timer); } resizeObserver?.disconnect(); textarea.removeEventListener("input", scheduleRefresh); textarea.removeEventListener("scroll", syncScroll); }; if (options.mountId !== undefined) { TabLocalFiles.registerDocumentMountCleanup(options.mountId, dispose); } refresh(); return { refresh, dispose }; } TextEditor.attachSourceHighlight = attachSourceHighlight; function createAutoSaveController(options) { let timer; let inFlight; let disposed = false; const clearTimer = () => { if (timer !== undefined) { window.clearTimeout(timer); timer = undefined; } }; const drain = async () => { while (!disposed) { const snapshot = options.getText(); if (!options.isDirty(snapshot)) { return true; } const saved = await options.save(snapshot); if (!saved && options.getText() === snapshot) { return false; } } return false; }; const flush = () => { clearTimer(); if (disposed) { return Promise.resolve(false); } runAutoSaveBarriers(); if (!inFlight) { inFlight = drain().finally(() => { inFlight = undefined; }); } return inFlight; }; return { schedule: () => { if (disposed) { return; } clearTimer(); options.onPending?.(); timer = window.setTimeout(() => { timer = undefined; void flush(); }, options.delayMs); }, cancel: clearTimer, flush, dispose: () => { disposed = true; clearTimer(); } }; } TextEditor.createAutoSaveController = createAutoSaveController; function editorFor(rootId) { const root = document.getElementById(rootId); return root?.__tlfEditor; } function applyIndent(textarea, outdent) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const edit = indentEdit(textarea.value, start, end, outdent); if (edit.rangeStart === edit.rangeEnd && !edit.replacement) { return; } applyUndoableEdit(textarea, edit); } function indentEdit(value, selectionStart, selectionEnd, outdent) { if (selectionStart === selectionEnd) { if (!outdent) { return { replacement: INDENT, rangeStart: selectionStart, rangeEnd: selectionEnd, selectionStart: selectionStart + INDENT.length, selectionEnd: selectionEnd + INDENT.length }; } const lineStart = value.lastIndexOf("\n", Math.max(0, selectionStart - 1)) + 1; const indentLength = value.slice(lineStart, lineStart + INDENT.length).match(/^ {1,2}/)?.[0].length || 0; return { replacement: "", rangeStart: lineStart, rangeEnd: lineStart + indentLength, selectionStart: Math.max(lineStart, selectionStart - indentLength), selectionEnd: Math.max(lineStart, selectionEnd - indentLength) }; } const lineStart = value.lastIndexOf("\n", Math.max(0, selectionStart - 1)) + 1; const rangeEnd = value[selectionEnd - 1] === "\n" ? selectionEnd - 1 : selectionEnd; const block = value.slice(lineStart, rangeEnd); const lines = block.split("\n"); const firstLineDelta = outdent ? -(lines[0].match(/^ {1,2}/)?.[0].length || 0) : INDENT.length; const replacement = lines .map((line) => (outdent ? line.replace(/^ {1,2}/, "") : `${INDENT}${line}`)) .join("\n"); const totalDelta = replacement.length - block.length; return { replacement, rangeStart: lineStart, rangeEnd, selectionStart: Math.max(lineStart, selectionStart + firstLineDelta), selectionEnd: Math.max(lineStart, selectionEnd + totalDelta) }; } TextEditor.indentEdit = indentEdit; function isTextInputTarget(target) { const element = target; return Boolean(element?.closest?.('input, textarea, select, [contenteditable]:not([contenteditable="false"])')); } TextEditor.isTextInputTarget = isTextInputTarget; async function copyTextToClipboard(value, host) { if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(value); return "clipboard"; } catch { } } const fallback = document.createElement("textarea"); fallback.value = value; fallback.readOnly = true; fallback.setAttribute("aria-hidden", "true"); fallback.style.position = "fixed"; fallback.style.inset = "0"; fallback.style.opacity = "0"; fallback.style.pointerEvents = "none"; host.append(fallback); fallback.focus(); fallback.select(); const copied = document.execCommand("copy"); fallback.remove(); if (!copied) { throw new Error("Copy failed"); } return "selection"; } })(TextEditor = TabLocalFiles.TextEditor || (TabLocalFiles.TextEditor = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ChatEditProtocol; (function (ChatEditProtocol) { ChatEditProtocol.PORT = "tlf-chat-edit-v1"; ChatEditProtocol.MESSAGE = "tlf-chat-edit-v1"; ChatEditProtocol.TTL_MS = 10 * 60 * 1000; ChatEditProtocol.HEARTBEAT_MS = 20 * 1000; ChatEditProtocol.RECONNECT_MS = 500; ChatEditProtocol.RECONNECT_MAX_MS = 30 * 1000; ChatEditProtocol.WARMUP_MS = 10 * 1000; ChatEditProtocol.REREGISTER_WAIT_MS = 3 * 1000; function trustedWebSender(sender) { if (sender.id || !sender.url) return false; try { const url = new URL(sender.url); if (sender.origin !== undefined && sender.origin !== url.origin) return false; return new Set([ "http://localhost:3000", "https://tabai-test.meituan.com", "https://tab-browser-test-sg.meituan.com", "https://web.tabbit.com", "https://staging.tabbit.com", "https://web.tabbit.ai", "https://staging.tabbit.ai" ]).has(url.origin) && /^\/(newtab|chat|session|panel|sidebar)(\/|$)/.test(url.pathname); } catch { return false; } } ChatEditProtocol.trustedWebSender = trustedWebSender; })(ChatEditProtocol = TabLocalFiles.ChatEditProtocol || (TabLocalFiles.ChatEditProtocol = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ChatEditViewer; (function (ChatEditViewer) { let port; const targets = new Map(); let heartbeat; let reconnect; let failures = 0; function hasLiveTargets() { const now = Date.now(); for (const [id, target] of targets) if (target.expires < now) targets.delete(id); return targets.size > 0; } function stopHeartbeat() { if (heartbeat !== undefined) clearInterval(heartbeat); heartbeat = undefined; } function release() { stopHeartbeat(); const closing = port; port = undefined; try { closing?.disconnect?.(); } catch { } } function handleRequest(connection, message) { if (message?.kind !== "request") return; const { data, requestId } = message; const target = targets.get(data?.contextId); const run = async () => { if (!target || target.expires < Date.now() || !document.getElementById(target.rootId)) return { success: false, error: "TARGET_EXPIRED" }; if (data.action === "read") return TabLocalFiles.TextEditor.readChatContext(target.rootId, data.contextId); if (data.action === "apply") return TabLocalFiles.TextEditor.applyChatEdit(target.rootId, data); if (data.action === "undo") return TabLocalFiles.TextEditor.undoChatEdit(target.rootId, data.contextId, data.operationId); return { success: false, error: "INVALID_REQUEST" }; }; void run().catch(() => ({ success: false, error: "EDIT_FAILED" })).then((result) => { try { connection.postMessage({ kind: "result", requestId, result }); } catch { } }); } function scheduleReconnect() { if (reconnect !== undefined || !hasLiveTargets()) return; const delay = Math.min(TabLocalFiles.ChatEditProtocol.RECONNECT_MS * 2 ** failures, TabLocalFiles.ChatEditProtocol.RECONNECT_MAX_MS); failures++; reconnect = setTimeout(() => { reconnect = undefined; if (hasLiveTargets()) connect(); }, delay); } function connect() { if (port) return port; const runtime = globalThis.chrome?.runtime; if (!runtime?.connect) return undefined; let connection; try { connection = runtime.connect({ name: TabLocalFiles.ChatEditProtocol.PORT }); } catch { return undefined; } port = connection; connection.onDisconnect.addListener(() => { if (port !== connection) return; port = undefined; stopHeartbeat(); scheduleReconnect(); }); connection.onMessage.addListener((message) => handleRequest(connection, message)); for (const [contextId, target] of targets) { try { connection.postMessage({ kind: "register", contextId, expires: target.expires }); } catch { } } heartbeat = setInterval(() => { if (!hasLiveTargets()) { release(); return; } failures = 0; try { connection.postMessage({ kind: "ping" }); } catch { } }, TabLocalFiles.ChatEditProtocol.HEARTBEAT_MS); return connection; } function register(rootId, context) { const expires = Date.now() + TabLocalFiles.ChatEditProtocol.TTL_MS; targets.set(context.contextId, { rootId, expires }); while (targets.size > 64) targets.delete(targets.keys().next().value); const existing = port; const connection = connect(); if (!connection) { targets.delete(context.contextId); return undefined; } if (existing) { try { existing.postMessage({ kind: "register", contextId: context.contextId, expires }); } catch { } } return { ...context, provider: "tab-local-files", title: document.title }; } function captureDocument(rootId) { const context = TabLocalFiles.TextEditor.captureChatContext(rootId, 0, 0, "preview", "document"); return context && register(rootId, context); } ChatEditViewer.captureDocument = captureDocument; function captureSelection() { const active = document.activeElement; if (active instanceof HTMLTextAreaElement && active.matches("[data-tlf-editor-textarea]")) { const root = active.closest("[data-tlf-editor]"); if (!root || active.selectionStart === active.selectionEnd) return undefined; const context = TabLocalFiles.TextEditor.captureChatContext(root.id, active.selectionStart, active.selectionEnd, "source", "selection"); return context && register(root.id, context); } const inline = TabLocalFiles.MarkdownInlineEdit.captureChatSelection(); if (inline) return register(inline.rootId, inline.context); const selection = window.getSelection(); if (!selection?.rangeCount || selection.isCollapsed) return undefined; const range = selection.getRangeAt(0); const element = range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement; const block = element?.closest("[data-tlf-src-lines]"); const root = block?.closest("[data-tlf-editor]"); if (!block || !root || !block.contains(range.endContainer)) return undefined; const value = TabLocalFiles.TextEditor.getText(root.id); if (value === undefined) return undefined; const [startLine, endLine] = (block.dataset.tlfSrcLines ?? "").split("-").map(Number); if (!Number.isInteger(startLine) || !Number.isInteger(endLine)) return undefined; const lines = value.split("\n"); const sourceStart = lines.slice(0, startLine).reduce((sum, line) => sum + line.length + 1, 0); const source = lines.slice(startLine, endLine).join("\n"); const marked = globalThis.marked; if (!marked?.lexer) return undefined; const projection = TabLocalFiles.ChatSource.project(source, marked.lexer(source)); if (!projection || projection.text !== block.textContent) return undefined; const prefix = document.createRange(); prefix.selectNodeContents(block); prefix.setEnd(range.startContainer, range.startOffset); const from = prefix.toString().length; const raw = TabLocalFiles.ChatSource.sourceRange(projection, from, from + range.toString().length); if (!raw) return undefined; const context = TabLocalFiles.TextEditor.captureChatContext(root.id, sourceStart + raw.start, sourceStart + raw.end, "preview", "selection"); return context && register(root.id, context); } ChatEditViewer.captureSelection = captureSelection; })(ChatEditViewer = TabLocalFiles.ChatEditViewer || (TabLocalFiles.ChatEditViewer = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var LiveSync; (function (LiveSync) { const POLL_INTERVAL_MS = 1000; const ACTIVE_GAP_MS = 2000; const COOLDOWN_AFTER_MS = 3000; const IDLE_AFTER_MS = 2000; function createExternalWriteTracker(options) { let state = "idle"; let lastChangeAt = 0; let timer; let disposed = false; const clearTimer = () => { if (timer !== undefined) { options.cancel(timer); timer = undefined; } }; const setState = (next) => { if (state === next) { return; } state = next; options.onStateChange(next); }; const toCooldown = () => { setState("cooldown"); clearTimer(); timer = options.schedule(() => { timer = undefined; setState("idle"); }, IDLE_AFTER_MS); }; const toActive = () => { clearTimer(); setState("active"); timer = options.schedule(() => { timer = undefined; toCooldown(); }, COOLDOWN_AFTER_MS); }; return { recordExternalChange: () => { if (disposed) { return; } const now = options.now(); const gap = lastChangeAt === 0 ? Number.POSITIVE_INFINITY : now - lastChangeAt; lastChangeAt = now; if (gap > 0 && gap < ACTIVE_GAP_MS) { toActive(); } else if (state === "active") { toActive(); } }, getState: () => state, dispose: () => { disposed = true; clearTimer(); } }; } LiveSync.createExternalWriteTracker = createExternalWriteTracker; let activeController; function bind(options) { if (!document.getElementById(options.rootId)) { return; } const controller = { rootId: options.rootId, sourcePath: options.sourcePath, absorb: options.absorb ?? ((diskText) => TabLocalFiles.TextEditor.absorbExternalText(options.rootId, diskText)), tracker: createExternalWriteTracker({ now: () => Date.now(), schedule: (fn, ms) => window.setTimeout(fn, ms), cancel: (id) => window.clearTimeout(id), onStateChange: (state) => TabLocalFiles.setExternalWriteActive(state === "active") }), polling: false, disposed: false }; activeController = controller; const onVisibilityChange = () => { if (!document.hidden && !controller.disposed) { void poll(controller); } }; const canPoll = typeof window.setTimeout === "function" && typeof document.addEventListener === "function"; if (canPoll) { document.addEventListener("visibilitychange", onVisibilityChange); } TabLocalFiles.setExternalWriteActive(false); if (canPoll) { schedulePoll(controller); } TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => { controller.disposed = true; controller.tracker.dispose(); TabLocalFiles.setExternalWriteActive(false); if (controller.pollTimer !== undefined) { window.clearTimeout(controller.pollTimer); controller.pollTimer = undefined; } if (canPoll) { document.removeEventListener("visibilitychange", onVisibilityChange); } if (activeController === controller) { activeController = undefined; } }); } LiveSync.bind = bind; function schedulePoll(controller) { if (controller.disposed || controller.pollTimer !== undefined) { return; } controller.pollTimer = window.setTimeout(() => { controller.pollTimer = undefined; void poll(controller); }, POLL_INTERVAL_MS); } async function poll(controller) { if (controller.disposed || controller.polling) { return; } if (document.hidden) { schedulePoll(controller); return; } controller.polling = true; try { const stat = await TabLocalFiles.statOriginalFile(controller.sourcePath); if (controller.disposed) { return; } if (!stat) { return; } const statKey = `${stat.mtime || ""}|${stat.size}`; if (controller.lastStatKey === undefined) { controller.lastStatKey = statKey; return; } if (statKey === controller.lastStatKey) { return; } controller.lastStatKey = statKey; const diskText = await TabLocalFiles.readTextFromOriginalFile(controller.sourcePath); if (controller.disposed || diskText === undefined) { return; } const decision = controller.absorb(diskText); if (decision === undefined) { return; } if (decision !== "noop") { controller.tracker.recordExternalChange(); } if (decision === "apply") { TabLocalFiles.updateFileHeaderStatMetadata(stat); } } catch (error) { console.warn("[Previewer] Markdown live sync poll failed.", error); } finally { controller.polling = false; schedulePoll(controller); } } })(LiveSync = TabLocalFiles.LiveSync || (TabLocalFiles.LiveSync = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var DirectoryWatch; (function (DirectoryWatch) { const POLL_INTERVAL_MS = 2000; const MAX_WATCHED_LEVELS = 20; const SIDEBAR_SELECTOR = ".tlf-directory-sidebar[data-tlf-directory-snapshot]"; const TREE_SELECTOR = ".tlf-directory-tree"; const NODE_SELECTOR = "details[data-tlf-directory-node]"; const CHILDREN_SELECTOR = "[data-tlf-directory-children]"; const LIMIT_MESSAGE_SELECTOR = ":scope > .tlf-directory-tree-limit"; function entryNamesSignature(names) { return [...names].sort().join("\n"); } DirectoryWatch.entryNamesSignature = entryNamesSignature; function isWatchableLevel(level) { return Boolean(level.path) && level.open && level.loaded && !level.loading && !level.limited && !level.archive; } DirectoryWatch.isWatchableLevel = isWatchableLevel; function levelsNeedingRefresh(polls, refreshedSignatures) { const stale = polls .filter((poll) => poll.renderedSignature !== poll.listingSignature && refreshedSignatures[poll.path] !== poll.listingSignature) .map((poll) => poll.path); return pruneNestedPaths(stale); } DirectoryWatch.levelsNeedingRefresh = levelsNeedingRefresh; function pruneNestedPaths(paths) { const ordered = [...new Set(paths)].sort((a, b) => pathDepth(a) - pathDepth(b) || a.localeCompare(b)); const kept = []; for (const path of ordered) { if (!kept.some((ancestor) => path.startsWith(ancestor))) { kept.push(path); } } return kept; } DirectoryWatch.pruneNestedPaths = pruneNestedPaths; function pathDepth(path) { return path.split("/").length; } let started = false; let visibilityBound = false; let pollTimer; let polling = false; let refreshedSignatures = {}; function start() { if (started || typeof window.setTimeout !== "function" || typeof document.addEventListener !== "function") { return; } if (!document.querySelector(SIDEBAR_SELECTOR)) { return; } started = true; if (!visibilityBound) { visibilityBound = true; document.addEventListener("visibilitychange", () => { if (started && !document.hidden) { void poll(); } }); } schedulePoll(); } DirectoryWatch.start = start; function stop() { started = false; refreshedSignatures = {}; if (pollTimer !== undefined) { window.clearTimeout(pollTimer); pollTimer = undefined; } } function schedulePoll() { if (pollTimer !== undefined) { return; } pollTimer = window.setTimeout(() => { pollTimer = undefined; void poll(); }, POLL_INTERVAL_MS); } async function poll() { if (polling) { return; } if (document.hidden) { schedulePoll(); return; } const sidebar = document.querySelector(SIDEBAR_SELECTOR); if (!sidebar) { stop(); return; } if (sidebar.dataset.tlfDirectoryBusy === "true") { schedulePoll(); return; } polling = true; try { const polls = []; for (const level of collectLevels(sidebar)) { const entries = await TabLocalFiles.listDirectoryEntries(level.path); if (!entries || !level.container.isConnected) { continue; } TabLocalFiles.Directory.refreshDirectoryOverview(level.path, entries); polls.push({ path: level.path, renderedSignature: entryNamesSignature(renderedEntryNames(level.container)), listingSignature: entryNamesSignature(TabLocalFiles.Directory.visibleEntryNames(entries)) }); } const stalePaths = levelsNeedingRefresh(polls, refreshedSignatures); refreshedSignatures = Object.fromEntries(polls.map((level) => [level.path, level.listingSignature])); for (const path of stalePaths) { await TabLocalFiles.Directory.refreshSidebarDirectoryLevel(path); } } catch (error) { console.warn("[Previewer] Directory sidebar watch failed.", error); } finally { polling = false; schedulePoll(); } } function collectLevels(sidebar) { const levels = []; const tree = sidebar.querySelector(TREE_SELECTOR); const rootPath = sidebarRootPath(sidebar); if (tree && rootPath) { pushLevel(levels, tree, { path: rootPath, open: true, loaded: true, loading: false, limited: isLimitedLevel(tree), archive: isArchivePath(rootPath) }); } for (const node of Array.from(sidebar.querySelectorAll(NODE_SELECTOR))) { if (levels.length >= MAX_WATCHED_LEVELS) { break; } const children = node.querySelector(CHILDREN_SELECTOR); const path = TabLocalFiles.Directory.treeNodePath(node); if (!children || !path) { continue; } pushLevel(levels, children, { path, open: node.open, loaded: children.dataset.tlfDirectoryLoaded === "true", loading: children.dataset.tlfDirectoryLoading === "true", limited: isLimitedLevel(children), archive: Boolean(node.dataset.tlfDirectoryArchivePath) || isArchivePath(path) }); } return levels; } function pushLevel(levels, container, descriptor) { if (isWatchableLevel(descriptor)) { levels.push({ ...descriptor, container }); } } function isArchivePath(path) { return TabLocalFiles.ZipArchive.isArchivePath(path) || TabLocalFiles.ZipArchive.isVirtualPath(path); } function isLimitedLevel(container) { return Boolean(container.querySelector(LIMIT_MESSAGE_SELECTOR)); } function sidebarRootPath(sidebar) { const rawSnapshot = sidebar.dataset.tlfDirectorySnapshot; if (!rawSnapshot) { return undefined; } try { const snapshot = JSON.parse(rawSnapshot); if (!snapshot.rootPath) { return undefined; } return snapshot.rootPath.endsWith("/") ? snapshot.rootPath : `${snapshot.rootPath}/`; } catch { return undefined; } } function renderedEntryNames(container) { const names = []; for (const row of Array.from(container.children)) { const label = row.matches(NODE_SELECTOR) ? row.querySelector(":scope > summary > .tlf-directory-tree-label") : row.matches("a.tlf-directory-tree-file") ? row.querySelector(":scope > .tlf-directory-tree-label") : undefined; const name = label?.textContent; if (name) { names.push(name); } } return names; } })(DirectoryWatch = TabLocalFiles.DirectoryWatch || (TabLocalFiles.DirectoryWatch = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ChangeMarks; (function (ChangeMarks) { ChangeMarks.READ_DWELL_MS = 5000; const VISIBLE_RATIO = 0.6; const VIEWPORT_FILL_RATIO = 0.5; const IO_THRESHOLDS = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]; const GUTTER_CLICK_ZONE_PX = 28; const REFRESH_DELAY_MS = 30; const SCROLL_THROTTLE_MS = 60; const EDGE_SLACK_PX = 8; const IDS_ATTR = "data-tlf-ext-ids"; const MARK_SELECTOR = `[${IDS_ATTR}]`; const BLOCK_CLASSES = ["tlf-ext", "tlf-ext-add", "tlf-ext-mod", "tlf-ext-del-before", "tlf-ext-del-after", "tlf-ext-fresh"]; const controllers = new Map(); function bind(options) { if (!TabLocalFiles.ExternalChanges.readStoredEnabled()) { return; } if (typeof options.root.querySelectorAll !== "function" || typeof options.root.addEventListener !== "function") { return; } const existing = controllers.get(options.rootId); if (existing) { dispose(existing); } const controller = { rootId: options.rootId, root: options.root, textarea: options.textarea, changes: options.changes, disposed: false, washed: new Set(), markers: [], overlays: new Map(), tracker: createReadTracker((ids) => { if (!controller.disposed) { controller.changes.markRead(ids); } }, (id) => isStillInView(controller, id)), cleanups: [] }; controllers.set(options.rootId, controller); controller.cleanups.push(controller.changes.onChange(() => scheduleRefresh(controller))); const onScroll = () => { if (controller.scrollTimer !== undefined || controller.disposed) { return; } controller.scrollTimer = window.setTimeout(() => { controller.scrollTimer = undefined; syncTextareaOverlay(controller); syncMarkers(controller); }, SCROLL_THROTTLE_MS); }; options.root.addEventListener("scroll", onScroll, true); controller.cleanups.push(() => options.root.removeEventListener("scroll", onScroll, true)); const onClick = (event) => onRootClick(controller, event); options.root.addEventListener("click", onClick); controller.cleanups.push(() => options.root.removeEventListener("click", onClick)); const onModeChange = () => scheduleRefresh(controller); options.root.addEventListener("tlf:editor-mode-change", onModeChange); controller.cleanups.push(() => options.root.removeEventListener("tlf:editor-mode-change", onModeChange)); if (typeof MutationObserver === "function") { const observer = new MutationObserver(() => scheduleRefresh(controller)); observer.observe(options.root, { attributes: true, attributeFilter: ["data-tlf-edit-variant"] }); controller.cleanups.push(() => observer.disconnect()); } if (typeof ResizeObserver === "function") { const observer = new ResizeObserver(() => scheduleRefresh(controller)); observer.observe(options.root); controller.cleanups.push(() => observer.disconnect()); } const onVisibility = () => controller.tracker.onVisibilityChange(!document.hidden); document.addEventListener("visibilitychange", onVisibility); controller.cleanups.push(() => document.removeEventListener("visibilitychange", onVisibility)); TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => dispose(controller)); } ChangeMarks.bind = bind; function refresh(rootId) { const controller = controllers.get(rootId); if (controller) { scheduleRefresh(controller); } } ChangeMarks.refresh = refresh; function dispose(controller) { controller.disposed = true; controller.cleanups.forEach((cleanup) => cleanup()); controller.tracker.dispose(); if (controller.refreshTimer !== undefined) { window.clearTimeout(controller.refreshTimer); } if (controller.scrollTimer !== undefined) { window.clearTimeout(controller.scrollTimer); } controller.proseMirrorObserver?.disconnect(); clearBlockMarks(controller.root); controller.overlays.forEach((overlay) => overlay.remove()); controller.mirror?.remove(); controller.nav?.host.remove(); if (controllers.get(controller.rootId) === controller) { controllers.delete(controller.rootId); } } function scheduleRefresh(controller) { if (controller.disposed || controller.refreshTimer !== undefined) { return; } controller.refreshTimer = window.setTimeout(() => { controller.refreshTimer = undefined; if (!controller.disposed) { render(controller); } }, REFRESH_DELAY_MS); } function render(controller) { const entries = controller.changes.entries(); clearBlockMarks(controller.root); controller.overlays.forEach((overlay) => overlay.replaceChildren()); controller.markers = []; if (entries.length) { const fresh = new Set(); entries.forEach((entry) => { if (!controller.washed.has(entry.id)) { controller.washed.add(entry.id); fresh.add(entry.id); } }); renderTextareaOverlay(controller, entries, fresh); renderCodePreview(controller, entries, fresh); renderBlockSurfaces(controller, entries, fresh); controller.markers = collectMarkers(controller); } else { controller.washed.clear(); } controller.tracker.observe(controller.markers); watchProseMirror(controller); syncNavigation(controller); } function collectMarkers(controller) { return Array.from(controller.root.querySelectorAll(MARK_SELECTOR)).filter(isDisplayed); } function syncMarkers(controller) { const markers = collectMarkers(controller); const changed = markers.length !== controller.markers.length || markers.some((element, index) => element !== controller.markers[index]); if (changed) { controller.markers = markers; controller.tracker.observe(markers); } syncNavigation(controller); } function watchProseMirror(controller) { if (typeof MutationObserver !== "function") { return; } const proseMirror = controller.root.querySelector(".ProseMirror"); if (proseMirror === controller.proseMirrorRoot) { return; } controller.proseMirrorObserver?.disconnect(); controller.proseMirrorObserver = undefined; controller.proseMirrorRoot = proseMirror ?? undefined; if (!proseMirror) { return; } const observer = new MutationObserver(() => { if (!controller.disposed && controller.scrollTimer === undefined) { controller.scrollTimer = window.setTimeout(() => { controller.scrollTimer = undefined; syncMarkers(controller); }, SCROLL_THROTTLE_MS); } }); observer.observe(proseMirror, { childList: true }); controller.proseMirrorObserver = observer; } function isStillInView(controller, id) { const elements = collectMarkers(controller).filter((element) => idsOf(element).includes(id)); return elements.some((element) => { const rect = element.getBoundingClientRect(); if (rect.height <= 0) { return false; } const scroller = scrollerOf(element); const bounds = scroller ? scroller.getBoundingClientRect() : { top: 0, bottom: window.innerHeight, height: window.innerHeight }; const visibleHeight = Math.min(rect.bottom, bounds.bottom) - Math.max(rect.top, bounds.top); return visibleHeight >= rect.height * VISIBLE_RATIO || visibleHeight >= bounds.height * VIEWPORT_FILL_RATIO; }); } function clearBlockMarks(root) { root.querySelectorAll(`.tlf-ext, .tlf-ext-del-before, .tlf-ext-del-after, .tlf-ext-fresh`).forEach((element) => { if (element.closest(".tlf-ext-overlay")) { return; } if (element.closest(".ProseMirror")) { element.classList.remove("tlf-ext-fresh"); return; } element.classList.remove(...BLOCK_CLASSES); element.removeAttribute(IDS_ATTR); }); } function isDisplayed(element) { return element.offsetParent !== null || element.getClientRects().length > 0; } function idsOf(element) { return (element.getAttribute(IDS_ATTR) || "") .split(" ") .filter(Boolean) .map((id) => Number(id)) .filter((id) => Number.isFinite(id)); } function appendId(element, id) { const ids = idsOf(element); if (!ids.includes(id)) { ids.push(id); } element.setAttribute(IDS_ATTR, ids.join(" ")); } function renderBlockSurfaces(controller, entries, fresh) { const blocks = []; controller.root.querySelectorAll("[data-tlf-src-lines]").forEach((element) => { if (element.closest(".ProseMirror")) { return; } const [start, end] = (element.getAttribute("data-tlf-src-lines") || "").split("-").map(Number); if (Number.isInteger(start) && Number.isInteger(end)) { blocks.push({ element, start, end }); } }); if (blocks.length) { markSourceBlocks(blocks, entries, fresh); } controller.root.querySelectorAll(`.ProseMirror ${MARK_SELECTOR}`).forEach((element) => { if (idsOf(element).some((id) => fresh.has(id))) { element.classList.add("tlf-ext-fresh"); } }); } function markSourceBlocks(blocks, entries, fresh) { for (const entry of entries) { if (entry.kind === "del") { const following = blocks.find((block) => block.start >= entry.start); const target = following ?? blocks[blocks.length - 1]; if (target) { target.element.classList.add(following ? "tlf-ext-del-before" : "tlf-ext-del-after"); appendId(target.element, entry.id); } continue; } const hits = blocks.filter((block) => block.start < entry.end && block.end > entry.start); const leaves = hits.filter((block) => !hits.some((other) => other !== block && block.element.contains(other.element))); for (const block of leaves) { block.element.classList.add("tlf-ext", `tlf-ext-${entry.kind}`); if (fresh.has(entry.id)) { block.element.classList.add("tlf-ext-fresh"); } appendId(block.element, entry.id); } } } ChangeMarks.markSourceBlocks = markSourceBlocks; function lineOffsets(text) { const offsets = [0]; for (let index = 0; index < text.length; index += 1) { if (text.charCodeAt(index) === 10) { offsets.push(index + 1); } } offsets.push(text.length + 1); return offsets; } function createTextMeasurer(container, text, hostRect) { if (typeof document.createRange !== "function" || typeof document.createTreeWalker !== "function") { return undefined; } const nodes = []; const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); let total = 0; for (let node = walker.nextNode(); node; node = walker.nextNode()) { const textNode = node; nodes.push({ node: textNode, start: total }); total += textNode.data.length; } if (!nodes.length) { return undefined; } const offsets = lineOffsets(text); const lineCount = offsets.length - 1; const lineHeight = typeof window.getComputedStyle === "function" ? parseFloat(window.getComputedStyle(container).lineHeight) : NaN; const positionAt = (offset) => { const clamped = Math.max(0, Math.min(offset, total)); let low = 0; let high = nodes.length - 1; while (low < high) { const mid = (low + high + 1) >> 1; if (nodes[mid].start <= clamped) { low = mid; } else { high = mid - 1; } } const found = nodes[low]; return [found.node, Math.min(clamped - found.start, found.node.data.length)]; }; const rectsBetween = (from, to) => { const range = document.createRange(); const [startNode, startOffset] = positionAt(from); const [endNode, endOffset] = positionAt(to); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); const rects = Array.from(range.getClientRects()); return rects.length ? rects : [range.getBoundingClientRect()]; }; const lineStart = (line) => offsets[Math.max(0, Math.min(line, lineCount - 1))]; const lineEnd = (line) => { const index = Math.max(0, Math.min(line, lineCount - 1)); return Math.max(offsets[index], offsets[index + 1] - 1); }; return { box: (start, end) => { if (start >= end) { return undefined; } const from = lineStart(start); const to = Math.max(from, lineEnd(end - 1)); const rects = rectsBetween(from, to === from ? Math.min(from + 1, total) : to); const host = hostRect(); let top = Number.POSITIVE_INFINITY; let bottom = Number.NEGATIVE_INFINITY; let glyphHeight = 0; rects.forEach((rect) => { if (rect.height > 0 || rect.width > 0) { top = Math.min(top, rect.top - host.top); bottom = Math.max(bottom, rect.bottom - host.top); glyphHeight = glyphHeight || rect.height; } }); if (!Number.isFinite(top) || !Number.isFinite(bottom)) { return undefined; } const leading = Number.isFinite(lineHeight) && lineHeight > glyphHeight ? (lineHeight - glyphHeight) / 2 : 0; return { top: top - leading, bottom: bottom + leading }; }, gapTop: (line) => { const host = hostRect(); if (line >= lineCount) { const rects = rectsBetween(lineStart(lineCount - 1), total); const last = rects[rects.length - 1]; return last ? last.bottom - host.top : undefined; } const from = lineStart(line); const rects = rectsBetween(from, Math.min(from + 1, total)); if (!rects[0]) { return undefined; } const leading = Number.isFinite(lineHeight) && lineHeight > rects[0].height ? (lineHeight - rects[0].height) / 2 : 0; return rects[0].top - host.top - leading; } }; } function overlayFor(controller, host, className) { let overlay = controller.overlays.get(host); if (!overlay || !overlay.isConnected || overlay.parentElement !== host) { overlay?.remove(); overlay = document.createElement("div"); overlay.className = `tlf-ext-overlay ${className}`; overlay.setAttribute("aria-hidden", "true"); host.appendChild(overlay); controller.overlays.set(host, overlay); } return overlay; } function paintOverlay(overlay, measurer, entries, fresh, lineCount) { const layer = document.createElement("div"); layer.className = "tlf-ext-overlay-layer"; for (const entry of entries) { if (entry.kind === "del") { const top = measurer.gapTop(Math.min(entry.start, lineCount)); if (top === undefined) { continue; } const mark = document.createElement("div"); mark.className = "tlf-ext-del"; mark.style.top = `${Math.round(top) - 4}px`; mark.title = TabLocalFiles.t("changes.deleted"); mark.setAttribute(IDS_ATTR, String(entry.id)); layer.appendChild(mark); continue; } const box = measurer.box(entry.start, entry.end); if (!box) { continue; } const bar = document.createElement("div"); bar.className = `tlf-ext-bar tlf-ext-${entry.kind}${fresh.has(entry.id) ? " tlf-ext-fresh" : ""}`; bar.style.top = `${Math.round(box.top)}px`; bar.style.height = `${Math.max(4, Math.round(box.bottom - box.top))}px`; bar.title = `${TabLocalFiles.t(entry.kind === "add" ? "changes.added" : "changes.modified")} · ${new Date(entry.at).toLocaleTimeString()}`; bar.setAttribute(IDS_ATTR, String(entry.id)); layer.appendChild(bar); } overlay.replaceChildren(layer); } ChangeMarks.MIRRORED_STYLES = [ "fontFamily", "fontSize", "fontWeight", "fontStyle", "lineHeight", "letterSpacing", "wordSpacing", "tabSize", "whiteSpace", "wordBreak", "overflowWrap", "textIndent", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft" ]; function renderTextareaOverlay(controller, entries, fresh) { const textarea = controller.textarea; const stack = textarea.parentElement; if (!stack || !isDisplayed(textarea) || typeof window.getComputedStyle !== "function") { return; } let mirror = controller.mirror; if (!mirror || mirror.parentElement !== stack) { mirror?.remove(); mirror = document.createElement("pre"); mirror.className = "tlf-ext-mirror"; mirror.setAttribute("aria-hidden", "true"); stack.appendChild(mirror); controller.mirror = mirror; } const computed = window.getComputedStyle(textarea); ChangeMarks.MIRRORED_STYLES.forEach((property) => { mirror.style[property] = computed[property]; }); mirror.style.boxSizing = "border-box"; mirror.style.width = `${textarea.clientWidth}px`; mirror.style.height = `${textarea.clientHeight}px`; const value = textarea.value; mirror.textContent = value.endsWith("\n") || value === "" ? `${value} ` : value; const overlay = overlayFor(controller, stack, "tlf-ext-overlay-textarea"); const measurer = createTextMeasurer(mirror, value, () => mirror.getBoundingClientRect()); if (!measurer) { overlay.replaceChildren(); return; } paintOverlay(overlay, measurer, entries, fresh, lineOffsets(value).length - 1); syncTextareaOverlay(controller); } function syncTextareaOverlay(controller) { const stack = controller.textarea.parentElement; const overlay = stack ? controller.overlays.get(stack) : undefined; const layer = overlay?.firstElementChild; if (layer) { layer.style.transform = `translateY(${-controller.textarea.scrollTop}px)`; } } function renderCodePreview(controller, entries, fresh) { const view = controller.root.querySelector("[data-tlf-editor-preview] [data-tlf-code-view]"); const code = view?.querySelector(".tlf-code-pre > code"); if (!view || !code || !isDisplayed(view)) { return; } const text = code.textContent || ""; const overlay = overlayFor(controller, view, "tlf-ext-overlay-code"); const measurer = createTextMeasurer(code, text, () => view.getBoundingClientRect()); if (!measurer) { overlay.replaceChildren(); return; } paintOverlay(overlay, measurer, entries, fresh, lineOffsets(text).length - 1); } function createReadTracker(onRead, stillInView) { const observers = new Map(); const visibleElements = new Set(); const timers = new Map(); let observed = []; const visibleIds = () => { const ids = new Set(); visibleElements.forEach((element) => idsOf(element).forEach((id) => ids.add(id))); return ids; }; const syncTimers = () => { const ids = visibleIds(); timers.forEach((timer, id) => { if (!ids.has(id)) { window.clearTimeout(timer); timers.delete(id); } }); ids.forEach((id) => { if (!timers.has(id)) { timers.set(id, window.setTimeout(() => { timers.delete(id); if (stillInView(id)) { onRead([id]); } }, ChangeMarks.READ_DWELL_MS)); } }); }; const clearTimers = () => { timers.forEach((timer) => window.clearTimeout(timer)); timers.clear(); }; const observerFor = (root) => { if (typeof IntersectionObserver !== "function") { return undefined; } let observer = observers.get(root); if (!observer) { observer = new IntersectionObserver((records) => { records.forEach((record) => { const element = record.target; const rootHeight = record.rootBounds?.height ?? 0; const inView = record.isIntersecting && (record.intersectionRatio >= VISIBLE_RATIO || (rootHeight > 0 && record.intersectionRect.height >= rootHeight * VIEWPORT_FILL_RATIO)); if (inView) { visibleElements.add(element); } else { visibleElements.delete(element); } }); syncTimers(); }, { root, threshold: IO_THRESHOLDS }); observers.set(root, observer); } return observer; }; return { observe: (elements) => { observed.forEach((element) => { observers.forEach((observer) => observer.unobserve(element)); }); visibleElements.clear(); observed = elements.slice(); observed.forEach((element) => { observerFor(scrollerOf(element))?.observe(element); }); if (!observed.length) { clearTimers(); } }, onVisibilityChange: (visible) => { if (!visible) { clearTimers(); visibleElements.clear(); return; } observed.forEach((element) => { const observer = observerFor(scrollerOf(element)); observer?.unobserve(element); observer?.observe(element); }); }, dispose: () => { clearTimers(); observers.forEach((observer) => observer.disconnect()); observers.clear(); visibleElements.clear(); observed = []; } }; } function scrollerOf(element) { let current = element.parentElement; while (current) { if (current.classList.contains("tlf-ext-overlay-textarea")) { return current.parentElement; } if (typeof window.getComputedStyle === "function") { const overflowY = window.getComputedStyle(current).overflowY; if ((overflowY === "auto" || overflowY === "scroll") && current.scrollHeight > current.clientHeight) { return current; } } current = current.parentElement; } return null; } function onRootClick(controller, event) { const target = event.target; const bar = target?.closest?.(".tlf-ext-bar, .tlf-ext-del"); if (bar) { controller.changes.markRead(idsOf(bar)); event.preventDefault(); event.stopPropagation(); return; } for (const marker of collectMarkers(controller)) { if (marker.closest(".tlf-ext-overlay")) { continue; } const rect = marker.getBoundingClientRect(); if (event.clientY >= rect.top && event.clientY <= rect.bottom && event.clientX < rect.left && event.clientX >= rect.left - GUTTER_CLICK_ZONE_PX) { controller.changes.markRead(idsOf(marker)); event.preventDefault(); return; } } } function activeScroller(controller) { const candidates = [ controller.root.querySelector("[data-tlf-editor-inline]"), controller.root.querySelector("[data-tlf-editor-preview]"), controller.textarea ]; return candidates.find((element) => Boolean(element) && isDisplayed(element) && element.clientHeight > 0); } function ensureNavigation(controller) { if (controller.nav?.host.isConnected) { return controller.nav; } const body = controller.root.querySelector("[data-tlf-editor-body]"); if (!body) { return undefined; } const host = document.createElement("div"); host.className = "tlf-ext-nav"; host.setAttribute("aria-hidden", "true"); const ruler = document.createElement("div"); ruler.className = "tlf-ext-ruler"; const makePill = (edge) => { const pill = document.createElement("button"); pill.type = "button"; pill.className = `tlf-ext-pill tlf-ext-pill-${edge}`; const label = document.createElement("span"); label.className = "tlf-ext-pill-label"; pill.appendChild(label); const clear = document.createElement("button"); clear.type = "button"; clear.className = "tlf-ext-pill-clear"; clear.title = TabLocalFiles.t("changes.markAllRead"); clear.setAttribute("aria-label", TabLocalFiles.t("changes.markAllRead")); clear.innerHTML = TabLocalFiles.renderLucideIcon("check"); clear.addEventListener("click", (event) => { event.stopPropagation(); controller.changes.markAllRead(); }); pill.appendChild(clear); pill.addEventListener("click", () => jump(controller, edge === "above" ? -1 : 1)); host.appendChild(pill); return [pill, clear]; }; host.appendChild(ruler); const [above, clearAbove] = makePill("above"); const [below, clearBelow] = makePill("below"); body.appendChild(host); controller.nav = { host, ruler, above, below, clearAbove, clearBelow }; return controller.nav; } function markerPositions(controller, scroller) { const scrollerRect = scroller.getBoundingClientRect(); const positions = []; controller.markers.forEach((element) => { const inScroller = scroller === controller.textarea ? Boolean(element.closest(".tlf-ext-overlay-textarea")) : scroller.contains(element) && !element.closest(".tlf-ext-overlay-textarea"); if (!inScroller) { return; } const rect = element.getBoundingClientRect(); positions.push({ element, top: rect.top - scrollerRect.top + scroller.scrollTop, bottom: rect.bottom - scrollerRect.top + scroller.scrollTop, kind: element.classList.contains("tlf-ext-mod") ? "mod" : element.classList.contains("tlf-ext-del") || element.classList.contains("tlf-ext-del-before") || element.classList.contains("tlf-ext-del-after") ? "del" : "add" }); }); return positions.sort((a, b) => a.top - b.top); } function syncNavigation(controller) { const scroller = activeScroller(controller); const nav = ensureNavigation(controller); if (!nav) { return; } if (!controller.markers.length || !scroller) { nav.host.hidden = true; return; } const body = nav.host.parentElement; const bodyRect = body.getBoundingClientRect(); const scrollerRect = scroller.getBoundingClientRect(); nav.host.hidden = false; nav.host.style.left = `${scrollerRect.left - bodyRect.left}px`; nav.host.style.top = `${scrollerRect.top - bodyRect.top}px`; nav.host.style.width = `${scrollerRect.width}px`; nav.host.style.height = `${scrollerRect.height}px`; const positions = markerPositions(controller, scroller); const viewTop = scroller.scrollTop; const viewBottom = viewTop + scroller.clientHeight; const scrollHeight = Math.max(1, scroller.scrollHeight); const rulerHeight = Math.max(0, scrollerRect.height); let above = 0; let below = 0; const ticks = []; positions.forEach((position) => { if (position.bottom < viewTop + EDGE_SLACK_PX) { above += 1; } else if (position.top > viewBottom - EDGE_SLACK_PX) { below += 1; } const tick = document.createElement("div"); tick.className = `tlf-ext-tick tlf-ext-tick-${position.kind}`; tick.style.top = `${Math.min(rulerHeight - 3, (position.top / scrollHeight) * rulerHeight)}px`; tick.title = TabLocalFiles.t("changes.jump"); tick.addEventListener("click", () => scrollTo(scroller, position.top)); ticks.push(tick); }); nav.ruler.replaceChildren(...ticks); nav.above.hidden = above === 0; nav.below.hidden = below === 0; nav.above.firstElementChild.textContent = TabLocalFiles.t("changes.unreadAbove", { count: above }); nav.below.firstElementChild.textContent = TabLocalFiles.t("changes.unreadBelow", { count: below }); } function scrollTo(scroller, top) { const target = Math.max(0, top - scroller.clientHeight / 3); if (typeof scroller.scrollTo === "function") { scroller.scrollTo({ top: target, behavior: "smooth" }); } else { scroller.scrollTop = target; } } function jump(controller, direction) { const scroller = activeScroller(controller); if (!scroller) { return; } const positions = markerPositions(controller, scroller); const viewTop = scroller.scrollTop; const viewBottom = viewTop + scroller.clientHeight; const target = direction < 0 ? positions.filter((position) => position.bottom < viewTop + EDGE_SLACK_PX).pop() : positions.find((position) => position.top > viewBottom - EDGE_SLACK_PX); if (target) { scrollTo(scroller, target.top); } } })(ChangeMarks = TabLocalFiles.ChangeMarks || (TabLocalFiles.ChangeMarks = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var Zip; (function (Zip) { const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50; const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50; const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50; const MAX_EOCD_SEARCH = 66000; const UTF8_GENERAL_PURPOSE_FLAG = 0x0800; async function readEntries(buffer) { const view = new DataView(buffer); const endOffset = findEndOfCentralDirectory(view); const entryCount = view.getUint16(endOffset + 10, true); const centralDirectoryOffset = view.getUint32(endOffset + 16, true); const entries = new Map(); let offset = centralDirectoryOffset; for (let index = 0; index < entryCount; index += 1) { if (view.getUint32(offset, true) !== CENTRAL_DIRECTORY_SIGNATURE) { throw new Error("Invalid ZIP central directory"); } const compressionMethod = view.getUint16(offset + 10, true); const compressedSize = view.getUint32(offset + 20, true); const fileNameLength = view.getUint16(offset + 28, true); const extraLength = view.getUint16(offset + 30, true); const commentLength = view.getUint16(offset + 32, true); const localHeaderOffset = view.getUint32(offset + 42, true); const fileName = decodeBytes(new Uint8Array(buffer, offset + 46, fileNameLength)); if (!fileName.endsWith("/")) { entries.set(fileName, await readEntryData(buffer, view, localHeaderOffset, compressedSize, compressionMethod)); } offset += 46 + fileNameLength + extraLength + commentLength; } return entries; } Zip.readEntries = readEntries; function textEntry(entries, name) { const bytes = entries.get(name); return bytes ? decodeBytes(bytes) : undefined; } Zip.textEntry = textEntry; function setTextEntry(entries, name, text) { entries.set(name, new TextEncoder().encode(text)); } Zip.setTextEntry = setTextEntry; function writeEntries(entries) { const localParts = []; const centralParts = []; let localOffset = 0; entries.forEach((data, name) => { const fileNameBytes = new TextEncoder().encode(name); const crc = crc32(data); const localHeader = createLocalHeader(fileNameBytes, data, crc); const centralHeader = createCentralHeader(fileNameBytes, data, crc, localOffset); localParts.push(localHeader, data); centralParts.push(centralHeader); localOffset += localHeader.length + data.length; }); const centralDirectorySize = centralParts.reduce((size, part) => size + part.length, 0); const centralDirectoryOffset = localOffset; const endRecord = createEndOfCentralDirectory(entries.size, centralDirectorySize, centralDirectoryOffset); return concatBytes([...localParts, ...centralParts, endRecord]); } Zip.writeEntries = writeEntries; function findEndOfCentralDirectory(view) { const minimumOffset = Math.max(0, view.byteLength - MAX_EOCD_SEARCH); for (let offset = view.byteLength - 22; offset >= minimumOffset; offset -= 1) { if (view.getUint32(offset, true) === END_OF_CENTRAL_DIRECTORY_SIGNATURE) { return offset; } } throw new Error("ZIP end of central directory not found"); } async function readEntryData(buffer, view, localHeaderOffset, compressedSize, compressionMethod) { if (view.getUint32(localHeaderOffset, true) !== LOCAL_FILE_HEADER_SIGNATURE) { throw new Error("Invalid ZIP local file header"); } const fileNameLength = view.getUint16(localHeaderOffset + 26, true); const extraLength = view.getUint16(localHeaderOffset + 28, true); const dataOffset = localHeaderOffset + 30 + fileNameLength + extraLength; const compressed = new Uint8Array(buffer.slice(dataOffset, dataOffset + compressedSize)); if (compressionMethod === 0) { return compressed; } if (compressionMethod === 8) { return inflateRaw(compressed); } throw new Error(`Unsupported ZIP compression method: ${compressionMethod}`); } async function inflateRaw(data) { if (!("DecompressionStream" in window)) { throw new Error("DecompressionStream is not available"); } const bytes = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw")); const decompressed = await new Response(stream).arrayBuffer(); return new Uint8Array(decompressed); } function decodeBytes(bytes) { return new TextDecoder("utf-8").decode(bytes); } function createLocalHeader(fileNameBytes, data, crc) { const header = new Uint8Array(30 + fileNameBytes.length); const view = new DataView(header.buffer); view.setUint32(0, LOCAL_FILE_HEADER_SIGNATURE, true); view.setUint16(4, 20, true); view.setUint16(6, UTF8_GENERAL_PURPOSE_FLAG, true); view.setUint16(8, 0, true); writeDosTimestamp(view, 10); view.setUint32(14, crc, true); view.setUint32(18, data.length, true); view.setUint32(22, data.length, true); view.setUint16(26, fileNameBytes.length, true); view.setUint16(28, 0, true); header.set(fileNameBytes, 30); return header; } function createCentralHeader(fileNameBytes, data, crc, localOffset) { const header = new Uint8Array(46 + fileNameBytes.length); const view = new DataView(header.buffer); view.setUint32(0, CENTRAL_DIRECTORY_SIGNATURE, true); view.setUint16(4, 20, true); view.setUint16(6, 20, true); view.setUint16(8, UTF8_GENERAL_PURPOSE_FLAG, true); view.setUint16(10, 0, true); writeDosTimestamp(view, 12); view.setUint32(16, crc, true); view.setUint32(20, data.length, true); view.setUint32(24, data.length, true); view.setUint16(28, fileNameBytes.length, true); view.setUint16(30, 0, true); view.setUint16(32, 0, true); view.setUint16(34, 0, true); view.setUint16(36, 0, true); view.setUint32(38, 0, true); view.setUint32(42, localOffset, true); header.set(fileNameBytes, 46); return header; } function createEndOfCentralDirectory(entryCount, centralDirectorySize, centralDirectoryOffset) { const record = new Uint8Array(22); const view = new DataView(record.buffer); view.setUint32(0, END_OF_CENTRAL_DIRECTORY_SIGNATURE, true); view.setUint16(4, 0, true); view.setUint16(6, 0, true); view.setUint16(8, entryCount, true); view.setUint16(10, entryCount, true); view.setUint32(12, centralDirectorySize, true); view.setUint32(16, centralDirectoryOffset, true); view.setUint16(20, 0, true); return record; } function writeDosTimestamp(view, offset) { const now = new Date(); const time = (now.getHours() << 11) | (now.getMinutes() << 5) | Math.floor(now.getSeconds() / 2); const date = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate(); view.setUint16(offset, time, true); view.setUint16(offset + 2, date, true); } function concatBytes(parts) { const totalLength = parts.reduce((length, part) => length + part.length, 0); const result = new Uint8Array(totalLength); let offset = 0; parts.forEach((part) => { result.set(part, offset); offset += part.length; }); return result; } function crc32(bytes) { let crc = 0xffffffff; for (let index = 0; index < bytes.length; index += 1) { crc = CRC_TABLE[(crc ^ bytes[index]) & 0xff] ^ (crc >>> 8); } return (crc ^ 0xffffffff) >>> 0; } const CRC_TABLE = createCrcTable(); function createCrcTable() { const table = []; for (let value = 0; value < 256; value += 1) { let crc = value; for (let bit = 0; bit < 8; bit += 1) { crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1; } table[value] = crc >>> 0; } return table; } })(Zip = TabLocalFiles.Zip || (TabLocalFiles.Zip = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { const PHOSPHOR_ICON_BASE_PATH_BY_THEME = { light: "icons/phosphor_light", dark: "icons/phosphor_dark" }; const PHOSPHOR_ICON_BY_EXTENSION = [ [".md", "file-md"], [".markdown", "file-md"], [".svg", "file-svg"], [".html", "file-html"], [".htm", "file-html"], [".text_html", "file-html"], [".css", "file-css"], [".json", "file-code"], [".js", "file-js"], [".jsx", "file-jsx"], [".ts", "file-ts"], [".tsx", "file-tsx"], [".py", "file-py"], [".java", "file-code"], [".go", "file-code"], [".rs", "file-rs"], [".cpp", "file-cpp"], [".c", "file-c"], [".h", "file-code"], [".hpp", "file-code"], [".xml", "file-code"], [".yaml", "file-code"], [".yml", "file-code"], [".sh", "file-code"], [".sql", "file-sql"], [".log", "file-text"], [".pdf", "file-pdf"], [".doc", "file-doc"], [".docx", "file-doc"], [".rtf", "file-doc"], [".ppt", "file-ppt"], [".pptx", "file-ppt"], [".xls", "file-xls"], [".xlsx", "file-xls"], [".csv", "file-csv"], [".tsv", "file-csv"], [".txt", "file-txt"], [".png", "file-png"], [".jpg", "file-jpg"], [".jpeg", "file-jpg"], [".webp", "file-image"], [".gif", "file-image"], [".bmp", "file-image"], [".avif", "file-image"], [".ico", "file-image"], [".apng", "file-image"], [".tex", "file-text"], [".bib", "file-text"], [".srt", "file-text"], [".vtt", "file-text"], [".ass", "file-text"], [".ssa", "file-text"], [".ttml", "file-text"], [".dfxp", "file-text"], [".smi", "file-text"], [".sbv", "file-text"], [".sub", "file-text"], [".mpl", "file-text"], [".lrc", "file-text"], [".idx", "file-text"], [".zip", "file-zip"], [".jar", "file-zip"], [".apk", "file-zip"], [".crx", "file-zip"], [".skill", "file-zip"], [".xpi", "file-zip"], [".vsix", "file-zip"], [".whl", "file-zip"], [".nupkg", "file-zip"], [".epub", "file-text"], [".odt", "file-doc"], [".ods", "file-csv"], [".odp", "file-ppt"], [".tar", "file-archive"], [".gz", "file-archive"], [".mp3", "file-audio"], [".wav", "file-audio"], [".flac", "file-audio"], [".ogg", "file-audio"], [".oga", "file-audio"], [".opus", "file-audio"], [".weba", "file-audio"], [".m4a", "file-audio"], [".mka", "file-audio"], [".mp4", "file-video"], [".webm", "file-video"], [".m4v", "file-video"], [".mkv", "file-video"], [".ogv", "file-video"], [".ogm", "file-video"], [".mov", "file-video"], [".srt", "file-video"], [".ass", "file-video"], [".ssa", "file-video"], [".vtt", "file-video"], [".sbv", "file-video"], [".sub", "file-video"], [".mpl", "file-video"], [".lrc", "file-video"], [".smi", "file-video"], [".ttml", "file-video"], [".dfxp", "file-video"], [".idx", "file-video"], [".xhtml", "file-html"], [".xht", "file-html"], [".xhtm", "file-html"] ]; const PHOSPHOR_ICON_BY_FILE_TYPE_ID = { directory: "folder", subtitle: "file-text", "code-text": "file-code" }; let currentFaviconContext; function faviconPathForFile(decodedPath, definition, theme = currentIconTheme()) { return phosphorIconPath(iconNameForFile(decodedPath, definition), theme); } TabLocalFiles.faviconPathForFile = faviconPathForFile; function themedFaviconPathsForFile(decodedPath, definition) { const iconName = iconNameForFile(decodedPath, definition); return { light: phosphorIconPath(iconName, "light"), dark: phosphorIconPath(iconName, "dark") }; } TabLocalFiles.themedFaviconPathsForFile = themedFaviconPathsForFile; function iconNameForFile(decodedPath, definition) { const fileTypeIcon = definition ? PHOSPHOR_ICON_BY_FILE_TYPE_ID[definition.id] : undefined; if (fileTypeIcon === "folder" || decodedPath.endsWith("/")) { return "folder"; } const normalizedPath = decodedPath.toLowerCase(); const extensionMatch = PHOSPHOR_ICON_BY_EXTENSION.find(([extension]) => normalizedPath.endsWith(extension)); return extensionMatch?.[1] || fileTypeIcon || "file"; } function fileIconNameForPath(decodedPath, definition) { return iconNameForFile(decodedPath, definition || TabLocalFiles.findEnabledFileType(decodedPath, TabLocalFiles.FILE_TYPE_DEFINITIONS)); } TabLocalFiles.fileIconNameForPath = fileIconNameForPath; function fileIconUrlForPath(decodedPath, definition) { return extensionResourceUrl(faviconPathForFile(decodedPath, definition || TabLocalFiles.findEnabledFileType(decodedPath, TabLocalFiles.FILE_TYPE_DEFINITIONS))); } TabLocalFiles.fileIconUrlForPath = fileIconUrlForPath; function fileIconMaskStyleForPath(decodedPath, definition) { const paths = themedFaviconPathsForFile(decodedPath, definition || TabLocalFiles.findEnabledFileType(decodedPath, TabLocalFiles.FILE_TYPE_DEFINITIONS)); return [ `--tlf-icon-url-light: url(${extensionResourceUrl(paths.light)})`, `--tlf-icon-url-dark: url(${extensionResourceUrl(paths.dark)})` ].join("; "); } TabLocalFiles.fileIconMaskStyleForPath = fileIconMaskStyleForPath; function applyFileFavicon(context) { currentFaviconContext = context; refreshFileFavicon(); } TabLocalFiles.applyFileFavicon = applyFileFavicon; function refreshFileFavicon() { if (!currentFaviconContext) { return; } const link = document.querySelector('link[rel~="icon"]') || document.createElement("link"); link.rel = "icon"; link.type = "image/svg+xml"; link.href = extensionResourceUrl(faviconPathForFile(currentFaviconContext.decodedPath, currentFaviconContext.definition)); link.dataset.tlfFavicon = currentIconTheme() === "dark" ? "phosphor-dark" : "phosphor-light"; if (!link.parentNode) { document.head.appendChild(link); } } TabLocalFiles.refreshFileFavicon = refreshFileFavicon; function currentIconTheme() { const documentTheme = typeof document !== "undefined" ? document.documentElement?.dataset?.tlfTheme : undefined; if (documentTheme === "dark" || documentTheme === "light") { return documentTheme; } return typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } function phosphorIconPath(iconName, theme) { return `${PHOSPHOR_ICON_BASE_PATH_BY_THEME[theme]}/${iconName}.svg`; } function extensionResourceUrl(path) { const runtime = globalThis.chrome?.runtime; return runtime?.getURL ? runtime.getURL(path) : path; } })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var Directory; (function (Directory) { const DIRECTORY_SIDEBAR_STORAGE_KEY = "tab-local-files:directory-sidebar"; const DIRECTORY_SETTINGS_STORAGE_KEY = "tab-local-files:directory-settings"; const DEFAULT_DIRECTORY_SETTINGS = { showHiddenFiles: false, foldersFirst: true, rememberSidebarWidth: true, sortKey: "name", sortDirection: "asc", viewMode: "list" }; function matches(decodedPath) { return decodedPath.endsWith("/"); } Directory.matches = matches; function render(context) { const entries = context.sourceUrl === window.location.href ? readDirectoryEntries(window.location.href) : readDirectoryEntriesFromHtml(context.rawText, context.sourceUrl); return { title: context.fileName, bodyHtml: renderDirectoryPreviewShell(renderDirectoryPreviewOverview(context, entries)), contentClassName: "tlf-directory", toolbar: TabLocalFiles.directoryToolbar(), sidebarHtml: context.sidebarDisabled ? undefined : renderDirectorySidebar({ rootName: context.fileName, rootPath: context.decodedPath, entries }), afterMount: () => { bindDirectoryPreviewActions(context); void bindDirectoryWorkspace(context, entries); } }; } Directory.render = render; function sidebarFromSession(currentPath) { const snapshot = readSidebarSnapshot(); if (!snapshot || !isInsideRoot(currentPath, snapshot.rootPath)) { return undefined; } return renderDirectorySidebar(snapshot, currentPath); } Directory.sidebarFromSession = sidebarFromSession; async function snapshotForPath(currentPath) { const localSnapshot = safeReadSidebarSnapshot(); if (localSnapshot && isInsideRoot(currentPath, localSnapshot.rootPath)) { return localSnapshot; } const extensionSnapshot = await safeReadExtensionSidebarSnapshot(); if (extensionSnapshot && isInsideRoot(currentPath, extensionSnapshot.rootPath)) { return extensionSnapshot; } return undefined; } Directory.snapshotForPath = snapshotForPath; function renderSnapshotSidebar(snapshot, currentPath) { return renderDirectorySidebar(snapshot, currentPath); } Directory.renderSnapshotSidebar = renderSnapshotSidebar; function readDirectoryEntries(currentUrl) { return entriesFromAnchors(document.querySelectorAll("a[href]"), currentUrl); } function readDirectoryEntriesFromHtml(rawHtml, currentUrl) { const parsedDocument = new DOMParser().parseFromString(rawHtml, "text/html"); return entriesFromAnchors(parsedDocument.querySelectorAll("a[href]"), currentUrl); } function entriesFromAnchors(anchors, currentUrl) { return Array.from(anchors) .map((anchor) => toDirectoryEntry(anchor, currentUrl)) .filter((entry) => Boolean(entry)) .filter((entry) => !isParentDirectoryEntry(entry, currentUrl)) .sort(sortEntries); } function toDirectoryEntry(anchor, currentUrl) { const href = absoluteAnchorHref(anchor, currentUrl); if (!href.startsWith("file://") || href === currentUrl) { return undefined; } const url = new URL(href); const isDirectory = url.pathname.endsWith("/"); const name = cleanEntryName(anchor.textContent || decodeURIComponent(url.pathname)); return { name: isDirectory && !name.endsWith("/") ? `${name}/` : name, href, kind: isDirectory ? "directory" : "file", hidden: isHiddenEntryName(name) }; } function absoluteAnchorHref(anchor, currentUrl) { const rawHref = anchor.getAttribute?.("href") || anchor.href; try { return new URL(rawHref, currentUrl).toString(); } catch { return anchor.href; } } function cleanEntryName(value) { return value.trim().replace(/\s+$/g, "") || TabLocalFiles.t("common.untitled"); } function visibleEntryNames(entries) { const limited = limitDirectorySnapshot({ rootName: "", rootPath: "/", entries }); return visibleDirectoryEntries(limited.entries).map((entry) => entry.name); } Directory.visibleEntryNames = visibleEntryNames; function visibleDirectoryEntries(entries, settings = readSettings()) { return entries .filter((entry) => settings.showHiddenFiles || !isHiddenDirectoryEntry(entry)) .sort((a, b) => sortEntries(a, b, settings)); } function sortEntries(a, b, settings = DEFAULT_DIRECTORY_SETTINGS) { if (settings.foldersFirst && a.kind !== b.kind) { return a.kind === "directory" ? -1 : 1; } let result = 0; if (settings.sortKey === "kind" && a.kind === "file" && b.kind === "file") { result = extensionForSort(a.name).localeCompare(extensionForSort(b.name)); } if (result === 0) { result = a.name.localeCompare(b.name); } return settings.sortDirection === "desc" ? -result : result; } function extensionForSort(name) { const match = name.toLowerCase().match(/\.[^.]+$/); return match ? match[0] : ""; } function isHiddenEntryName(name) { return name.replace(/\/+$/, "").startsWith("."); } function isHiddenDirectoryEntry(entry) { return Boolean(entry.hidden) || isHiddenEntryName(entry.name); } function isParentDirectoryEntry(entry, currentUrl) { if (entry.kind !== "directory") { return false; } try { const currentPath = decodeURIComponent(new URL(currentUrl).pathname); const parentPath = parentDirectoryPath(currentPath); const entryPath = normalizeDirectoryPath(decodeURIComponent(new URL(entry.href).pathname)); return Boolean(parentPath && entryPath === parentPath); } catch { return false; } } function renderDirectorySidebar(snapshot, currentPath = snapshot.rootPath) { const limitedSnapshot = limitDirectorySnapshot(snapshot); const visibleEntries = visibleDirectoryEntries(limitedSnapshot.entries); const limited = snapshot.entries.length > limitedSnapshot.entries.length; const treeItems = visibleEntries.length ? visibleEntries.map((entry) => renderTreeEntry(entry, limitedSnapshot, currentPath)).join("") : renderDirectoryTreeMessage("empty"); return ` `; } function limitDirectorySnapshot(snapshot) { const limit = TabLocalFiles.LargeFile.DIRECTORY_ENTRY_LIMIT; return snapshot.entries.length > limit ? { ...snapshot, entries: snapshot.entries.slice(0, limit) } : snapshot; } function renderDirectoryLimitMessage() { return `

${TabLocalFiles.escapeHtml(TabLocalFiles.t("large.directoryLimited", { count: TabLocalFiles.LargeFile.DIRECTORY_ENTRY_LIMIT }))}

`; } function renderDirectorySettingsMenu() { const settings = readSettings(); return `
${TabLocalFiles.renderLucideIcon("settings")}
`; } function renderDirectorySettingsSegmentedRow(attribute, label, activeValue, segments) { return `
${TabLocalFiles.escapeHtml(label)}
${segments.map((segment) => renderDirectorySettingsSegment(attribute, segment, activeValue)).join("")}
`; } function renderDirectorySettingsSegment(attribute, segment, activeValue) { const active = segment.value === activeValue; const content = segment.icon ? TabLocalFiles.renderLucideIcon(segment.icon) : `${TabLocalFiles.escapeHtml(segment.label)}`; return ` `; } function renderDirectorySettingSwitch(key, label, checked) { return ` `; } function renderDirectoryTreeMessage(kind) { const classes = { empty: "tlf-directory-tree-empty", loading: "tlf-directory-tree-loading", expandFailed: "tlf-directory-tree-error", readFailed: "tlf-directory-tree-error" }; const keys = { empty: "directory.empty", loading: "directory.loading", expandFailed: "directory.expandFailed", readFailed: "directory.readFailed" }; return `

${TabLocalFiles.escapeHtml(TabLocalFiles.t(keys[kind]))}

`; } function renderCreateDocumentMenu(snapshot) { const inArchive = TabLocalFiles.ZipArchive.isArchivePath(snapshot.rootPath) || TabLocalFiles.ZipArchive.isVirtualPath(snapshot.rootPath); const folderItem = inArchive ? "" : ` `; const menuItems = TabLocalFiles.CREATE_DOCUMENT_TYPES.map((type) => { return ` `; }).join(""); return `
${TabLocalFiles.renderLucideIcon("plus")}
`; } function renderTreeEntry(entry, snapshot, currentPath = snapshot.rootPath) { const decodedPath = decodedPathFromHref(entry.href); const iconStyle = TabLocalFiles.fileIconMaskStyleForPath(decodedPath || entry.href); const hiddenClass = isHiddenDirectoryEntry(entry) ? " tlf-directory-tree-entry-hidden" : ""; const manageable = Boolean(decodedPath && !TabLocalFiles.ZipArchive.isVirtualPath(decodedPath)); const draggable = manageable ? ' draggable="true"' : ""; const rowMenu = manageable ? TabLocalFiles.DirectoryManage.renderRowMenuButton() : ""; if (entry.kind === "file" && decodedPath && TabLocalFiles.ZipArchive.isArchivePath(decodedPath)) { const virtualRoot = TabLocalFiles.ZipArchive.joinVirtualPath(decodedPath, ""); const selectedClass = isSelectedArchiveNode(decodedPath, currentPath) ? " tlf-directory-tree-directory-selected" : ""; const openAttribute = shouldAutoExpandArchiveNode(decodedPath, currentPath) ? " open" : ""; return `
${TabLocalFiles.escapeHtml(entry.name)} ${rowMenu}
`; } if (entry.kind === "directory") { const sourceHref = entry.href; const selectedClass = isSelectedDirectoryEntry(decodedPath, currentPath) ? " tlf-directory-tree-directory-selected" : ""; const openAttribute = shouldAutoExpandDirectoryEntry(decodedPath, currentPath) ? " open" : ""; const entryPathAttribute = decodedPath ? ` data-tlf-directory-entry-path="${TabLocalFiles.escapeAttr(normalizeDirectoryPath(decodedPath))}"` : ""; return `
${TabLocalFiles.escapeHtml(entry.name)} ${rowMenu}
`; } const href = entryHref(entry, snapshot); const selectedClass = isSelectedFileEntry(decodedPath, currentPath) ? " tlf-directory-tree-file-selected" : ""; const entryPathAttribute = decodedPath ? ` data-tlf-directory-entry-path="${TabLocalFiles.escapeAttr(decodedPath)}"` : ""; return ` ${TabLocalFiles.escapeHtml(entry.name)} ${rowMenu} `; } function isExpandZoneClick(event) { const target = event.target; if (target?.closest(".tlf-directory-tree-marker")) { return true; } if (typeof event.clientX !== "number") { return false; } const summary = event.currentTarget; const icon = summary?.querySelector(".tlf-directory-tree-icon"); const iconLeft = icon?.getBoundingClientRect().left; return typeof iconLeft === "number" && event.clientX < iconLeft; } function bindTreeControls() { document.querySelectorAll("[data-tlf-directory-node]").forEach((node) => { if (node.dataset.tlfDirectoryBound === "true") { return; } node.dataset.tlfDirectoryBound = "true"; node.querySelector("summary")?.addEventListener("click", (event) => { if (!TabLocalFiles.InlineViewer.isPlainPrimaryClick(event)) { return; } if (isExpandZoneClick(event)) { return; } event.preventDefault(); const path = node.dataset.tlfDirectoryArchivePath || node.dataset.tlfDirectoryEntryPath || decodedPathFromHref(node.dataset.tlfDirectoryHref || ""); if (path && (TabLocalFiles.ZipArchive.isArchivePath(path) || TabLocalFiles.ZipArchive.isVirtualPath(path))) { void previewTreeNode(node); return; } const href = node.dataset.tlfDirectoryHref; if (!href) { return; } void TabLocalFiles.EditSession.requestNavigation().then((allowed) => { if (allowed) { window.location.href = TabLocalFiles.extensionViewerUrl(href); } }); }); node.addEventListener("toggle", () => { if (node.open) { void populateDirectoryNode(node); } }); if (node.open) { void populateDirectoryNode(node); } }); document.querySelectorAll("[data-tlf-directory-parent]").forEach((button) => { if (button.dataset.tlfDirectoryParentBound === "true") { return; } button.dataset.tlfDirectoryParentBound = "true"; button.addEventListener("click", () => { void navigateSidebarToParentFolder(button); }); }); } Directory.bindTreeControls = bindTreeControls; const DIRECTORY_PREVIEW_SHELL_ID = "tlf-directory-preview-shell"; function renderDirectoryPreviewShell(bodyHtml) { return `
${bodyHtml}
`; } async function previewTreeNode(node) { const path = node.dataset.tlfDirectoryArchivePath || node.dataset.tlfDirectoryEntryPath || decodedPathFromHref(node.dataset.tlfDirectoryHref || ""); if (!path) { return; } if (!(await TabLocalFiles.EditSession.requestNavigation())) { return; } const definition = TabLocalFiles.findPreviewableFileType(path) || TabLocalFiles.UnsupportedFile.definition; const fileName = TabLocalFiles.fileNameFromPath(path); try { const mountId = TabLocalFiles.beginDocumentMount(); const context = { mountId, decodedPath: path, sourceUrl: pathToFileUrl(path), fileName, fileTitle: TabLocalFiles.fileTitleFromName(fileName, definition), rawText: "", definition }; const rendered = definition.renderer(context); if (!TabLocalFiles.isCurrentDocumentMount(mountId) || !replacePreviewMain(context, rendered)) { return; } TabLocalFiles.bindLayoutControls(); markPreviewSelected(node); try { rendered.afterMount?.(context); } catch (error) { console.error("[Previewer] afterMount failed", error); void TabLocalFiles.recordRenderContextAnalyticsEvent(context, { type: "task", name: "file_preview_failed", value: "file-preview", triggerMethod: "manual_click", subModuleName: "viewer", fields: { stage: "after_mount", error_name: error instanceof Error ? error.name : typeof error } }); } void TabLocalFiles.recordRenderContextAnalyticsEvent(context, { type: "show", name: "file_preview_show", value: "file-preview", triggerMethod: "manual_click", subModuleName: "viewer", fields: { render_mode: "full", navigation_mode: "inline" } }); } catch (error) { console.error("[Previewer] Failed to mount directory preview", error); void TabLocalFiles.recordFileAnalyticsEvent({ type: "task", name: "file_preview_failed", value: "file-preview", triggerMethod: "manual_click", subModuleName: "viewer", sourceUrl: pathToFileUrl(path), fileName, fileType: definition.id, fields: { stage: "directory_preview_mount", error_name: error instanceof Error ? error.name : typeof error } }); } } function replacePreviewMain(context, rendered) { const page = document.querySelector(".tlf-page.tlf-workspace-page"); const main = page?.querySelector(":scope > .tlf-main"); if (!page || !main) { return false; } page.setAttribute("data-file-type", context.definition.id); page.classList.toggle("tlf-page-no-header", Boolean(context.headerDisabled)); main.outerHTML = TabLocalFiles.renderDocumentMain(context, rendered); return true; } function markPreviewSelected(node) { const sidebar = node.closest("[data-tlf-directory-snapshot]"); sidebar?.querySelectorAll(".tlf-directory-tree-directory-selected, .tlf-directory-tree-file-selected").forEach((selected) => { selected.classList.remove("tlf-directory-tree-directory-selected", "tlf-directory-tree-file-selected"); }); node.querySelector("summary")?.classList.add("tlf-directory-tree-directory-selected"); } let lastPreviewState; function renderDirectoryPreviewOverview(context, entries) { const settings = readSettings(); const visibleEntries = visibleDirectoryEntries(entries, settings); lastPreviewState = { context, entries }; const iconStyle = TabLocalFiles.fileIconMaskStyleForPath(context.decodedPath); const description = visibleEntries.length ? TabLocalFiles.t("directory.previewDescription") : TabLocalFiles.t("directory.empty"); return `

${TabLocalFiles.escapeHtml(context.fileName)}

${TabLocalFiles.escapeHtml(description)}

${renderDirectoryPreviewDetailRow(TabLocalFiles.t("directory.previewItemCount"), TabLocalFiles.formatCount(visibleEntries.length, "entry"))} ${renderDirectoryPreviewDetailRow(TabLocalFiles.t("directory.previewPath"), context.decodedPath)}
${renderDirectoryPreviewActions(context)}
${renderDirectoryViewToggle(settings)} ${renderDirectoryPreviewContents(visibleEntries, settings)}
`; } function renderDirectoryViewToggle(settings) { return `
${renderDirectoryViewButton("list", TabLocalFiles.t("directory.viewList"), "list", settings.viewMode)} ${renderDirectoryViewButton("grid", TabLocalFiles.t("directory.viewGrid"), "layout-grid", settings.viewMode)}
`; } function renderDirectoryViewButton(mode, label, icon, activeMode) { const active = mode === activeMode; return ` `; } function renderDirectoryPreviewContents(entries, settings) { if (!entries.length) { return `

${TabLocalFiles.escapeHtml(TabLocalFiles.t("directory.empty"))}

`; } return settings.viewMode === "grid" ? renderDirectoryPreviewGrid(entries) : renderDirectoryPreviewTable(entries, settings); } function renderDirectoryPreviewTable(entries, settings) { return `
${renderDirectorySortableColumn("name", TabLocalFiles.t("directory.sortByName"), settings)} ${renderDirectorySortableColumn("kind", TabLocalFiles.t("directory.sortByKind"), settings)} ${entries.map((entry) => renderDirectoryPreviewRow(entry)).join("")}
`; } function renderDirectorySortableColumn(key, label, settings) { const active = settings.sortKey === key; const ariaSort = active ? (settings.sortDirection === "desc" ? "descending" : "ascending") : "none"; const directionHint = active ? TabLocalFiles.t(settings.sortDirection === "desc" ? "directory.sortDescending" : "directory.sortAscending") : ""; const arrow = active ? TabLocalFiles.renderLucideIcon(settings.sortDirection === "desc" ? "chevron-down" : "chevron-up") : ""; return ` `; } function renderDirectoryPreviewRow(entry) { const hiddenClass = isHiddenDirectoryEntry(entry) ? " tlf-directory-preview-entry-hidden" : ""; return ` ${renderDirectoryEntryLink(entry, "tlf-directory-preview-name")} ${TabLocalFiles.escapeHtml(entryKindLabel(entry))} `; } function renderDirectoryPreviewGrid(entries) { return `
${entries.map((entry) => renderDirectoryPreviewCard(entry)).join("")}
`; } function renderDirectoryPreviewCard(entry) { return renderDirectoryEntryLink(entry, "tlf-directory-preview-card", entry.name, isHiddenDirectoryEntry(entry) ? "tlf-directory-preview-entry-hidden" : ""); } function renderDirectoryEntryLink(entry, linkClass, title = entry.name, extraClass = "") { const decodedPath = decodedPathFromHref(entry.href); const iconStyle = TabLocalFiles.fileIconMaskStyleForPath(decodedPath || entry.href); return ` ${TabLocalFiles.escapeHtml(entry.name)} `; } function entryKindLabel(entry) { if (entry.kind === "directory") { return TabLocalFiles.t("directory.kindFolder"); } return extensionForSort(entry.name) || "—"; } function renderDirectoryPreviewDetailRow(label, value) { return `
${TabLocalFiles.escapeHtml(label)}
${TabLocalFiles.escapeHtml(value)}
`; } function renderDirectoryPreviewActions(context) { const canReveal = TabLocalFiles.isLocalFileSourceUrl(context.sourceUrl); const revealLabel = canReveal ? TabLocalFiles.revealInFileManagerLabel() : TabLocalFiles.t("common.revealUnavailableRemote"); return `
`; } function bindDirectoryPreviewActions(context) { const revealButton = document.querySelector("[data-tlf-directory-preview-reveal]"); revealButton?.addEventListener("click", () => { void TabLocalFiles.openPathInFileManager(context.decodedPath).then((ok) => { TabLocalFiles.showToast(ok ? TabLocalFiles.t("unsupported.revealRequested") : TabLocalFiles.t("unsupported.revealFailed"), ok ? "success" : "error"); }); }); bindDirectoryPreviewContentsControls(); } function bindDirectoryPreviewContentsControls() { const shell = document.getElementById(DIRECTORY_PREVIEW_SHELL_ID); if (!shell) { return; } shell.querySelectorAll("[data-tlf-directory-column-sort]").forEach((button) => { button.addEventListener("click", () => { const sortKey = button.dataset.tlfDirectoryColumnSort; if (!sortKey) { return; } const settings = readSettings(); const sortDirection = settings.sortKey === sortKey && settings.sortDirection === "asc" ? "desc" : "asc"; writeSettings({ sortKey, sortDirection }); applySettingsToSidebars(); applySettingsToPreview(); TabLocalFiles.refreshDirectorySettingControls(); }); }); shell.querySelectorAll("[data-tlf-directory-view]").forEach((button) => { button.addEventListener("click", () => { const viewMode = button.dataset.tlfDirectoryView; if (viewMode !== "list" && viewMode !== "grid") { return; } writeSettings({ viewMode }); applySettingsToPreview(); TabLocalFiles.refreshDirectorySettingControls(); }); }); } function applySettingsToPreview() { if (!lastPreviewState) { return; } const shell = document.getElementById(DIRECTORY_PREVIEW_SHELL_ID); if (!shell) { return; } shell.innerHTML = renderDirectoryPreviewOverview(lastPreviewState.context, lastPreviewState.entries); bindDirectoryPreviewActions(lastPreviewState.context); } Directory.applySettingsToPreview = applySettingsToPreview; function refreshDirectoryOverview(directoryPath, entries) { const state = lastPreviewState; if (!state || normalizeDirectoryPath(state.context.decodedPath) !== normalizeDirectoryPath(directoryPath)) { return; } const shell = document.getElementById(DIRECTORY_PREVIEW_SHELL_ID); if (!shell || visibleEntryNames(state.entries).join("\n") === visibleEntryNames(entries).join("\n")) { return; } const scrollTop = shell.scrollTop || 0; shell.innerHTML = renderDirectoryPreviewOverview(state.context, entries); bindDirectoryPreviewActions(state.context); if (scrollTop) { shell.scrollTop = scrollTop; } } Directory.refreshDirectoryOverview = refreshDirectoryOverview; function syncSidebarCurrentPath(currentPath) { document.querySelectorAll("[data-tlf-directory-snapshot]").forEach((sidebar) => { sidebar.dataset.tlfDirectoryCurrentPath = currentPath; sidebar.querySelectorAll("[data-tlf-directory-node]").forEach((node) => { const archivePath = node.dataset.tlfDirectoryArchivePath; const entryPath = node.dataset.tlfDirectoryEntryPath || decodedPathFromHref(node.dataset.tlfDirectoryHref || ""); const normalizedEntryPath = entryPath ? normalizeDirectoryPath(entryPath) : undefined; const shouldOpen = archivePath ? shouldAutoExpandArchiveNode(archivePath, currentPath) : Boolean(normalizedEntryPath && shouldAutoExpandDirectoryEntry(normalizedEntryPath, currentPath)); const summary = node.querySelector("summary"); summary?.classList.toggle("tlf-directory-tree-directory-selected", archivePath ? isSelectedArchiveNode(archivePath, currentPath) : isSelectedDirectoryEntry(normalizedEntryPath, currentPath)); if (shouldOpen && !node.open) { node.open = true; void populateDirectoryNode(node); } }); sidebar.querySelectorAll(".tlf-directory-tree-file").forEach((file) => { const entryPath = file.dataset.tlfDirectoryEntryPath; file.classList.toggle("tlf-directory-tree-file-selected", isSelectedFileEntry(entryPath, currentPath)); }); }); } Directory.syncSidebarCurrentPath = syncSidebarCurrentPath; async function populateDirectoryNode(node) { const href = node.dataset.tlfDirectoryHref; const children = node.querySelector("[data-tlf-directory-children]"); if (!href || !children || children.dataset.tlfDirectoryLoaded === "true" || children.dataset.tlfDirectoryLoading === "true") { return; } children.dataset.tlfDirectoryLoading = "true"; children.innerHTML = renderDirectoryTreeMessage("loading"); try { const entries = await directoryNodeEntries(href); const childSnapshot = limitDirectorySnapshot(fallbackSnapshotForHref(href, entries)); const linkSnapshot = readSnapshotForTreeNode(node) || childSnapshot; const currentPath = readCurrentPathForTreeNode(node) || linkSnapshot.rootPath; const visibleEntries = visibleDirectoryEntries(childSnapshot.entries); children.innerHTML = visibleEntries.length ? visibleEntries.map((entry) => renderTreeEntry(entry, linkSnapshot, currentPath)).join("") : renderDirectoryTreeMessage("empty"); if (entries.length > childSnapshot.entries.length) { children.insertAdjacentHTML("beforeend", renderDirectoryLimitMessage()); } children.dataset.tlfDirectoryLoaded = "true"; bindTreeControls(); } catch { children.innerHTML = renderDirectoryTreeMessage("expandFailed"); } finally { delete children.dataset.tlfDirectoryLoading; } } async function directoryNodeEntries(href) { const path = decodedPathFromHref(href); if (path && (TabLocalFiles.ZipArchive.isArchivePath(path) || TabLocalFiles.ZipArchive.isVirtualPath(path))) { return (await TabLocalFiles.listDirectoryEntries(path)) || []; } const rawHtml = await TabLocalFiles.readCurrentFileAsText(href); return rawHtml ? readDirectoryEntriesFromHtml(rawHtml, href) : (await readDirectoryEntriesFromWorkspacePath(href)) || []; } async function readDirectoryEntriesFromWorkspacePath(href) { const path = decodedPathFromHref(href); if (!path) { return undefined; } return TabLocalFiles.listDirectoryEntries(path); } async function readDirectoryEntriesFromWorkspace(rootPath) { if (!TabLocalFiles.TabLocalWorkspace.isAvailable()) { return undefined; } let client; try { client = await TabLocalFiles.TabLocalWorkspace.createClient(pathToFileUrl(rootPath), { writable: false }); const entries = await client.listDirectory(TabLocalFiles.TabLocalWorkspace.ROOT_PATH, { maxEntries: TabLocalFiles.LargeFile.DIRECTORY_ENTRY_LIMIT + 1 }); return entries.map((entry) => workspaceEntryToDirectoryEntry(rootPath, entry)).sort(sortEntries); } catch { return undefined; } finally { if (client) { void client.release().catch(() => undefined); } } } function workspaceEntryToDirectoryEntry(rootPath, entry) { const relativePath = (entry.path || entry.name).replace(/^\/+/, ""); const root = normalizeDirectoryPath(rootPath); const fullPath = entry.kind === "directory" ? normalizeDirectoryPath(`${root}${relativePath}`) : `${root}${relativePath}`; const name = entry.kind === "directory" && !entry.name.endsWith("/") ? `${entry.name}/` : entry.name; return { name, href: pathToFileUrl(fullPath), kind: entry.kind, hidden: isHiddenEntryName(entry.name), size: Number.isFinite(entry.size) ? entry.size : undefined, mtime: Number.isFinite(entry.mtime) && entry.mtime > 0 ? entry.mtime : undefined }; } function readSnapshotForTreeNode(node) { const rawSnapshot = node.closest("[data-tlf-directory-snapshot]")?.dataset.tlfDirectorySnapshot; if (!rawSnapshot) { return undefined; } try { const snapshot = JSON.parse(rawSnapshot); return snapshot.rootPath && Array.isArray(snapshot.entries) ? snapshot : undefined; } catch { return undefined; } } function readCurrentPathForTreeNode(node) { return node.closest("[data-tlf-directory-current-path]")?.dataset.tlfDirectoryCurrentPath; } function fallbackSnapshotForHref(href, entries) { const rootPath = decodedPathFromHref(href) || "/"; return { rootName: fileNameFromDirectoryPath(rootPath), rootPath, entries }; } function renderDirectoryRootLink(snapshot) { const iconStyle = TabLocalFiles.fileIconMaskStyleForPath(snapshot.rootPath); return ` ${TabLocalFiles.escapeHtml(snapshot.rootName)} `; } function parentDirectoryPath(rootPath) { const trimmedPath = rootPath.replace(/\/+$/, ""); if (!trimmedPath || trimmedPath === "/") { return undefined; } const parent = trimmedPath.slice(0, trimmedPath.lastIndexOf("/") + 1); return parent || "/"; } function renderParentDirectoryRow(rootPath) { const parentPath = parentDirectoryPath(rootPath); if (!parentPath || TabLocalFiles.ZipArchive.isArchivePath(rootPath) || TabLocalFiles.ZipArchive.isVirtualPath(rootPath)) { return ""; } const label = TabLocalFiles.t("directory.backToParent"); const iconStyle = TabLocalFiles.fileIconMaskStyleForPath(parentPath); return ` `; } async function navigateSidebarToParentFolder(button) { const sidebar = button.closest("[data-tlf-directory-snapshot]"); const tree = sidebar?.querySelector(".tlf-directory-tree"); const snapshot = sidebar ? readSnapshotFromElement(sidebar) : undefined; if (!sidebar || !tree || !snapshot || sidebar.dataset.tlfDirectoryParentLoading === "true") { return; } const parentPath = parentDirectoryPath(snapshot.rootPath); if (!parentPath) { return; } const currentPath = sidebar.dataset.tlfDirectoryCurrentPath || snapshot.rootPath; const openPaths = collectOpenTreePaths(sidebar); sidebar.dataset.tlfDirectoryParentLoading = "true"; tree.innerHTML = renderParentDirectoryRow(snapshot.rootPath) + renderDirectoryTreeMessage("loading"); try { const entries = await directoryNodeEntries(pathToFileUrl(parentPath)); if (!document.body || !document.body.contains(sidebar)) { return; } const parentSnapshot = limitDirectorySnapshot({ rootName: fileNameFromDirectoryPath(parentPath), rootPath: parentPath, entries }); applySidebarRootEntries(sidebar, parentSnapshot.rootName, parentSnapshot.rootPath, currentPath, entries); const rootLink = sidebar.querySelector(".tlf-directory-root-link"); if (rootLink) { rootLink.outerHTML = renderDirectoryRootLink(parentSnapshot); } sidebar.querySelectorAll("[data-tlf-create-document-directory]").forEach((item) => { item.dataset.tlfCreateDocumentDirectory = parentSnapshot.rootPath; }); safeSessionSet(DIRECTORY_SIDEBAR_STORAGE_KEY, JSON.stringify(parentSnapshot)); void safeWriteExtensionSidebarSnapshot(parentSnapshot); void TabLocalFiles.DirectoryManage.refreshFavorites(); } catch { applySidebarRootEntries(sidebar, snapshot.rootName, snapshot.rootPath, currentPath, snapshot.entries); restoreOpenTreePaths(sidebar, openPaths); TabLocalFiles.showToast(TabLocalFiles.t("directory.readFailed"), "error"); } finally { delete sidebar.dataset.tlfDirectoryParentLoading; } } function selectedDirectoryPathForCurrentPath(currentPath) { return currentPath.endsWith("/") ? normalizeDirectoryPath(currentPath) : undefined; } function isSelectedDirectoryEntry(entryPath, currentPath) { if (!entryPath) { return false; } const selectedDirectoryPath = selectedDirectoryPathForCurrentPath(currentPath); return Boolean(selectedDirectoryPath && normalizeDirectoryPath(entryPath) === selectedDirectoryPath); } function isSelectedFileEntry(entryPath, currentPath) { return Boolean(entryPath && !currentPath.endsWith("/") && entryPath === currentPath); } function isSelectedArchiveNode(archivePath, currentPath) { return currentPath === archivePath; } function shouldAutoExpandArchiveNode(archivePath, currentPath) { return currentPath === archivePath || currentPath.startsWith(TabLocalFiles.ZipArchive.joinVirtualPath(archivePath, "")); } function shouldAutoExpandDirectoryEntry(entryPath, currentPath) { if (!entryPath) { return false; } const directoryPath = normalizeDirectoryPath(entryPath); return currentPath === directoryPath || currentPath.startsWith(directoryPath); } function normalizeDirectoryPath(pathname) { return pathname.endsWith("/") ? pathname : `${pathname}/`; } function pathToFileUrl(pathname) { return `file://${pathname.split("/").map(encodeURIComponent).join("/")}`; } function decodedPathFromHref(href) { try { return decodeURIComponent(new URL(href).pathname); } catch { return undefined; } } function fileNameFromDirectoryPath(pathname) { return pathname.replace(/\/+$/, "").split("/").filter(Boolean).pop() || TabLocalFiles.t("common.localFile"); } function entryHref(entry, snapshot) { try { const sourceUrl = new URL(entry.href).toString(); return TabLocalFiles.extensionViewerUrl(sourceUrl, snapshot); } catch { return entry.href; } } async function bindDirectoryWorkspace(context, initialEntries) { let entries = initialEntries; if (!entries.length) { const pathEntries = await readDirectoryEntriesFromWorkspacePath(context.sourceUrl); const listedEntries = pathEntries || (await readDirectoryEntriesFromWorkspace(context.decodedPath)); if (!TabLocalFiles.isCurrentDocumentMount(context.mountId)) { return; } if (listedEntries) { entries = listedEntries; refreshDirectorySidebar(context, entries); refreshDirectoryPreviewBody(context, entries); } else { showDirectorySidebarError(); } } storeSidebarSnapshot(context, entries); } function refreshDirectoryPreviewBody(context, entries) { const shell = document.getElementById(DIRECTORY_PREVIEW_SHELL_ID); if (!shell) { return; } shell.innerHTML = renderDirectoryPreviewOverview(context, entries); bindDirectoryPreviewActions(context); } function refreshDirectorySidebar(context, entries) { const sidebar = document.querySelector(".tlf-directory-sidebar[data-tlf-directory-snapshot]"); if (!sidebar) { return; } applySidebarRootEntries(sidebar, context.fileName, context.navigationRootPath || context.decodedPath, context.decodedPath, entries); } function applySidebarRootEntries(sidebar, rootName, rootPath, currentPath, entries) { const tree = sidebar.querySelector(".tlf-directory-tree"); if (!tree) { return; } const snapshot = limitDirectorySnapshot({ rootName, rootPath, entries }); const visibleEntries = visibleDirectoryEntries(snapshot.entries); const limited = entries.length > snapshot.entries.length; sidebar.dataset.tlfDirectorySnapshot = JSON.stringify(snapshot); sidebar.dataset.tlfDirectoryCurrentPath = currentPath; sidebar.dataset.tlfDirectoryLimited = String(limited); tree.innerHTML = renderParentDirectoryRow(snapshot.rootPath) + (visibleEntries.length ? visibleEntries.map((entry) => renderTreeEntry(entry, snapshot, currentPath)).join("") : renderDirectoryTreeMessage("empty")); if (limited) { tree.insertAdjacentHTML("beforeend", renderDirectoryLimitMessage()); } bindTreeControls(); } async function refreshSidebarDirectoryLevel(parentPath) { const sidebar = document.querySelector(".tlf-directory-sidebar[data-tlf-directory-snapshot]"); if (!sidebar) { return; } const snapshot = readSnapshotFromElement(sidebar); if (!snapshot) { return; } const tree = sidebar.querySelector(".tlf-directory-tree"); const scrollTop = tree?.scrollTop || 0; const restoreTreeScroll = () => { if (tree && scrollTop) { tree.scrollTop = scrollTop; } }; const normalizedParent = normalizeDirectoryPath(parentPath); if (normalizeDirectoryPath(snapshot.rootPath) === normalizedParent) { const entries = await directoryNodeEntries(pathToFileUrl(normalizedParent)); if (!document.body || !document.body.contains(sidebar)) { return; } const openPaths = collectOpenTreePaths(sidebar); applySidebarRootEntries(sidebar, snapshot.rootName, snapshot.rootPath, sidebar.dataset.tlfDirectoryCurrentPath || snapshot.rootPath, entries); restoreOpenTreePaths(sidebar, openPaths); restoreTreeScroll(); return; } const node = findTreeNodeForPath(sidebar, normalizedParent); const children = node?.querySelector("[data-tlf-directory-children]"); if (!node || !node.open || !children) { return; } const openPaths = collectOpenTreePaths(node); delete children.dataset.tlfDirectoryLoaded; await populateDirectoryNode(node); restoreOpenTreePaths(node, openPaths); restoreTreeScroll(); } Directory.refreshSidebarDirectoryLevel = refreshSidebarDirectoryLevel; function treeNodePath(node) { const entryPath = node.dataset.tlfDirectoryEntryPath; if (entryPath) { return normalizeDirectoryPath(entryPath); } const decoded = decodedPathFromHref(node.dataset.tlfDirectoryHref || ""); return decoded ? normalizeDirectoryPath(decoded) : undefined; } Directory.treeNodePath = treeNodePath; function findTreeNodeForPath(scope, normalizedPath) { return Array.from(scope.querySelectorAll("details[data-tlf-directory-node]")).find((node) => treeNodePath(node) === normalizedPath); } function collectOpenTreePaths(scope) { return Array.from(scope.querySelectorAll("details[data-tlf-directory-node][open]")) .map((node) => treeNodePath(node)) .filter((path) => Boolean(path)); } function restoreOpenTreePaths(scope, paths) { for (const path of paths) { const node = findTreeNodeForPath(scope, path); if (node && !node.open) { node.open = true; } } } function showDirectorySidebarError() { const tree = document.querySelector(".tlf-directory-sidebar .tlf-directory-tree"); if (tree) { tree.innerHTML = renderDirectoryTreeMessage("readFailed"); } } function storeSidebarSnapshot(context, entries) { const existingSnapshot = safeReadSidebarSnapshot(); if (existingSnapshot && context.navigationRootPath === existingSnapshot.rootPath && context.decodedPath !== existingSnapshot.rootPath) { return; } const snapshot = limitDirectorySnapshot({ rootName: context.fileName, rootPath: context.navigationRootPath || context.decodedPath, entries }); safeSessionSet(DIRECTORY_SIDEBAR_STORAGE_KEY, JSON.stringify(snapshot)); void safeWriteExtensionSidebarSnapshot(snapshot); } function readSidebarSnapshot() { const rawSnapshot = safeSessionGet(DIRECTORY_SIDEBAR_STORAGE_KEY); if (!rawSnapshot) { return undefined; } try { const snapshot = JSON.parse(rawSnapshot); if (!snapshot.rootPath || !Array.isArray(snapshot.entries)) { return undefined; } return snapshot; } catch { return undefined; } } function isInsideRoot(path, rootPath) { return path === rootPath || path.startsWith(rootPath); } function safeReadSidebarSnapshot() { try { return readSidebarSnapshot(); } catch { return undefined; } } function readSettings() { const sharedSettings = TabLocalFiles.Preferences.read().directory; if (sharedSettings) { return normalizeSettings(sharedSettings); } const legacySettings = safeLocalStorageGet(DIRECTORY_SETTINGS_STORAGE_KEY); if (!legacySettings) { return { ...DEFAULT_DIRECTORY_SETTINGS }; } try { return normalizeSettings(JSON.parse(legacySettings)); } catch { return { ...DEFAULT_DIRECTORY_SETTINGS }; } } Directory.readSettings = readSettings; function normalizeSettings(parsed) { return { showHiddenFiles: parsed.showHiddenFiles === true, foldersFirst: parsed.foldersFirst !== false, rememberSidebarWidth: parsed.rememberSidebarWidth !== false, sortKey: parsed.sortKey === "kind" ? parsed.sortKey : "name", sortDirection: parsed.sortDirection === "desc" ? "desc" : "asc", viewMode: parsed.viewMode === "grid" ? "grid" : "list" }; } function writeSettings(settings) { const nextSettings = { ...readSettings(), ...settings }; TabLocalFiles.Preferences.update({ directory: { ...nextSettings } }); return nextSettings; } Directory.writeSettings = writeSettings; function applySettingsToSidebars() { document.querySelectorAll("[data-tlf-directory-snapshot]").forEach((sidebar) => { const snapshot = readSnapshotFromElement(sidebar); const tree = sidebar.querySelector(".tlf-directory-tree"); if (!snapshot || !tree) { return; } const limitedSnapshot = limitDirectorySnapshot(snapshot); const visibleEntries = visibleDirectoryEntries(limitedSnapshot.entries); const currentPath = sidebar.dataset.tlfDirectoryCurrentPath || snapshot.rootPath; const limited = sidebar.dataset.tlfDirectoryLimited === "true"; tree.innerHTML = renderParentDirectoryRow(limitedSnapshot.rootPath) + (visibleEntries.length ? visibleEntries.map((entry) => renderTreeEntry(entry, limitedSnapshot, currentPath)).join("") : renderDirectoryTreeMessage("empty")); if (limited) { tree.insertAdjacentHTML("beforeend", renderDirectoryLimitMessage()); } bindTreeControls(); }); } Directory.applySettingsToSidebars = applySettingsToSidebars; function readSnapshotFromElement(element) { const rawSnapshot = element.dataset.tlfDirectorySnapshot; if (!rawSnapshot) { return undefined; } try { const snapshot = JSON.parse(rawSnapshot); return snapshot.rootPath && Array.isArray(snapshot.entries) ? snapshot : undefined; } catch { return undefined; } } function safeLocalStorageGet(key) { try { return window.localStorage.getItem(key); } catch { return null; } } function safeLocalStorageSet(key, value) { try { window.localStorage.setItem(key, value); } catch { } } function safeSessionSet(key, value) { try { window.sessionStorage.setItem(key, value); } catch { } } function safeSessionGet(key) { try { return window.sessionStorage.getItem(key); } catch { return null; } } async function safeReadExtensionSidebarSnapshot() { try { return await readExtensionSidebarSnapshot(); } catch { return undefined; } } async function safeWriteExtensionSidebarSnapshot(snapshot) { try { await writeExtensionSidebarSnapshot(snapshot); } catch { } } function writeExtensionSidebarSnapshot(snapshot) { const sessionStorageApi = chromeSessionStorage(); if (!sessionStorageApi?.set) { return Promise.resolve(); } const set = sessionStorageApi.set.bind(sessionStorageApi); return new Promise((resolve) => { set({ [DIRECTORY_SIDEBAR_STORAGE_KEY]: snapshot }, () => resolve()); }); } function readExtensionSidebarSnapshot() { const sessionStorageApi = chromeSessionStorage(); if (!sessionStorageApi?.get) { return Promise.resolve(undefined); } const get = sessionStorageApi.get.bind(sessionStorageApi); return new Promise((resolve) => { get(DIRECTORY_SIDEBAR_STORAGE_KEY, (result) => { const snapshot = result[DIRECTORY_SIDEBAR_STORAGE_KEY]; resolve(snapshot?.rootPath && Array.isArray(snapshot.entries) ? snapshot : undefined); }); }); } function chromeSessionStorage() { return globalThis.chrome?.storage?.session; } })(Directory = TabLocalFiles.Directory || (TabLocalFiles.Directory = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var DirectoryManage; (function (DirectoryManage) { const SIDEBAR_SELECTOR = ".tlf-directory-sidebar[data-tlf-directory-snapshot]"; const TREE_SELECTOR = ".tlf-directory-tree"; const FILE_ROW_SELECTOR = "a.tlf-directory-tree-file"; const NODE_SELECTOR = "details[data-tlf-directory-node]"; const RENAME_INPUT_SELECTOR = "[data-tlf-directory-rename-input]"; const ROW_MENU_BUTTON_SELECTOR = "[data-tlf-directory-row-menu]"; const ROW_MENU_OPEN_CLASS = "tlf-directory-tree-menu-open"; const FAVORITES_LIST_SELECTOR = "[data-tlf-directory-favorites-list]"; const FAVORITE_ROW_SELECTOR = "[data-tlf-directory-favorite-path]"; const DRAG_TYPE = "application/x-tlf-directory-entry"; const DRAG_EXPAND_DELAY_MS = 700; const DROP_TARGET_CLASS = "tlf-directory-tree-drop-target"; const DRAGGING_CLASS = "tlf-directory-tree-dragging"; function entryFromPath(path, kind) { if (!path || TabLocalFiles.ZipArchive.isVirtualPath(path)) { return undefined; } const normalized = kind === "directory" ? normalizeDirectoryPath(path) : path.replace(/\/+$/, ""); const { name } = TabLocalFiles.splitEntryPath(normalized); return name ? { path: normalized, name, kind } : undefined; } DirectoryManage.entryFromPath = entryFromPath; function entryFromRow(row) { const data = row.dataset || {}; if (data.tlfDirectoryArchivePath) { return entryFromPath(data.tlfDirectoryArchivePath, "file"); } if (data.tlfDirectoryNode !== undefined) { const path = TabLocalFiles.Directory.treeNodePath(row); return path ? entryFromPath(path, "directory") : undefined; } return data.tlfDirectoryEntryPath ? entryFromPath(data.tlfDirectoryEntryPath, "file") : undefined; } DirectoryManage.entryFromRow = entryFromRow; function canMoveInto(entry, targetDirectory) { const target = normalizeDirectoryPath(targetDirectory); if (TabLocalFiles.ZipArchive.isVirtualPath(target) || TabLocalFiles.splitEntryPath(entry.path).parentPath === target) { return false; } return !(entry.kind === "directory" && target.startsWith(entry.path)); } DirectoryManage.canMoveInto = canMoveInto; function pathAfterMove(path, fromPath, toPath) { if (path === fromPath) { return toPath; } if (fromPath.endsWith("/") && path.startsWith(fromPath)) { return `${normalizeDirectoryPath(toPath)}${path.slice(fromPath.length)}`; } return undefined; } DirectoryManage.pathAfterMove = pathAfterMove; function menuActionsFor(entry, favorite) { const actions = entry.kind === "directory" ? ["new-folder"] : []; actions.push("rename", favorite ? "unfavorite" : "favorite", "trash"); return actions; } DirectoryManage.menuActionsFor = menuActionsFor; function renderRowMenuButton() { return ``; } DirectoryManage.renderRowMenuButton = renderRowMenuButton; function invalidNameReason(name, siblingNames, currentName) { if (/[\\/:*?"<>|]/.test(name) || hasControlCharacters(name) || name === "." || name === "..") { return TabLocalFiles.t("rename.invalidName"); } const lowerName = name.toLowerCase(); const taken = siblingNames.some((sibling) => { const siblingName = sibling.replace(/\/+$/, ""); return siblingName !== currentName && siblingName.toLowerCase() === lowerName; }); return taken ? TabLocalFiles.t("rename.duplicateName") : undefined; } DirectoryManage.invalidNameReason = invalidNameReason; function favoriteFolders(entries) { return entries.filter((entry) => entry.kind === "directory"); } DirectoryManage.favoriteFolders = favoriteFolders; function renderFavoritesItems(folders, rootPath) { if (!folders.length) { return `

${TabLocalFiles.escapeHtml(TabLocalFiles.t("directory.favoritesEmpty"))}

`; } const root = normalizeDirectoryPath(rootPath); return folders .map((folder) => { const path = normalizeDirectoryPath(folder.path); const selected = path === root; const parentName = TabLocalFiles.splitEntryPath(TabLocalFiles.splitEntryPath(path).parentPath).name; const iconStyle = TabLocalFiles.fileIconMaskStyleForPath(path); return ` ${TabLocalFiles.escapeHtml(folder.name)} ${TabLocalFiles.escapeHtml(parentName)} `; }) .join(""); } DirectoryManage.renderFavoritesItems = renderFavoritesItems; let cachedFavorites; let favoritesListenerBound = false; function renderFavoritesMenu(rootPath) { const label = TabLocalFiles.t("directory.favoritesMenu"); return `
${TabLocalFiles.renderLucideIcon("star")}
`; } DirectoryManage.renderFavoritesMenu = renderFavoritesMenu; async function refreshFavorites() { cachedFavorites = await TabLocalFiles.FileBookmarks.readFavorites(); redrawFavorites(); } DirectoryManage.refreshFavorites = refreshFavorites; function redrawFavorites() { const folders = favoriteFolders(cachedFavorites || []); document.querySelectorAll(FAVORITES_LIST_SELECTOR).forEach((list) => { const sidebar = list.closest(SIDEBAR_SELECTOR); list.innerHTML = renderFavoritesItems(folders, (sidebar && sidebarRootPath(sidebar)) || ""); }); } function favoriteForEntry(entry) { return (cachedFavorites || []).find((favorite) => samePath(favorite.path, entry)); } function samePath(path, entry) { return entry.kind === "directory" ? normalizeDirectoryPath(path) === entry.path : path === entry.path; } async function setFavorite(entry, favorite) { const existing = (await TabLocalFiles.FileBookmarks.readFavorites()).find((item) => samePath(item.path, entry)); if (favorite === Boolean(existing)) { return; } const bookmark = existing || { url: pathToFileUrl(entry.path), path: entry.path, name: entry.name, kind: entry.kind, icon: entry.kind === "directory" ? "folder" : TabLocalFiles.fileIconNameForPath(entry.path), fileType: entry.kind === "directory" ? "directory" : TabLocalFiles.findPreviewableFileType(entry.path)?.id }; const starred = await TabLocalFiles.FileBookmarks.toggleFavorite(bookmark); TabLocalFiles.showToast(TabLocalFiles.t(starred ? "favorites.added" : "favorites.removed"), "success"); await refreshFavorites(); } let dragSource; let dragExpandNode; let dragExpandTimer; function bind() { document.querySelectorAll(SIDEBAR_SELECTOR).forEach((sidebar) => { if (sidebar.dataset.tlfDirectoryManageBound === "true") { return; } sidebar.dataset.tlfDirectoryManageBound = "true"; bindSidebar(sidebar); }); if (!favoritesListenerBound) { favoritesListenerBound = true; TabLocalFiles.FileBookmarks.onChange(() => { void refreshFavorites(); }); } void refreshFavorites(); } DirectoryManage.bind = bind; function bindSidebar(sidebar) { sidebar.addEventListener("click", (event) => handleRowMenuClick(sidebar, event), true); sidebar.addEventListener("click", (event) => handleSidebarClick(sidebar, event)); sidebar.addEventListener("keydown", (event) => handleRowKeyDown(sidebar, event)); sidebar.addEventListener("dragstart", (event) => handleDragStart(sidebar, event)); sidebar.addEventListener("dragover", (event) => handleDragOver(sidebar, event)); sidebar.addEventListener("dragleave", (event) => { const next = event.relatedTarget; if (!next || !sidebar.contains(next)) { setDropTarget(sidebar, undefined); } }); sidebar.addEventListener("drop", (event) => handleDrop(sidebar, event)); sidebar.addEventListener("dragend", () => endDrag(sidebar)); } function handleSidebarClick(sidebar, event) { const target = event.target; if (!target?.closest) { return; } const newFolder = target.closest("[data-tlf-directory-new-folder]"); if (newFolder) { newFolder.closest("details")?.removeAttribute("open"); const root = sidebarRootPath(sidebar); if (root) { void createFolder(sidebar, root); } return; } const favorite = target.closest(FAVORITE_ROW_SELECTOR); if (favorite && TabLocalFiles.InlineViewer.isPlainPrimaryClick(event)) { event.preventDefault(); favorite.closest("details")?.removeAttribute("open"); void TabLocalFiles.EditSession.requestNavigation().then((allowed) => { if (allowed) { window.location.href = favorite.href; } }); } } function rowHitFromTarget(target) { const row = target?.closest?.(`${FILE_ROW_SELECTOR}, ${NODE_SELECTOR} > summary`); if (!row) { return undefined; } const owner = row.matches(FILE_ROW_SELECTOR) ? row : row.parentElement; const entry = owner ? entryFromRow(owner) : undefined; return owner && entry ? { owner, row, entry } : null; } function handleRowMenuClick(sidebar, event) { const button = event.target?.closest?.(ROW_MENU_BUTTON_SELECTOR); if (!button) { return; } event.preventDefault(); event.stopPropagation(); const hit = rowHitFromTarget(button); if (!hit) { return; } if (openMenuRow === hit.row) { closeOpenMenu?.(); return; } openRowMenu(sidebar, hit); } function runRowAction(sidebar, hit, action) { switch (action) { case "new-folder": void createFolder(sidebar, hit.entry.path); return; case "rename": startRename(sidebar, hit.owner, hit.entry); return; case "favorite": case "unfavorite": void setFavorite(hit.entry, action === "favorite"); return; case "trash": void trashEntry(hit.entry, sidebar); return; } } function handleRowKeyDown(sidebar, event) { const target = event.target; if (!target?.closest || target.closest(RENAME_INPUT_SELECTOR)) { return; } const hit = rowHitFromTarget(target); if (!hit || hit.row !== target) { return; } const noModifiers = !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey; const shiftOnly = event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey; if ((event.key === "ContextMenu" && noModifiers) || (event.key === "F10" && shiftOnly)) { event.preventDefault(); event.stopPropagation(); openRowMenu(sidebar, hit); return; } if (event.key === "F2" && noModifiers) { event.preventDefault(); event.stopPropagation(); startRename(sidebar, hit.owner, hit.entry); return; } const macTrashChord = event.key === "Backspace" && event.metaKey && !event.altKey && !event.ctrlKey && !event.shiftKey; if ((event.key === "Delete" && noModifiers) || macTrashChord) { event.preventDefault(); event.stopPropagation(); void trashEntry(hit.entry, sidebar); } } let closeOpenMenu; let openMenuRow; function openRowMenu(sidebar, hit) { closeOpenMenu?.(); const anchor = hit.row.querySelector(`:scope > ${ROW_MENU_BUTTON_SELECTOR}`); if (!anchor) { return; } const actions = menuActionsFor(hit.entry, Boolean(favoriteForEntry(hit.entry))); const menu = document.createElement("div"); menu.className = "tlf-directory-context-menu"; menu.setAttribute("role", "menu"); menu.setAttribute("aria-label", TabLocalFiles.t("common.fileActions")); menu.setAttribute("translate", "no"); menu.innerHTML = actions.map((action) => renderMenuItem(action)).join(""); document.body.append(menu); hit.row.classList.add(ROW_MENU_OPEN_CLASS); openMenuRow = hit.row; const rect = anchor.getBoundingClientRect(); const margin = 8; const gap = 4; const below = rect.bottom + gap; const top = below + menu.offsetHeight + margin <= window.innerHeight ? below : rect.top - gap - menu.offsetHeight; menu.style.left = `${Math.max(margin, Math.min(rect.left, window.innerWidth - menu.offsetWidth - margin))}px`; menu.style.top = `${Math.max(margin, top)}px`; const items = Array.from(menu.querySelectorAll("[data-tlf-directory-menu-action]")); const close = () => { if (closeOpenMenu !== close) { return; } closeOpenMenu = undefined; openMenuRow = undefined; hit.row.classList.remove(ROW_MENU_OPEN_CLASS); menu.remove(); document.removeEventListener("pointerdown", onOutsidePointer, true); window.removeEventListener("blur", close); window.removeEventListener("resize", close); document.removeEventListener("scroll", close, true); }; const onOutsidePointer = (pointerEvent) => { const target = pointerEvent.target; if (!menu.contains(target) && !anchor.contains(target)) { close(); } }; menu.addEventListener("click", (clickEvent) => { const button = clickEvent.target?.closest("[data-tlf-directory-menu-action]"); const action = button?.dataset.tlfDirectoryMenuAction; if (!action) { return; } close(); runRowAction(sidebar, hit, action); }); menu.addEventListener("contextmenu", (menuEvent) => menuEvent.preventDefault()); menu.addEventListener("keydown", (keyEvent) => { const index = items.indexOf(document.activeElement); if (keyEvent.key === "Escape" || keyEvent.key === "Tab") { keyEvent.preventDefault(); close(); hit.row.focus(); } else if (keyEvent.key === "ArrowDown" || keyEvent.key === "ArrowUp") { keyEvent.preventDefault(); const step = keyEvent.key === "ArrowDown" ? 1 : -1; items[(index + step + items.length) % items.length]?.focus(); } }); document.addEventListener("pointerdown", onOutsidePointer, true); window.addEventListener("blur", close); window.addEventListener("resize", close); document.addEventListener("scroll", close, true); closeOpenMenu = close; items[0]?.focus(); } function renderMenuItem(action) { const place = TabLocalFiles.trashPlaceLabel(); const items = { "new-folder": { label: TabLocalFiles.t("directory.newFolder"), icon: "folder-plus" }, rename: { label: TabLocalFiles.t("directory.rename"), icon: "pencil" }, favorite: { label: TabLocalFiles.t("favorites.add"), icon: "star" }, unfavorite: { label: TabLocalFiles.t("favorites.remove"), icon: "star" }, trash: { label: TabLocalFiles.t("trash.action", { place }), icon: "trash-2" } }; const item = items[action]; return ` `; } function handleDragStart(sidebar, event) { const hit = rowHitFromTarget(event.target); if (!hit || !event.dataTransfer) { return; } if (sidebar.dataset.tlfDirectoryBusy === "true") { event.preventDefault(); return; } dragSource = hit.entry; event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData(DRAG_TYPE, hit.entry.path); hit.row.classList.add(DRAGGING_CLASS); sidebar.dataset.tlfDirectoryBusy = "true"; } function handleDragOver(sidebar, event) { if (!dragSource) { return; } const target = dropTargetFor(sidebar, event.target); if (!target || !canMoveInto(dragSource, target.directory)) { setDropTarget(sidebar, undefined); return; } event.preventDefault(); if (event.dataTransfer) { event.dataTransfer.dropEffect = "move"; } setDropTarget(sidebar, target); } function handleDrop(sidebar, event) { const entry = dragSource; const target = entry ? dropTargetFor(sidebar, event.target) : undefined; endDrag(sidebar); if (!entry || !target || !canMoveInto(entry, target.directory)) { return; } event.preventDefault(); void moveEntry(sidebar, entry, target.directory); } function endDrag(sidebar) { dragSource = undefined; setDropTarget(sidebar, undefined); sidebar.querySelectorAll(`.${DRAGGING_CLASS}`).forEach((row) => row.classList.remove(DRAGGING_CLASS)); delete sidebar.dataset.tlfDirectoryBusy; } function dropTargetFor(sidebar, target) { const tree = target?.closest?.(TREE_SELECTOR); if (!target || !tree || target.closest(".tlf-directory-tree-parent")) { return undefined; } const node = target.closest(NODE_SELECTOR); if (node) { const entry = node.dataset.tlfDirectoryArchivePath ? undefined : entryFromRow(node); const summary = node.querySelector(":scope > summary"); return entry?.kind === "directory" && summary ? { directory: entry.path, highlight: summary, node } : undefined; } const root = sidebarRootPath(sidebar); if (!root || TabLocalFiles.ZipArchive.isVirtualPath(root) || TabLocalFiles.ZipArchive.isArchivePath(root.replace(/\/+$/, ""))) { return undefined; } return { directory: root, highlight: tree }; } function setDropTarget(sidebar, target) { sidebar.querySelectorAll(`.${DROP_TARGET_CLASS}`).forEach((element) => { if (element !== target?.highlight) { element.classList.remove(DROP_TARGET_CLASS); } }); target?.highlight.classList.add(DROP_TARGET_CLASS); const node = target?.node && !target.node.open ? target.node : undefined; if (node === dragExpandNode) { return; } if (dragExpandTimer !== undefined) { window.clearTimeout(dragExpandTimer); dragExpandTimer = undefined; } dragExpandNode = node; if (node) { dragExpandTimer = window.setTimeout(() => { dragExpandTimer = undefined; dragExpandNode = undefined; if (dragSource && node.isConnected) { node.open = true; } }, DRAG_EXPAND_DELAY_MS); } } async function moveEntry(sidebar, entry, targetDirectory) { const currentPath = sidebar.dataset.tlfDirectoryCurrentPath || ""; const movesOpenDocument = pathAfterMove(currentPath, entry.path, entry.path) !== undefined; if (movesOpenDocument && !(await TabLocalFiles.EditSession.requestNavigation())) { return; } const folderName = TabLocalFiles.splitEntryPath(targetDirectory).name || targetDirectory; const result = await TabLocalFiles.moveEntryToDirectory(entry.path, targetDirectory); if (!result.ok || !result.path) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "move", result: "failed", failureStage: "move" }); TabLocalFiles.showToast(moveErrorMessage(result.error, folderName), "error"); return; } TabLocalFiles.recordCurrentFileOperationResult({ operation: "move", result: "success" }); await followInBookmarks(entry.path, result.path); const nextDocumentPath = pathAfterMove(currentPath, entry.path, result.path); if (nextDocumentPath) { openPath(nextDocumentPath, sidebar); return; } TabLocalFiles.showToast(TabLocalFiles.t("directory.moved", { folder: folderName }), "success"); await refreshDirectories([TabLocalFiles.splitEntryPath(entry.path).parentPath, targetDirectory]); } function moveErrorMessage(error, folderName) { if (error === "Destination already exists") { return TabLocalFiles.t("directory.moveExists", { folder: folderName }); } return TabLocalFiles.t("directory.moveFailed", { error: error || TabLocalFiles.t("common.unknownError") }); } async function renameEntry(sidebar, entry, newName) { const currentPath = sidebar.dataset.tlfDirectoryCurrentPath || ""; const renamesOpenDocument = pathAfterMove(currentPath, entry.path, entry.path) !== undefined; if (renamesOpenDocument && !(await TabLocalFiles.EditSession.requestNavigation())) { return false; } const result = await TabLocalFiles.renameEntry(entry.path, newName); if (!result.ok || !result.path) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "rename", result: "failed", failureStage: "rename" }); const message = result.error === "Destination file type is not allowed" ? TabLocalFiles.t("directory.extensionLocked") : TabLocalFiles.t("rename.failed", { error: result.error || TabLocalFiles.t("common.unknownError") }); TabLocalFiles.showToast(message, "error"); return false; } TabLocalFiles.recordCurrentFileOperationResult({ operation: "rename", result: "success" }); await followInBookmarks(entry.path, result.path); const nextDocumentPath = pathAfterMove(currentPath, entry.path, result.path); if (nextDocumentPath) { openPath(nextDocumentPath, sidebar); return true; } await refreshDirectories([TabLocalFiles.splitEntryPath(entry.path).parentPath]); focusRow(sidebar, result.path); return true; } async function createFolder(sidebar, directory) { const result = await TabLocalFiles.createUniqueDirectory(directory, TabLocalFiles.t("directory.newFolderName")); if (!result.ok || !result.path) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "create_folder", result: "failed", failureStage: "create" }); TabLocalFiles.showToast(TabLocalFiles.t("directory.newFolderFailed", { error: result.error || TabLocalFiles.t("common.unknownError") }), "error"); return; } TabLocalFiles.recordCurrentFileOperationResult({ operation: "create_folder", result: "success" }); const node = await showTreeEntry(sidebar, directory, result.path); await refreshOverview(directory); const entry = entryFromPath(result.path, "directory"); if (node && entry) { startRename(sidebar, node, entry); } } async function trashEntry(entry, sidebar) { const place = TabLocalFiles.trashPlaceLabel(); if (!(await TabLocalFiles.EditSession.confirmMoveToTrash(entry.name, entry.kind === "directory", place))) { return; } const result = await TabLocalFiles.moveEntryToTrash(entry.path); if (!result.ok) { TabLocalFiles.recordCurrentFileOperationResult({ operation: "trash", result: "failed", failureStage: "trash" }); TabLocalFiles.showToast(TabLocalFiles.t("trash.failed", { place, error: result.error || TabLocalFiles.t("common.unknownError") }), "error"); return; } TabLocalFiles.recordCurrentFileOperationResult({ operation: "trash", result: "success" }); TabLocalFiles.showToast(TabLocalFiles.t("trash.done", { place }), "success"); await followInBookmarks(entry.path, undefined); const parentPath = TabLocalFiles.splitEntryPath(entry.path).parentPath; const currentPath = sidebar?.dataset.tlfDirectoryCurrentPath ?? documentPath(); if (currentPath && pathAfterMove(currentPath, entry.path, entry.path) !== undefined) { TabLocalFiles.EditSession.abandonActive(); openPath(parentPath, sidebar); return; } await refreshDirectories([parentPath]); } DirectoryManage.trashEntry = trashEntry; function trashOpenDocument(path, isDirectory) { const entry = entryFromPath(path, isDirectory ? "directory" : "file"); if (!entry) { return Promise.resolve(); } return trashEntry(entry, document.querySelector(SIDEBAR_SELECTOR) || undefined); } DirectoryManage.trashOpenDocument = trashOpenDocument; function documentPath() { return document.querySelector("[data-tlf-file-toolbar]")?.dataset.tlfPath; } function openPath(path, sidebar) { const root = sidebar ? sidebarRootPath(sidebar) : undefined; const snapshot = sidebar && root && path.startsWith(root) ? readSidebarSnapshot(sidebar) : undefined; window.location.href = TabLocalFiles.extensionViewerUrl(pathToFileUrl(path), snapshot); } async function refreshDirectories(paths) { for (const path of Array.from(new Set(paths.map((item) => normalizeDirectoryPath(item))))) { await TabLocalFiles.Directory.refreshSidebarDirectoryLevel(path); await refreshOverview(path); } } async function refreshOverview(path) { const entries = await TabLocalFiles.listDirectoryEntries(normalizeDirectoryPath(path)); if (entries) { TabLocalFiles.Directory.refreshDirectoryOverview(path, entries); } } async function followInBookmarks(fromPath, toPath) { await TabLocalFiles.FileBookmarks.relocate(fromPath, toPath); await refreshFavorites(); } function focusRow(sidebar, path) { const row = path.endsWith("/") ? findNode(sidebar, path)?.querySelector(":scope > summary") : Array.from(sidebar.querySelectorAll(FILE_ROW_SELECTOR)).find((link) => link.dataset.tlfDirectoryEntryPath === path); row?.focus(); } async function showTreeEntry(sidebar, directory, entryPath) { const normalized = normalizeDirectoryPath(directory); if (normalized !== sidebarRootPath(sidebar)) { const node = findNode(sidebar, normalized); if (!node) { return undefined; } node.open = true; } await TabLocalFiles.Directory.refreshSidebarDirectoryLevel(normalized); return findNode(sidebar, entryPath); } function findNode(sidebar, directoryPath) { return Array.from(sidebar.querySelectorAll(NODE_SELECTOR)).find((node) => !node.dataset.tlfDirectoryArchivePath && TabLocalFiles.Directory.treeNodePath(node) === directoryPath); } function startRename(sidebar, owner, entry) { const row = owner.matches(NODE_SELECTOR) ? owner.querySelector(":scope > summary") : owner; const label = row?.querySelector(".tlf-directory-tree-label"); if (!row || !label || row.querySelector(RENAME_INPUT_SELECTOR)) { return; } const siblingNames = Array.from(owner.parentElement?.children || []) .map((sibling) => sibling.querySelector(".tlf-directory-tree-label")?.textContent || "") .filter(Boolean); const input = document.createElement("input"); input.type = "text"; input.className = "tlf-directory-tree-rename-input"; input.value = entry.name; input.spellcheck = false; input.autocomplete = "off"; input.setAttribute("aria-label", TabLocalFiles.t("directory.renameAria")); input.dataset.tlfDirectoryRenameInput = ""; const previousDraggable = row.getAttribute("draggable"); row.setAttribute("draggable", "false"); label.hidden = true; label.after(input); sidebar.dataset.tlfDirectoryBusy = "true"; input.focus(); const extension = entry.kind === "file" ? extensionOf(entry.name) : ""; input.setSelectionRange(0, extension && extension.length < entry.name.length ? entry.name.length - extension.length : entry.name.length); let state = "editing"; const finish = (refocus) => { if (state === "done") { return; } state = "done"; input.remove(); label.hidden = false; if (previousDraggable === null) { row.removeAttribute("draggable"); } else { row.setAttribute("draggable", previousDraggable); } delete sidebar.dataset.tlfDirectoryBusy; if (refocus && row.isConnected) { row.focus(); } }; const resumeEditing = () => { state = "editing"; input.disabled = false; input.focus(); input.select(); }; const commit = async () => { if (state !== "editing") { return; } const nextName = input.value.trim(); if (!nextName || nextName === entry.name) { finish(true); return; } const reason = invalidNameReason(nextName, siblingNames, entry.name); if (reason) { TabLocalFiles.showToast(reason, "error"); resumeEditing(); return; } state = "saving"; input.disabled = true; const nextExtension = extensionOf(nextName); if (entry.kind === "file" && nextExtension !== extensionOf(entry.name) && !(await TabLocalFiles.EditSession.confirmExtensionChange(nextName, nextExtension))) { resumeEditing(); return; } if (await renameEntry(sidebar, entry, nextName)) { finish(false); } else { resumeEditing(); } }; input.addEventListener("keydown", (event) => { event.stopPropagation(); if (event.key === "Enter") { event.preventDefault(); void commit(); } else if (event.key === "Escape") { event.preventDefault(); finish(true); } }); input.addEventListener("keyup", (event) => { if (event.key === " ") { event.preventDefault(); } }); input.addEventListener("blur", () => { window.setTimeout(() => void commit(), 0); }); for (const type of ["click", "dblclick", "mousedown", "pointerdown", "contextmenu"]) { input.addEventListener(type, (event) => { event.stopPropagation(); if (type === "click" || type === "dblclick") { event.preventDefault(); } }); } } function sidebarRootPath(sidebar) { const rootPath = readSidebarSnapshot(sidebar)?.rootPath; return rootPath ? normalizeDirectoryPath(rootPath) : undefined; } function readSidebarSnapshot(sidebar) { try { const snapshot = JSON.parse(sidebar.dataset.tlfDirectorySnapshot || ""); return snapshot?.rootPath ? snapshot : undefined; } catch { return undefined; } } function normalizeDirectoryPath(path) { return path.endsWith("/") ? path : `${path}/`; } function pathToFileUrl(pathname) { return `file://${pathname.split("/").map(encodeURIComponent).join("/")}`; } function extensionOf(name) { const match = name.toLowerCase().match(/\.[^.]+$/); return match && match[0] !== name.toLowerCase() ? match[0] : ""; } function hasControlCharacters(value) { for (let index = 0; index < value.length; index += 1) { const code = value.charCodeAt(index); if (code < 32 || code === 127) { return true; } } return false; } })(DirectoryManage = TabLocalFiles.DirectoryManage || (TabLocalFiles.DirectoryManage = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownSplitScroll; (function (MarkdownSplitScroll) { const ZERO_WIDTH_SPACE = "\u200b"; const ACTIVATING_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown", "focusin"]; const MIRRORED_STYLE_PROPERTIES = [ "fontFamily", "fontSize", "fontStyle", "fontWeight", "fontVariant", "fontFeatureSettings", "letterSpacing", "wordSpacing", "lineHeight", "tabSize", "textTransform", "textIndent", "whiteSpace", "overflowWrap", "wordBreak", "direction", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight" ]; function mapScroll(pairs, value) { if (!pairs.length) { return value; } if (value <= pairs[0][0]) { return pairs[0][1]; } const last = pairs[pairs.length - 1]; if (value >= last[0]) { return last[1]; } let low = 0; let high = pairs.length - 1; while (high - low > 1) { const middle = (low + high) >> 1; if (pairs[middle][0] <= value) { low = middle; } else { high = middle; } } const [sourceStart, targetStart] = pairs[low]; const [sourceEnd, targetEnd] = pairs[high]; if (sourceEnd === sourceStart) { return targetStart; } return targetStart + ((value - sourceStart) / (sourceEnd - sourceStart)) * (targetEnd - targetStart); } MarkdownSplitScroll.mapScroll = mapScroll; function reversePairs(pairs) { return pairs.map(([source, target]) => [target, source]); } MarkdownSplitScroll.reversePairs = reversePairs; function scrollPairs(anchors, sourceEnd, targetEnd) { const pairs = [[0, 0]]; for (const anchor of anchors) { const [lastSource, lastTarget] = pairs[pairs.length - 1]; if (anchor.source > lastSource && anchor.target > lastTarget && anchor.source < sourceEnd && anchor.target < targetEnd) { pairs.push([anchor.source, anchor.target]); } } pairs.push([Math.max(0, sourceEnd), Math.max(0, targetEnd)]); return pairs; } MarkdownSplitScroll.scrollPairs = scrollPairs; function mirrorChunks(text, lines) { const all = text.split("\n"); const starts = lines.map((line) => Math.min(Math.max(0, line), all.length)); const chunk = (from, to) => { if (to <= from) { return ""; } const body = all.slice(from, to).join("\n"); return body === "" || body.endsWith("\n") ? `${body}${ZERO_WIDTH_SPACE}` : body; }; const chunks = [chunk(0, starts.length ? starts[0] : all.length)]; starts.forEach((start, index) => { chunks.push(chunk(start, index + 1 < starts.length ? starts[index + 1] : all.length)); }); return chunks; } MarkdownSplitScroll.mirrorChunks = mirrorChunks; function measureLineTops(textarea, lines) { const style = window.getComputedStyle(textarea); const mirror = document.createElement("div"); MIRRORED_STYLE_PROPERTIES.forEach((property) => { mirror.style[property] = style[property]; }); const paddingX = (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0); Object.assign(mirror.style, { position: "absolute", top: "0", left: "-100000px", visibility: "hidden", boxSizing: "content-box", border: "0", margin: "0", overflow: "hidden", width: `${Math.max(0, textarea.clientWidth - paddingX)}px` }); mirror.setAttribute("aria-hidden", "true"); const blocks = mirrorChunks(textarea.value, lines).map((text) => { const block = document.createElement("div"); block.textContent = text; return block; }); mirror.append(...blocks); document.body.appendChild(mirror); try { return blocks.slice(1).map((block) => block.offsetTop); } finally { mirror.remove(); } } MarkdownSplitScroll.measureLineTops = measureLineTops; function bind(options) { const { textarea, host } = options; let active = document.activeElement && host.contains(document.activeElement) ? "target" : "source"; let lineTopsCache; const sourceLineTops = (lines) => { const key = lines.join(","); const cached = lineTopsCache; if (cached && cached.width === textarea.clientWidth && cached.key === key && cached.value === textarea.value) { return cached.tops; } const tops = measureLineTops(textarea, lines); lineTopsCache = { value: textarea.value, width: textarea.clientWidth, key, tops }; return tops; }; const pairs = () => { const anchors = []; for (const anchor of options.anchors()) { if (anchor.element.isConnected && (!anchors.length || anchor.line > anchors[anchors.length - 1].line)) { anchors.push(anchor); } } const sourceTops = sourceLineTops(anchors.map((anchor) => anchor.line)); const hostTop = host.getBoundingClientRect().top + host.clientTop; return scrollPairs(anchors.map((anchor, index) => ({ source: sourceTops[index], target: anchor.element.getBoundingClientRect().top - hostTop + host.scrollTop })), textarea.scrollHeight - textarea.clientHeight, host.scrollHeight - host.clientHeight); }; const follow = (from) => { if (!options.enabled()) { return; } const current = pairs(); if (from === "source") { host.scrollTop = mapScroll(current, textarea.scrollTop); } else { textarea.scrollTop = mapScroll(reversePairs(current), host.scrollTop); } }; const claimSource = () => { active = "source"; }; const claimTarget = () => { active = "target"; }; const onSourceScroll = () => { if (active === "source") { follow("source"); } }; const onTargetScroll = () => { if (active === "target") { follow("target"); } }; ACTIVATING_EVENTS.forEach((type) => { textarea.addEventListener(type, claimSource, { passive: true }); host.addEventListener(type, claimTarget, { passive: true }); }); textarea.addEventListener("scroll", onSourceScroll, { passive: true }); host.addEventListener("scroll", onTargetScroll, { passive: true }); return { sync: (from) => { if (from) { active = from; } follow(active); }, dispose: () => { ACTIVATING_EVENTS.forEach((type) => { textarea.removeEventListener(type, claimSource); host.removeEventListener(type, claimTarget); }); textarea.removeEventListener("scroll", onSourceScroll); host.removeEventListener("scroll", onTargetScroll); lineTopsCache = undefined; } }; } MarkdownSplitScroll.bind = bind; })(MarkdownSplitScroll = TabLocalFiles.MarkdownSplitScroll || (TabLocalFiles.MarkdownSplitScroll = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownInlineEdit; (function (MarkdownInlineEdit) { MarkdownInlineEdit.EDIT_VARIANT_STORAGE_KEY = "tab-local-files:markdown-edit-variant"; MarkdownInlineEdit.FRONTMATTER_PANEL_ID = "tlf-frontmatter-panel"; const VENDOR_MODULE_PATH = "vendor/markdown-inline-editor.js"; const EDIT_MODE_SEGMENT_ID = "markdown.editmode"; const SOURCE_ONLY_ACTIONS = ["markdown.task", "markdown.clear", "markdown.format-tables"]; const COMMAND_BY_ACTION = { "markdown.h1": { kind: "heading", level: 1 }, "markdown.h2": { kind: "heading", level: 2 }, "markdown.h3": { kind: "heading", level: 3 }, "markdown.h4": { kind: "heading", level: 4 }, "markdown.h5": { kind: "heading", level: 5 }, "markdown.bold": { kind: "strong" }, "markdown.italic": { kind: "emphasis" }, "markdown.strike": { kind: "strikethrough" }, "markdown.inline-code": { kind: "inlineCode", supportsEmptySelection: true }, "markdown.ul": { kind: "bulletList" }, "markdown.ol": { kind: "orderedList" }, "markdown.task": { kind: "taskList" }, "markdown.quote": { kind: "blockquote" }, "markdown.codeblock": { kind: "codeBlock" }, "markdown.clear": { kind: "clearFormat" }, "markdown.table": { kind: "table" }, "markdown.hr": { kind: "hr" }, "markdown.link": { kind: "linkTooltip" }, "markdown.image": { kind: "imageBlock" } }; function splitFrontmatter(source) { if (!/^---[ \t]*\r?\n/.test(source)) { return { frontmatter: "", body: source }; } const firstLineEnd = source.indexOf("\n") + 1; const closePattern = /^---[ \t]*(?:\r?\n|$)/m; let cursor = firstLineEnd; while (cursor <= source.length) { const rest = source.slice(cursor); const match = rest.match(closePattern); if (!match || match.index === undefined) { return { frontmatter: "", body: source }; } if (match.index === 0 || rest[match.index - 1] === "\n") { const end = cursor + match.index + match[0].length; return { frontmatter: source.slice(0, end), body: source.slice(end) }; } cursor += match.index + match[0].length; } return { frontmatter: "", body: source }; } MarkdownInlineEdit.splitFrontmatter = splitFrontmatter; function joinFrontmatter(frontmatter, body) { return `${frontmatter}${body}`; } MarkdownInlineEdit.joinFrontmatter = joinFrontmatter; function frontmatterInnerText(frontmatter) { if (!frontmatter) { return ""; } const lines = frontmatter.replace(/\r\n?/g, "\n").split("\n"); let end = lines.length - 1; while (end > 0 && lines[end].trim() === "") { end -= 1; } if (end > 0 && /^---[ \t]*$/.test(lines[end])) { end -= 1; } return lines.slice(1, end + 1).join("\n"); } MarkdownInlineEdit.frontmatterInnerText = frontmatterInnerText; function buildFrontmatterBlock(inner) { const trimmed = inner.replace(/\s+$/, ""); if (!trimmed.trim()) { return ""; } return `---\n${trimmed}\n---\n`; } MarkdownInlineEdit.buildFrontmatterBlock = buildFrontmatterBlock; const FRONTMATTER_KEY_RE = /^([A-Za-z0-9_.$-][^:]*):(?:[ \t]+(.*))?$/; function parseFrontmatterEntries(inner) { const lines = inner.replace(/\r\n?/g, "\n").split("\n"); const entries = []; lines.forEach((line, index) => { const isContinuation = /^[ \t]/.test(line) || line.trim() === ""; if (isContinuation) { const last = entries[entries.length - 1]; if (last && last.endLine === index - 1 && line.trim() !== "") { last.endLine = index; last.scalar = false; last.value = last.value ? `${last.value}\n${line}` : line; } return; } const match = FRONTMATTER_KEY_RE.exec(line); if (!match) { return; } entries.push({ key: match[1].trim(), value: unquoteScalar(match[2] ?? ""), scalar: true, startLine: index, endLine: index }); }); return entries; } MarkdownInlineEdit.parseFrontmatterEntries = parseFrontmatterEntries; function writeFrontmatterEntry(inner, index, next) { return spliceFrontmatterLines(inner, index, [formatFrontmatterLine(next.key, next.value)]); } MarkdownInlineEdit.writeFrontmatterEntry = writeFrontmatterEntry; function removeFrontmatterEntry(inner, index) { return spliceFrontmatterLines(inner, index, []); } MarkdownInlineEdit.removeFrontmatterEntry = removeFrontmatterEntry; function addFrontmatterEntry(inner, key, value) { const body = inner.replace(/\r\n?/g, "\n").replace(/\n+$/, ""); const line = formatFrontmatterLine(key, value); return body ? `${body}\n${line}\n` : `${line}\n`; } MarkdownInlineEdit.addFrontmatterEntry = addFrontmatterEntry; function spliceFrontmatterLines(inner, index, replacement) { const entries = parseFrontmatterEntries(inner); const entry = entries[index]; if (!entry) { return inner; } const lines = inner.replace(/\r\n?/g, "\n").split("\n"); const hadTrailingNewline = lines[lines.length - 1] === ""; if (hadTrailingNewline) { lines.pop(); } lines.splice(entry.startLine, entry.endLine - entry.startLine + 1, ...replacement); const body = lines.join("\n"); return body ? `${body}\n` : ""; } function formatFrontmatterLine(key, value) { const trimmedKey = key.trim() || "key"; const flat = value.replace(/\s+/g, " ").trim(); if (flat === "") { return `${trimmedKey}:`; } return `${trimmedKey}: ${needsQuoting(flat) ? quoteScalar(flat) : flat}`; } function needsQuoting(value) { return (value !== value.trim() || /:\s/.test(value) || /\s#/.test(value) || /^[#&*!|>%@`{}[\],]/.test(value) || /^(-|---|\.\.\.)$/.test(value) || /["']/.test(value) || value.includes("\n")); } function quoteScalar(value) { return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; } function unquoteScalar(value) { const trimmed = value.trim(); if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) { return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\"); } if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) { return trimmed.slice(1, -1).replace(/''/g, "'"); } return trimmed; } function normalizeEditVariant(value) { if (value === "source" || value === "split" || value === "inline") { return value; } if (value === "wysiwyg") { return "inline"; } return "inline"; } MarkdownInlineEdit.normalizeEditVariant = normalizeEditVariant; function inlineCommandForAction(action) { return COMMAND_BY_ACTION[action]; } MarkdownInlineEdit.inlineCommandForAction = inlineCommandForAction; function effectiveEditVariant(stored, forceSource) { return forceSource ? "source" : stored; } MarkdownInlineEdit.effectiveEditVariant = effectiveEditVariant; function readStoredEditVariant() { try { return normalizeEditVariant(window.localStorage.getItem(MarkdownInlineEdit.EDIT_VARIANT_STORAGE_KEY)); } catch { return normalizeEditVariant(undefined); } } MarkdownInlineEdit.readStoredEditVariant = readStoredEditVariant; function writeStoredEditVariant(variant) { try { window.localStorage.setItem(MarkdownInlineEdit.EDIT_VARIANT_STORAGE_KEY, variant); } catch { } } let activeController; function createFrontmatterPanel(controller) { const element = document.createElement("div"); element.id = MarkdownInlineEdit.FRONTMATTER_PANEL_ID; element.className = "tlf-frontmatter-panel"; element.setAttribute("data-tlf-frontmatter", ""); element.hidden = true; let expanded = true; const label = TabLocalFiles.escapeHtml(TabLocalFiles.t("markdown.frontmatter")); element.innerHTML = `
`; const toggle = element.querySelector(".tlf-frontmatter-toggle"); const rows = element.querySelector("[data-tlf-frontmatter-rows]"); const innerText = () => frontmatterInnerText(controller.frontmatter); const applyInner = (inner) => { if (controller.disposed) { return; } const next = buildFrontmatterBlock(inner); if (next === controller.frontmatter) { return; } controller.frontmatter = next; const bodyMarkdown = controller.instance?.getMarkdown() ?? splitFrontmatter(controller.lastSynced).body; pushMarkdownToTextarea(controller, bodyMarkdown); render(); }; const isEditable = () => TabLocalFiles.documentEditMode() === "edit" && controller.variant !== "source"; const setExpanded = (next) => { expanded = next; toggle?.classList.toggle("is-open", next); toggle?.setAttribute("aria-expanded", next ? "true" : "false"); if (rows) { rows.hidden = !next; } }; function renderRow(entry, index, editable) { const rowEditable = editable && entry.scalar; const valuePlaceholder = TabLocalFiles.escapeHtml(TabLocalFiles.t("markdown.frontmatterEmptyValue")); const keyCell = rowEditable ? `` : `${TabLocalFiles.escapeHtml(entry.key)}`; const valueCell = rowEditable ? `` : `${TabLocalFiles.escapeHtml(entry.value || TabLocalFiles.t("markdown.frontmatterEmptyValue"))}`; const remove = rowEditable ? `` : ""; return `
${keyCell} ${valueCell} ${remove}
`; } function render() { if (!rows) { return; } const editable = isEditable(); element.dataset.tlfFrontmatterEditable = editable ? "true" : "false"; const entries = parseFrontmatterEntries(innerText()); const addRow = editable ? `` : ""; rows.innerHTML = entries.map((entry, index) => renderRow(entry, index, editable)).join("") + addRow; rows.hidden = !expanded; rows.querySelectorAll("textarea.tlf-frontmatter-value").forEach(autoSizeValue); } function autoSizeValue(field) { field.style.height = "auto"; field.style.height = `${field.scrollHeight}px`; } toggle?.addEventListener("click", () => setExpanded(!expanded)); rows?.addEventListener("click", (event) => { const target = event.target; const remove = target?.closest("[data-tlf-frontmatter-remove]"); if (remove) { applyInner(removeFrontmatterEntry(innerText(), Number(remove.dataset.tlfFrontmatterRemove))); return; } if (target?.closest("[data-tlf-frontmatter-add]")) { applyInner(addFrontmatterEntry(innerText(), TabLocalFiles.t("markdown.frontmatterNewKey"), "")); const keys = rows.querySelectorAll("[data-tlf-frontmatter-key]"); const added = keys[keys.length - 1]; added?.focus(); added?.select(); } }); const commitField = (input) => { const index = Number(input.dataset.tlfFrontmatterKey ?? input.dataset.tlfFrontmatterValue); const inner = innerText(); const entry = parseFrontmatterEntries(inner)[index]; if (!entry) { return; } const next = input.dataset.tlfFrontmatterKey !== undefined ? { key: input.value, value: entry.value } : { key: entry.key, value: input.value }; applyInner(writeFrontmatterEntry(inner, index, next)); }; rows?.addEventListener("focusout", (event) => { const input = event.target?.closest("[data-tlf-frontmatter-key], [data-tlf-frontmatter-value]"); if (input) { commitField(input); } }); rows?.addEventListener("input", (event) => { const field = event.target?.closest("textarea.tlf-frontmatter-value"); if (field) { autoSizeValue(field); } }); rows?.addEventListener("keydown", (event) => { const input = event.target?.closest("[data-tlf-frontmatter-key], [data-tlf-frontmatter-value]"); if (!input) { return; } if (event.key === "Enter") { event.preventDefault(); commitField(input); input.blur(); } else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); render(); } }); const refresh = () => { const mode = TabLocalFiles.documentEditMode(); const targetHost = mode === "preview" ? controller.root.querySelector("[data-tlf-editor-preview]") : mode === "edit" && controller.variant !== "source" ? controller.host : null; if (targetHost && element.parentElement !== targetHost && typeof targetHost.prepend === "function") { targetHost.prepend(element); } const visible = Boolean(controller.frontmatter) && Boolean(targetHost); element.hidden = !visible; if (!visible) { return; } if (rows?.contains(document.activeElement)) { return; } render(); }; setExpanded(expanded); render(); return { element, refresh }; } function bind(options) { const root = document.getElementById(options.rootId); const textarea = root?.querySelector("[data-tlf-editor-textarea]"); const body = root?.querySelector("[data-tlf-editor-body]"); if (!root || !textarea || !body) { return; } const host = document.createElement("div"); host.className = "tlf-editor-inline tlf-split-block"; host.setAttribute("data-tlf-editor-inline", ""); host.setAttribute("translate", "no"); body.appendChild(host); const controller = { root, textarea, host, resolveImageUrl: options.resolveImageUrl, renderHtml: options.renderHtml, renderAlertShell: options.renderAlertShell, uploadImage: options.uploadImage, forceSourceVariant: Boolean(options.forceSourceVariant), variant: "source", createToken: 0, lastSynced: textarea.value, frontmatter: splitFrontmatter(textarea.value).frontmatter, disposed: false }; controller.frontmatterPanel = createFrontmatterPanel(controller); activeController = controller; controller.frontmatterPanel.refresh(); const preview = root.querySelector("[data-tlf-editor-preview]"); preview?.addEventListener("scroll", () => { controller.lastPreviewScrollRatio = scrollRatioOf(preview); }); ["source", "split", "inline"].forEach((variant) => { TabLocalFiles.onToolbarAction(`${EDIT_MODE_SEGMENT_ID}.${variant}`, () => setVariant(controller, variant, "user")); }); root.addEventListener("tlf:editor-mode-change", (event) => { const mode = event.detail?.mode; if (controller.disposed) { return; } controller.frontmatterPanel?.refresh(); if (controller.variant === "source") { return; } if (mode === "edit") { controller.pendingScrollRatio = controller.lastPreviewScrollRatio; void activate(controller); } else { syncFromInlineEditor(controller); releaseSurface(); } }); const unregisterBarrier = TabLocalFiles.TextEditor.registerAutoSaveBarrier(() => { syncFromInlineEditor(controller); }); controller.unsubscribeChanges = TabLocalFiles.TextEditor.externalChanges(options.rootId)?.onChange(() => { pushExternalMarks(controller); }); controller.splitScroll = TabLocalFiles.MarkdownSplitScroll.bind({ textarea, host, enabled: () => !controller.disposed && controller.variant === "split" && Boolean(controller.instance) && TabLocalFiles.isDocumentEditing(), anchors: () => splitScrollAnchors(controller) }); TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => { controller.disposed = true; unregisterBarrier(); controller.unsubscribeChanges?.(); controller.splitScroll?.dispose(); releaseSurface(); const instance = controller.instance; controller.instance = undefined; void instance?.destroy(); if (activeController === controller) { activeController = undefined; } }); if (controller.forceSourceVariant) { ["split", "inline"].forEach((key) => { const button = document.querySelector(`[data-tlf-toolbar-action="${EDIT_MODE_SEGMENT_ID}.${key}"]`); if (button) { button.disabled = true; button.title = TabLocalFiles.t("markdown.largeSourceOnly"); } }); } applyVariant(controller, effectiveEditVariant(readStoredEditVariant(), controller.forceSourceVariant)); bindSplitWidthAvailability(controller, options.mountId); prefetchEditorWhenIdle(controller); } MarkdownInlineEdit.bind = bind; const SPLIT_MIN_WORKSPACE_WIDTH = 620; function bindSplitWidthAvailability(controller, mountId) { const root = controller.root; if (!root || controller.forceSourceVariant || typeof ResizeObserver !== "function") { return; } const button = document.querySelector(`[data-tlf-toolbar-action="${EDIT_MODE_SEGMENT_ID}.split"]`); if (!button) { return; } const defaultTitle = button.title; const evaluate = () => { const tooNarrow = root.clientWidth > 0 && root.clientWidth < SPLIT_MIN_WORKSPACE_WIDTH; button.disabled = tooNarrow; button.title = tooNarrow ? TabLocalFiles.t("markdown.splitTooNarrow") : defaultTitle; if (tooNarrow && controller.variant === "split") { setVariant(controller, "inline", "auto"); } }; const observer = new ResizeObserver(evaluate); observer.observe(root); evaluate(); TabLocalFiles.registerDocumentMountCleanup(mountId, () => observer.disconnect()); } function prefetchEditorWhenIdle(controller) { if (controller.forceSourceVariant || controller.creating) { return; } const start = () => { if (!controller.disposed && !controller.creating) { void import(runtimeAssetUrl(VENDOR_MODULE_PATH)).catch(() => undefined); } }; const idle = window .requestIdleCallback; if (typeof idle === "function") { idle(start, { timeout: 2000 }); } else { window.setTimeout(start, 1000); } } function searchInline(query, options) { return activeController?.instance?.search(query, options); } MarkdownInlineEdit.searchInline = searchInline; function navigateInlineSearch(direction) { return activeController?.instance?.searchNavigate(direction); } MarkdownInlineEdit.navigateInlineSearch = navigateInlineSearch; function replaceInlineSearch(replacement) { return activeController?.instance?.searchReplace(replacement); } MarkdownInlineEdit.replaceInlineSearch = replaceInlineSearch; function replaceAllInlineSearch(replacement) { return activeController?.instance?.searchReplaceAll(replacement); } MarkdownInlineEdit.replaceAllInlineSearch = replaceAllInlineSearch; function clearInlineSearch() { activeController?.instance?.clearSearch(); } MarkdownInlineEdit.clearInlineSearch = clearInlineSearch; function onPreviewRendered(value) { const controller = activeController; if (!controller || controller.disposed) { return; } controller.frontmatterPanel?.refresh(); if (value === controller.lastSynced) { return; } if (!controller.instance) { controller.lastSynced = value; controller.frontmatter = splitFrontmatter(value).frontmatter; controller.frontmatterPanel?.refresh(); return; } replaceInstanceMarkdown(controller, controller.instance, value); } MarkdownInlineEdit.onPreviewRendered = onPreviewRendered; function replaceInstanceMarkdown(controller, instance, value) { controller.lastSynced = value; const split = splitFrontmatter(value); controller.frontmatter = split.frontmatter; controller.frontmatterPanel?.refresh(); instance.setMarkdown(split.body); controller.expectedEcho = joinFrontmatter(controller.frontmatter, instance.getMarkdown()); pushExternalMarks(controller); controller.splitScroll?.sync(); } function splitScrollAnchors(controller) { const instance = controller.instance; if (!instance) { return []; } const offset = controller.frontmatter ? controller.frontmatter.split("\n").length - 1 : 0; try { return instance.blockAnchors().map((anchor) => ({ line: anchor.start + offset, element: anchor.element })); } catch (error) { console.warn("[Previewer] Inline editor could not report its blocks for split scrolling.", error); return []; } } function pushExternalMarks(controller) { const instance = controller.instance; if (!instance || controller.disposed) { return; } const ledger = TabLocalFiles.TextEditor.externalChanges(controller.root.id); if (!ledger) { return; } const offset = controller.frontmatter ? controller.frontmatter.split("\n").length - 1 : 0; const marks = []; for (const entry of ledger.entries()) { const start = entry.start - offset; const end = entry.end - offset; if (entry.kind === "del" ? start < 0 : end <= 0) { continue; } marks.push({ id: entry.id, kind: entry.kind, start: Math.max(0, start), end: Math.max(0, end) }); } try { instance.setExternalMarks(marks); } catch (error) { console.warn("[Previewer] Inline editor rejected external change marks.", error); } TabLocalFiles.ChangeMarks.refresh(controller.root.id); } function runToolbarAction(action) { const controller = activeController; if (!controller || controller.disposed || controller.variant === "source" || !controller.instance) { return false; } if (!TabLocalFiles.isDocumentEditing()) { return false; } if (controller.variant === "split" && !controller.instance.hasFocus()) { return false; } const spec = inlineCommandForAction(action); if (spec) { controller.instance.runCommand(spec); controller.instance.focus(); } return true; } MarkdownInlineEdit.runToolbarAction = runToolbarAction; function setVariant(controller, variant, trigger) { if (controller.disposed || controller.variant === variant) { return; } writeStoredEditVariant(variant); if (trigger === "user") { void TabLocalFiles.recordCurrentFileAnalyticsEvent({ type: "click", name: "markdown_edit_variant_change", value: "markdown-edit-variant", triggerMethod: "manual_click", subModuleName: "editor", fields: { variant } }); } applyVariant(controller, variant); } function scrollRatioOf(scroller) { const range = scroller.scrollHeight - scroller.clientHeight; if (range <= 0) { return undefined; } return Math.min(1, Math.max(0, scroller.scrollTop / range)); } MarkdownInlineEdit.scrollRatioOf = scrollRatioOf; function applyScrollRatio(scroller, ratio) { const range = scroller.scrollHeight - scroller.clientHeight; if (range > 0) { scroller.scrollTop = ratio * range; } } MarkdownInlineEdit.applyScrollRatio = applyScrollRatio; function applyVariant(controller, variant) { variant = effectiveEditVariant(variant, controller.forceSourceVariant); const previousVariant = controller.variant; controller.variant = variant; const editing = TabLocalFiles.isDocumentEditing(); const inlineBecomesVisible = editing && previousVariant === "source" && variant !== "source"; const textareaBecomesVisible = editing && previousVariant === "inline" && variant !== "inline"; const splitFrom = variant !== "split" ? undefined : inlineBecomesVisible ? "source" : textareaBecomesVisible ? "target" : undefined; const carriedRatio = splitFrom ? undefined : inlineBecomesVisible ? scrollRatioOf(controller.textarea) : textareaBecomesVisible ? scrollRatioOf(controller.host) : undefined; controller.pendingSplitSync = splitFrom; controller.root.dataset.tlfEditVariant = variant; TabLocalFiles.syncSegmentedToolbar(EDIT_MODE_SEGMENT_ID, variant); if (textareaBecomesVisible && carriedRatio !== undefined) { window.requestAnimationFrame?.(() => applyScrollRatio(controller.textarea, carriedRatio)); } if (variant !== "source") { if (inlineBecomesVisible) { controller.pendingScrollRatio = carriedRatio; } if (TabLocalFiles.isDocumentEditing()) { void activate(controller); } return; } controller.frontmatterPanel?.refresh(); syncFromInlineEditor(controller); releaseSurface(); controller.createToken += 1; const instance = controller.instance; controller.instance = undefined; void instance?.destroy(); controller.host.replaceChildren(); delete controller.host.dataset.tlfInlineState; if (previousVariant !== "source" && TabLocalFiles.isDocumentEditing()) { controller.textarea.focus(); } } async function activate(controller) { if (controller.disposed) { return; } if (controller.instance) { attachSurface(controller); if (controller.textarea.value !== controller.lastSynced) { replaceInstanceMarkdown(controller, controller.instance, controller.textarea.value); } controller.frontmatterPanel?.refresh(); consumePendingScrollRatio(controller); if (controller.variant === "inline") { controller.instance.focus(); } return; } if (controller.creating) { return controller.creating.then(() => { if (!controller.disposed && controller.variant !== "source" && !controller.creating) { return activate(controller); } return undefined; }); } const token = (controller.createToken += 1); controller.host.dataset.tlfInlineState = "loading"; const loading = document.createElement("div"); loading.className = "tlf-inline-loading"; loading.textContent = TabLocalFiles.t("markdown.inlineLoading"); controller.host.replaceChildren(loading); const value = controller.textarea.value; controller.lastSynced = value; const split = splitFrontmatter(value); controller.frontmatter = split.frontmatter; controller.frontmatterPanel?.refresh(); controller.creating = TabLocalFiles.VendorLoader.ensureKatex() .then(() => import(runtimeAssetUrl(VENDOR_MODULE_PATH))) .then((module) => module.createTabMarkdownInlineEditor({ root: controller.host, markdown: split.body, onMarkdownChange: (markdown) => { if (!controller.disposed) { pushMarkdownToTextarea(controller, markdown); } }, onFocusChange: (focused) => { if (focused && !controller.disposed && controller.instance && controller.textarea.value !== controller.lastSynced) { replaceInstanceMarkdown(controller, controller.instance, controller.textarea.value); } }, resolveImageUrl: controller.resolveImageUrl, uploadImage: controller.uploadImage, renderDiagram: (code) => TabLocalFiles.Markdown.renderDiagramHtml(code, controller.host.querySelector(".milkdown")?.clientWidth), renderMath: (source, displayMode) => TabLocalFiles.Markdown.renderMathHtml(source, displayMode), renderHtml: controller.renderHtml, renderSvgBlock: (code) => TabLocalFiles.Markdown.renderSvgBlockHtml(code), renderAlertShell: controller.renderAlertShell, labels: inlineEditorLabels() })) .then((handle) => { if (controller.disposed || token !== controller.createToken || controller.variant === "source") { void handle.destroy(); return; } loading.remove(); controller.instance = handle; controller.expectedEcho = joinFrontmatter(controller.frontmatter, handle.getMarkdown()); if (controller.textarea.value !== controller.lastSynced) { replaceInstanceMarkdown(controller, handle, controller.textarea.value); } controller.host.dataset.tlfInlineState = "ready"; pushExternalMarks(controller); if (TabLocalFiles.isDocumentEditing()) { attachSurface(controller); consumePendingScrollRatio(controller); if (controller.variant === "inline") { handle.focus(); } } }) .catch((error) => { console.warn("[Previewer] Markdown inline editor failed to load.", error); if (controller.disposed || token !== controller.createToken) { return; } delete controller.host.dataset.tlfInlineState; controller.host.replaceChildren(); TabLocalFiles.showToast(TabLocalFiles.t("markdown.inlineLoadFailed"), "error"); setVariant(controller, "source", "auto"); }) .finally(() => { controller.creating = undefined; }); return controller.creating; } function consumePendingScrollRatio(controller) { const ratio = controller.pendingScrollRatio; controller.pendingScrollRatio = undefined; if (ratio === undefined) { if (controller.variant === "split") { const from = controller.pendingSplitSync; controller.pendingSplitSync = undefined; window.requestAnimationFrame?.(() => controller.splitScroll?.sync(from)); } return; } window.requestAnimationFrame?.(() => { if (!controller.disposed) { applyScrollRatio(controller.host, ratio); if (controller.variant === "split") { controller.splitScroll?.sync("target"); } } }); } function attachSurface(controller) { TabLocalFiles.TextEditor.setActiveEditingSurface({ rootId: controller.root.id, applyChatEdits: (edits, _text, dryRun) => { const instance = controller.instance; if (!instance) throw new Error("TARGET_UNAVAILABLE"); const offset = controller.frontmatter.length; if (edits.some((edit) => edit.start < offset)) throw new Error("FRONTMATTER_SOURCE_EDIT_REQUIRED"); const body = instance.applyChatEdits(edits.map((edit) => ({ ...edit, start: edit.start - offset, end: edit.end - offset })), splitFrontmatter(controller.textarea.value).body, dryRun); const value = joinFrontmatter(controller.frontmatter, body); if (!dryRun) { controller.lastSynced = value; controller.expectedEcho = value; } return value; }, exclusive: controller.variant === "inline", hasFocus: () => controller.instance?.hasFocus() ?? false, runHistory: (command) => { controller.instance?.runHistory(command); controller.instance?.focus(); }, canHistory: (command) => controller.instance?.canHistory(command) ?? false }); TabLocalFiles.syncEditActionAvailability(); } function releaseSurface() { TabLocalFiles.TextEditor.setActiveEditingSurface(undefined); TabLocalFiles.syncEditActionAvailability(); } function captureChatSelection() { const controller = activeController; if (!controller?.instance || controller.variant === "source" || !controller.instance.hasFocus()) return undefined; syncFromInlineEditor(controller); const range = controller.instance.getChatSelection(splitFrontmatter(controller.textarea.value).body); if (!range) return undefined; const offset = controller.frontmatter.length; const context = TabLocalFiles.TextEditor.captureChatContext(controller.root.id, range.start + offset, range.end + offset, "inline", "selection"); return context && { rootId: controller.root.id, context }; } MarkdownInlineEdit.captureChatSelection = captureChatSelection; function syncFromInlineEditor(controller) { if (controller.disposed || !controller.instance) { return; } pushMarkdownToTextarea(controller, controller.instance.getMarkdown()); } function evaluateInlineSync(full, lastSynced, expectedEcho) { if (expectedEcho !== undefined && full === expectedEcho) { return { apply: false, expectedEcho }; } if (full === lastSynced) { return { apply: false }; } return { apply: true }; } MarkdownInlineEdit.evaluateInlineSync = evaluateInlineSync; function pushMarkdownToTextarea(controller, markdown) { const full = joinFrontmatter(controller.frontmatter, markdown); const decision = evaluateInlineSync(full, controller.lastSynced, controller.expectedEcho); controller.expectedEcho = decision.expectedEcho; if (!decision.apply) { return; } controller.lastSynced = full; if (controller.textarea.value !== full) { const { selectionStart, selectionEnd, scrollTop, scrollLeft } = controller.textarea; controller.textarea.value = full; try { controller.textarea.setSelectionRange(selectionStart, selectionEnd); } catch { } controller.textarea.scrollTop = scrollTop; controller.textarea.scrollLeft = scrollLeft; controller.textarea.dispatchEvent(new Event("input", { bubbles: true })); } } function inlineEditorLabels() { const t = TabLocalFiles.t; return { placeholder: t("inline.placeholder"), codeCopy: t("common.copy"), codeSearchLanguage: t("inline.codeSearchLanguage"), codeNoResult: t("inline.codeNoResult"), codePreviewLabel: t("inline.codePreview"), codePreviewLoading: t("inline.codePreviewLoading"), codePreviewHide: t("inline.codeHideCode"), codePreviewEdit: t("common.edit"), linkPlaceholder: t("inline.linkPlaceholder"), linkTextPlaceholder: t("markdown.insertLink"), imageUrlPlaceholder: t("inline.imageUrlPlaceholder"), taskToggle: t("markdown.taskToggle") }; } function runtimeAssetUrl(path) { const extensionUrl = globalThis.chrome?.runtime?.getURL?.(path); return extensionUrl || new URL(path, document.baseURI).toString(); } })(MarkdownInlineEdit = TabLocalFiles.MarkdownInlineEdit || (TabLocalFiles.MarkdownInlineEdit = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownImagePaste; (function (MarkdownImagePaste) { MarkdownImagePaste.ASSETS_DIRECTORY = ".assets"; const IMAGE_MIME_EXTENSIONS = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "image/bmp": "bmp", "image/avif": "avif", "image/svg+xml": "svg" }; function imageExtensionForMime(mime) { return IMAGE_MIME_EXTENSIONS[(mime || "").toLowerCase()]; } MarkdownImagePaste.imageExtensionForMime = imageExtensionForMime; function buildAssetFileName(extension, now, randomSuffix) { const pad = (value) => String(value).padStart(2, "0"); const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; return `image-${stamp}-${randomSuffix}.${extension}`; } MarkdownImagePaste.buildAssetFileName = buildAssetFileName; function documentDirectoryPath(decodedPath) { const slashIndex = decodedPath.lastIndexOf("/"); return slashIndex >= 0 ? decodedPath.slice(0, slashIndex + 1) : "/"; } MarkdownImagePaste.documentDirectoryPath = documentDirectoryPath; function markdownImageSnippet(relativePaths) { return relativePaths.map((path) => `![](${path})`).join("\n"); } MarkdownImagePaste.markdownImageSnippet = markdownImageSnippet; function extractImageFiles(data) { if (!data) { return []; } const images = []; const items = data.items; if (items && items.length) { for (let index = 0; index < items.length; index += 1) { const item = items[index]; if (item.kind === "file" && /^image\//i.test(item.type)) { const file = item.getAsFile(); if (file) { images.push(file); } } } if (images.length) { return images; } } const files = data.files; if (files && files.length) { for (let index = 0; index < files.length; index += 1) { const file = files[index]; if (/^image\//i.test(file.type)) { images.push(file); } } } return images; } MarkdownImagePaste.extractImageFiles = extractImageFiles; function randomSuffix() { return Math.random().toString(36).slice(2, 6).padEnd(4, "0"); } function extensionForFile(file) { const byMime = imageExtensionForMime(file.type); if (byMime) { return byMime; } const dot = (file.name || "").lastIndexOf("."); const byName = dot > 0 ? (file.name || "").slice(dot + 1).toLowerCase() : ""; return /^[a-z0-9]{1,5}$/.test(byName) ? byName : "png"; } async function saveImageToAssets(decodedPath, file) { const directory = documentDirectoryPath(decodedPath); const extension = extensionForFile(file); const bytes = new Uint8Array(await file.arrayBuffer()); let lastError; for (let attempt = 0; attempt < 2; attempt += 1) { const fileName = buildAssetFileName(extension, new Date(), randomSuffix()); try { await TabLocalFiles.writeBinaryIntoSubdirectory(directory, MarkdownImagePaste.ASSETS_DIRECTORY, fileName, bytes); return `./${MarkdownImagePaste.ASSETS_DIRECTORY}/${fileName}`; } catch (error) { lastError = error; } } throw lastError; } MarkdownImagePaste.saveImageToAssets = saveImageToAssets; function bind(options) { const root = document.getElementById(options.rootId); const textarea = root?.querySelector("[data-tlf-editor-textarea]"); if (!root || !textarea || typeof textarea.addEventListener !== "function") { return; } const capture = (files) => { void captureImages(options.decodedPath, files, textarea); }; const onPaste = (event) => { const files = extractImageFiles(event.clipboardData); if (!files.length) { return; } event.preventDefault(); capture(files); }; const onDragOver = (event) => { if (extractImageFiles(event.dataTransfer).length || hasImageFileHint(event.dataTransfer)) { event.preventDefault(); } }; const onDrop = (event) => { const files = extractImageFiles(event.dataTransfer); if (!files.length) { return; } event.preventDefault(); event.stopPropagation(); capture(files); }; textarea.addEventListener("paste", onPaste); textarea.addEventListener("dragover", onDragOver); textarea.addEventListener("drop", onDrop); TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => { textarea.removeEventListener("paste", onPaste); textarea.removeEventListener("dragover", onDragOver); textarea.removeEventListener("drop", onDrop); }); } MarkdownImagePaste.bind = bind; function hasImageFileHint(data) { if (!data || !data.items) { return false; } for (let index = 0; index < data.items.length; index += 1) { const item = data.items[index]; if (item.kind === "file" && (/^image\//i.test(item.type) || item.type === "")) { return true; } } return false; } async function captureImages(decodedPath, files, textarea) { const saved = []; let failed = 0; for (const file of files) { try { saved.push(await saveImageToAssets(decodedPath, file)); } catch { failed += 1; } } if (saved.length) { const start = textarea.selectionStart ?? textarea.value.length; const end = textarea.selectionEnd ?? start; const snippet = markdownImageSnippet(saved); TabLocalFiles.TextEditor.applyUndoableEdit(textarea, { replacement: snippet, rangeStart: start, rangeEnd: end, selectionStart: start + snippet.length, selectionEnd: start + snippet.length }); } if (failed) { TabLocalFiles.showToast(TabLocalFiles.t("markdown.imagePasteFailed"), "error"); } } })(MarkdownImagePaste = TabLocalFiles.MarkdownImagePaste || (TabLocalFiles.MarkdownImagePaste = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownSmartPaste; (function (MarkdownSmartPaste) { MarkdownSmartPaste.MAX_HTML_BYTES = 100000; const STRUCTURAL_TAG_PATTERN = /<(a|img|table|h[1-6]|ul|ol|blockquote|pre|code|strong|b|em|i|mark|del|s)\b/i; const PLAIN_PASTE_WINDOW_MS = 500; function isPastedUrl(text) { return /^https?:\/\/\S+$/i.test((text || "").trim()); } MarkdownSmartPaste.isPastedUrl = isPastedUrl; function isSubstantialHtml(html) { if (!html || html.length > MarkdownSmartPaste.MAX_HTML_BYTES) { return false; } return STRUCTURAL_TAG_PATTERN.test(html); } MarkdownSmartPaste.isSubstantialHtml = isSubstantialHtml; const VENDOR_MODULE_PATH = "vendor/markdown-smart-paste.js"; let vendorModule; function loadVendor() { if (!vendorModule) { vendorModule = import(runtimeAssetUrl(VENDOR_MODULE_PATH)); vendorModule.catch(() => { vendorModule = undefined; }); } return vendorModule; } function runtimeAssetUrl(path) { const extensionUrl = globalThis.chrome?.runtime?.getURL?.(path); return extensionUrl || new URL(path, document.baseURI).toString(); } function bind(options) { const root = document.getElementById(options.rootId); const textarea = root?.querySelector("[data-tlf-editor-textarea]"); if (!root || !textarea || typeof textarea.addEventListener !== "function") { return; } let plainPasteUntil = 0; const onKeyDown = (event) => { if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === "v") { plainPasteUntil = Date.now() + PLAIN_PASTE_WINDOW_MS; } }; const onPaste = (event) => { const data = event.clipboardData; if (!data || data.files.length) { return; } if (Date.now() < plainPasteUntil) { return; } const plain = data.getData("text/plain"); const start = textarea.selectionStart ?? 0; const end = textarea.selectionEnd ?? 0; if (end > start && isPastedUrl(plain)) { const selected = textarea.value.slice(start, end); if (!selected.includes("\n")) { event.preventDefault(); const snippet = `[${selected}](${plain.trim()})`; TabLocalFiles.TextEditor.applyUndoableEdit(textarea, { replacement: snippet, rangeStart: start, rangeEnd: end, selectionStart: start + snippet.length, selectionEnd: start + snippet.length }); return; } } const html = data.getData("text/html"); if (!isSubstantialHtml(html)) { return; } event.preventDefault(); void loadVendor() .then((module) => insertAtSelection(textarea, module.convertHtmlToMarkdown(html).trim() || plain)) .catch(() => insertAtSelection(textarea, plain)); }; textarea.addEventListener("keydown", onKeyDown); textarea.addEventListener("paste", onPaste); TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => { textarea.removeEventListener("keydown", onKeyDown); textarea.removeEventListener("paste", onPaste); }); } MarkdownSmartPaste.bind = bind; function insertAtSelection(textarea, text) { if (!text) { return; } const start = textarea.selectionStart ?? textarea.value.length; const end = textarea.selectionEnd ?? start; TabLocalFiles.TextEditor.applyUndoableEdit(textarea, { replacement: text, rangeStart: start, rangeEnd: end, selectionStart: start + text.length, selectionEnd: start + text.length }); } })(MarkdownSmartPaste = TabLocalFiles.MarkdownSmartPaste || (TabLocalFiles.MarkdownSmartPaste = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownCodeCopy; (function (MarkdownCodeCopy) { const COPIED_FEEDBACK_MS = 1600; const PREVIEW_PRE_SELECTOR = "[data-tlf-editor-preview] pre"; const WRAPPER_CLASS = "tlf-code-block"; const DIAGRAM_ANCESTORS = ["[data-tlf-mermaid-diagram]", "[data-tlf-markmap-diagram]"]; function decorate(root) { if (!root || typeof root.querySelectorAll !== "function") { return 0; } let added = 0; Array.from(root.querySelectorAll(PREVIEW_PRE_SELECTOR)).forEach((pre) => { if (!isCopyableCodeBlock(pre)) { return; } const owner = pre.ownerDocument ?? (typeof document === "undefined" ? null : document); const button = createCopyButton(owner); const wrapper = createWrapper(owner, pre); if (!button || !wrapper) { return; } pre.dataset.tlfCodeCopyDecorated = "true"; wrapper.appendChild(button); added += 1; }); return added; } MarkdownCodeCopy.decorate = decorate; function codeTextFor(pre) { const code = pre?.querySelector(":scope > code") ?? pre?.querySelector("code") ?? null; return code?.textContent ?? ""; } MarkdownCodeCopy.codeTextFor = codeTextFor; function selectionTouchesCode(selection) { if (!selection) { return false; } return isInsideCode(selection.anchorNode) || isInsideCode(selection.focusNode); } MarkdownCodeCopy.selectionTouchesCode = selectionTouchesCode; function isInsideCode(node) { return Boolean(node && typeof node.closest === "function" && node.closest("pre")); } function isCopyableCodeBlock(pre) { if (pre.dataset.tlfCodeCopyDecorated === "true") { return false; } if (typeof pre.closest === "function" && DIAGRAM_ANCESTORS.some((selector) => pre.closest(selector))) { return false; } const code = pre.querySelector(":scope > code"); return Boolean(code); } function createCopyButton(owner) { if (!owner || typeof owner.createElement !== "function") { return null; } const button = owner.createElement("button"); button.type = "button"; button.className = "tlf-code-copy"; button.dataset.tlfCodeCopy = "true"; clearCopiedFeedback(button); return button; } function createWrapper(owner, pre) { const parent = pre.parentNode; if (!owner || typeof owner.createElement !== "function" || !parent) { return null; } const wrapper = owner.createElement("div"); wrapper.className = WRAPPER_CLASS; parent.replaceChild(wrapper, pre); wrapper.appendChild(pre); return wrapper; } function showCopiedFeedback(button) { const label = TabLocalFiles.t("markdown.codeCopied"); button.classList.add("tlf-code-copy-done"); button.setAttribute("aria-label", label); button.setAttribute("title", label); button.innerHTML = TabLocalFiles.renderLucideIcon("check"); } MarkdownCodeCopy.showCopiedFeedback = showCopiedFeedback; function clearCopiedFeedback(button) { const label = TabLocalFiles.t("common.copy"); button.classList.remove("tlf-code-copy-done"); button.setAttribute("aria-label", label); button.setAttribute("title", label); button.innerHTML = TabLocalFiles.renderLucideIcon("copy"); } MarkdownCodeCopy.clearCopiedFeedback = clearCopiedFeedback; function bind(options) { const root = document.getElementById(options.rootId); if (!root || typeof root.addEventListener !== "function" || typeof document.addEventListener !== "function") { return; } const onClick = (event) => { const target = event.target; const button = target?.closest?.("[data-tlf-code-copy]"); if (!button || button.tagName !== "BUTTON") { return; } event.preventDefault(); void copyFromButton(button); }; const onSelectionChange = () => { const hide = selectionTouchesCode(document.getSelection()); root.classList.toggle("tlf-selecting-code", hide); }; root.addEventListener("click", onClick); document.addEventListener("selectionchange", onSelectionChange); TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => { root.removeEventListener("click", onClick); document.removeEventListener("selectionchange", onSelectionChange); }); } MarkdownCodeCopy.bind = bind; function blockFor(button) { const wrapper = button?.closest?.(`.${WRAPPER_CLASS}`); return wrapper?.querySelector?.("pre") ?? null; } MarkdownCodeCopy.blockFor = blockFor; async function copyFromButton(button) { const code = codeTextFor(blockFor(button)); if (!code) { return; } try { await TabLocalFiles.copyPlainTextToClipboard(code); } catch (error) { console.error("[Previewer] Copying a code block failed.", error); TabLocalFiles.showToast(TabLocalFiles.t("common.copyFailed"), "error"); return; } const pending = copiedTimers.get(button); if (pending !== undefined) { window.clearTimeout(pending); } showCopiedFeedback(button); copiedTimers.set(button, window.setTimeout(() => { copiedTimers.delete(button); clearCopiedFeedback(button); }, COPIED_FEEDBACK_MS)); } const copiedTimers = new WeakMap(); })(MarkdownCodeCopy = TabLocalFiles.MarkdownCodeCopy || (TabLocalFiles.MarkdownCodeCopy = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownCodeHighlight; (function (MarkdownCodeHighlight) { const VENDOR_MODULE_PATH = "vendor/markdown-code-highlight.js"; const CACHE_LIMIT = 256; const HIGHLIGHTED_MARKER = "tlfCodeHighlighted"; let vendorPromise; const cache = new Map(); function highlight(blocks) { const pending = blocks.filter((block) => { if (block.element.dataset[HIGHLIGHTED_MARKER] === "true" || !(block.element.textContent || "").trim()) { return false; } const cached = cache.get(cacheKey(block)); if (cached === undefined) { return true; } apply(block, block.element.textContent || "", cached); return false; }); if (!pending.length) { return Promise.resolve(); } return loadVendor().then(async (vendor) => { if (!vendor) { return; } for (const block of pending) { const text = block.element.textContent || ""; let html; try { html = (await vendor.highlightCode(text, block.language)) ?? null; } catch (error) { console.warn("[Previewer] Code highlighting failed; the block stays plain.", error); html = null; } remember(cacheKey(block), html); apply(block, text, html); } }); } MarkdownCodeHighlight.highlight = highlight; function apply(block, text, html) { if (html === null || !block.element.isConnected || block.element.textContent !== text) { return; } block.element.innerHTML = html; block.element.dataset[HIGHLIGHTED_MARKER] = "true"; } function cacheKey(block) { return `${block.language.toLowerCase()}\u0000${block.element.textContent || ""}`; } function remember(key, html) { cache.delete(key); cache.set(key, html); if (cache.size > CACHE_LIMIT) { cache.delete(cache.keys().next().value); } } function loadVendor() { vendorPromise ?? (vendorPromise = import(runtimeAssetUrl(VENDOR_MODULE_PATH)).catch((error) => { console.warn("[Previewer] Code highlighter failed to load; code blocks stay plain.", error); vendorPromise = undefined; return undefined; })); return vendorPromise; } function runtimeAssetUrl(path) { const extensionUrl = globalThis.chrome?.runtime?.getURL?.(path); return extensionUrl || new URL(path, document.baseURI).toString(); } })(MarkdownCodeHighlight = TabLocalFiles.MarkdownCodeHighlight || (TabLocalFiles.MarkdownCodeHighlight = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownRichCopy; (function (MarkdownRichCopy) { const INLINE_COPY_STYLES = { h1: "font-size:1.8em;margin:1em 0 .5em;padding-bottom:.3em;border-bottom:1px solid #eee;", h2: "font-size:1.4em;margin:1em 0 .5em;padding-bottom:.25em;border-bottom:1px solid #eee;", h3: "font-size:1.2em;margin:.8em 0 .4em;", h4: "margin:.8em 0 .4em;", h5: "margin:.8em 0 .4em;", h6: "margin:.8em 0 .4em;", p: "margin:.5em 0;line-height:1.75;", strong: "font-weight:600;", a: "color:#0969da;text-decoration:none;", code: "background:rgba(175,184,193,0.2);padding:2px 6px;border-radius:3px;font-size:.875em;font-family:Menlo,Monaco,monospace;", pre: "background:#f6f8fa;padding:16px;border-radius:6px;overflow-x:auto;margin:1em 0;", blockquote: "border-left:4px solid #ddd;padding-left:16px;margin:1em 0;color:#666;", ul: "padding-left:24px;margin:.5em 0;", ol: "padding-left:24px;margin:.5em 0;", li: "margin:.25em 0;", table: "border-collapse:collapse;width:100%;margin:1em 0;", th: "border:1px solid #ddd;padding:8px 12px;text-align:left;background:#f6f8fa;", td: "border:1px solid #ddd;padding:8px 12px;text-align:left;", hr: "border:none;border-top:2px solid #ddd;margin:2em 0;", img: "max-width:100%;", mark: "background:#fff3bf;padding:0 2px;border-radius:3px;", dl: "margin:1em 0;", dt: "font-weight:600;margin-top:.6em;", dd: "margin:.2em 0 .2em 1.5em;color:#666;", sub: "font-size:0.75em;line-height:0;", sup: "font-size:0.75em;line-height:0;", ins: "text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:2px;" }; const PRE_CODE_STYLE = "background:none;padding:0;font-size:.875em;line-height:1.6;font-family:Menlo,Monaco,monospace;"; function stripPreviewControls(container) { Array.from(container.querySelectorAll("[data-tlf-code-copy]")).forEach((node) => { node.remove?.(); }); } MarkdownRichCopy.stripPreviewControls = stripPreviewControls; function applyInlineCopyStyles(container) { for (const [tag, style] of Object.entries(INLINE_COPY_STYLES)) { const elements = container.querySelectorAll(tag); for (let index = 0; index < elements.length; index += 1) { elements[index].setAttribute("style", style); } } const preCode = container.querySelectorAll("pre code"); for (let index = 0; index < preCode.length; index += 1) { preCode[index].setAttribute("style", PRE_CODE_STYLE); } } MarkdownRichCopy.applyInlineCopyStyles = applyInlineCopyStyles; function styledCopyHtml(html) { const parsed = new DOMParser().parseFromString(html, "text/html"); applyInlineCopyStyles(parsed.body); return parsed.body.innerHTML; } function bind(options) { const root = document.getElementById(options.rootId); if (!root || typeof root.addEventListener !== "function") { return; } const preview = root.querySelector("[data-tlf-editor-preview]"); const inlineHost = root.querySelector("[data-tlf-editor-inline]"); const onCopyOrCut = (event) => { handleCopy(event, preview, inlineHost); }; root.addEventListener("copy", onCopyOrCut); root.addEventListener("cut", onCopyOrCut); TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => { root.removeEventListener("copy", onCopyOrCut); root.removeEventListener("cut", onCopyOrCut); }); } MarkdownRichCopy.bind = bind; function handleCopy(event, preview, inlineHost) { const target = event.target; if (!target || !event.clipboardData) { return; } if (inlineHost && inlineHost.contains(target)) { const html = event.clipboardData.getData("text/html"); if (html) { event.clipboardData.setData("text/html", styledCopyHtml(html)); return; } if (!event.defaultPrevented) { buildStyledSelectionPayload(event, inlineHost); } return; } if (preview && preview.contains(target)) { buildStyledSelectionPayload(event, preview); } } function buildStyledSelectionPayload(event, container) { const selection = window.getSelection(); if (!selection || selection.isCollapsed || selection.rangeCount === 0) { return; } if (!selection.anchorNode || !container.contains(selection.anchorNode) || !event.clipboardData) { return; } const holder = document.createElement("div"); holder.appendChild(selection.getRangeAt(0).cloneContents()); stripPreviewControls(holder); applyInlineCopyStyles(holder); event.preventDefault(); event.clipboardData.setData("text/html", holder.innerHTML); event.clipboardData.setData("text/plain", selection.toString()); } })(MarkdownRichCopy = TabLocalFiles.MarkdownRichCopy || (TabLocalFiles.MarkdownRichCopy = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownSearch; (function (MarkdownSearch) { MarkdownSearch.MAX_TEXT_MATCHES = 2000; function collectTextMatches(value, query, options) { const matches = []; if (!query) { return matches; } const caseSensitive = Boolean(options?.caseSensitive); const haystack = caseSensitive ? value : value.toLowerCase(); const needle = caseSensitive ? query : query.toLowerCase(); let index = haystack.indexOf(needle); while (index !== -1 && matches.length < MarkdownSearch.MAX_TEXT_MATCHES) { matches.push({ from: index, to: index + needle.length }); index = haystack.indexOf(needle, index + 1); } return matches; } MarkdownSearch.collectTextMatches = collectTextMatches; function collectReplaceRanges(value, query, options) { const ranges = []; if (!query) { return ranges; } const caseSensitive = Boolean(options?.caseSensitive); const haystack = caseSensitive ? value : value.toLowerCase(); const needle = caseSensitive ? query : query.toLowerCase(); let index = haystack.indexOf(needle); while (index !== -1) { ranges.push({ from: index, to: index + needle.length }); index = haystack.indexOf(needle, index + needle.length); } return ranges; } MarkdownSearch.collectReplaceRanges = collectReplaceRanges; function replaceAllText(value, query, replacement, options) { const ranges = collectReplaceRanges(value, query, options); if (!ranges.length) { return { text: value, count: 0 }; } const parts = []; let cursor = 0; for (const range of ranges) { parts.push(value.slice(cursor, range.from), replacement); cursor = range.to; } parts.push(value.slice(cursor)); return { text: parts.join(""), count: ranges.length }; } MarkdownSearch.replaceAllText = replaceAllText; function textareaScrollTopForOffset(value, offset, lineHeight, clientHeight) { let line = 0; const end = Math.min(offset, value.length); for (let index = 0; index < end; index += 1) { if (value.charCodeAt(index) === 10) { line += 1; } } return Math.max(0, line * lineHeight - clientHeight / 3); } MarkdownSearch.textareaScrollTopForOffset = textareaScrollTopForOffset; function createTextareaMatchLayer(textarea, lineHeight) { let layer; let mirror; let resizeObserver; let paintedMatches; let hits = []; const hitClass = (isCurrent) => (isCurrent ? "tlf-search-match tlf-search-match-current" : "tlf-search-match"); const syncScroll = () => { if (mirror) { mirror.style.transform = `translate(${-textarea.scrollLeft}px, ${-textarea.scrollTop}px)`; } }; const syncLayout = () => { if (!layer || !mirror) { return; } if (typeof window.getComputedStyle === "function") { const computed = window.getComputedStyle(textarea); TabLocalFiles.ChangeMarks.MIRRORED_STYLES.forEach((property) => { mirror.style[property] = computed[property]; }); } layer.style.width = `${textarea.clientWidth}px`; layer.style.height = `${textarea.clientHeight}px`; mirror.style.width = `${textarea.clientWidth}px`; syncScroll(); }; const ensureMirror = () => { const stack = textarea.parentElement; if (!stack) { return undefined; } if (!layer || !mirror) { layer = document.createElement("div"); layer.className = "tlf-editor-search-layer"; layer.setAttribute("aria-hidden", "true"); mirror = document.createElement("div"); mirror.className = "tlf-editor-search-mirror"; layer.appendChild(mirror); textarea.addEventListener("scroll", syncScroll); if (typeof ResizeObserver === "function") { resizeObserver = new ResizeObserver(() => syncLayout()); resizeObserver.observe(textarea); } } if (layer.parentElement !== stack) { stack.appendChild(layer); } return mirror; }; const reveal = (hit, match) => { const hitRect = hit?.getBoundingClientRect(); const mirrorRect = mirror?.getBoundingClientRect(); if (!hitRect || !mirrorRect || !hitRect.height) { textarea.scrollTop = textareaScrollTopForOffset(textarea.value, match.from, lineHeight, textarea.clientHeight); return; } const top = hitRect.top - mirrorRect.top; const left = hitRect.left - mirrorRect.left; if (top < textarea.scrollTop || top + hitRect.height > textarea.scrollTop + textarea.clientHeight) { textarea.scrollTop = Math.max(0, top - textarea.clientHeight / 3); } if (left < textarea.scrollLeft || left + hitRect.width > textarea.scrollLeft + textarea.clientWidth) { textarea.scrollLeft = Math.max(0, left - textarea.clientWidth / 3); } }; const clear = () => { textarea.removeEventListener("scroll", syncScroll); resizeObserver?.disconnect(); resizeObserver = undefined; layer?.remove(); layer = undefined; mirror = undefined; paintedMatches = undefined; hits = []; }; const paint = (matches, current, shouldReveal) => { if (!matches.length) { clear(); return; } const target = ensureMirror(); if (!target) { return; } syncLayout(); if (matches !== paintedMatches) { const value = textarea.value; const fragment = document.createDocumentFragment(); let cursor = 0; hits = matches.map((match) => { const from = Math.max(match.from, cursor); if (from >= match.to) { return undefined; } if (from > cursor) { fragment.appendChild(document.createTextNode(value.slice(cursor, from))); } const hit = document.createElement("span"); hit.textContent = value.slice(from, match.to); fragment.appendChild(hit); cursor = match.to; return hit; }); if (cursor < value.length) { fragment.appendChild(document.createTextNode(value.slice(cursor))); } target.replaceChildren(fragment); paintedMatches = matches; } hits.forEach((hit, index) => { if (hit) { hit.className = hitClass(index === current); } }); if (shouldReveal && current >= 0 && current < matches.length) { reveal(hits[current], matches[current]); } syncScroll(); }; return { paint, clear }; } MarkdownSearch.createTextareaMatchLayer = createTextareaMatchLayer; const TEXTAREA_LINE_HEIGHT = 24; const SEARCH_DEBOUNCE_MS = 150; let activeController; function bind(options) { const root = document.getElementById(options.rootId); const textarea = root?.querySelector("[data-tlf-editor-textarea]"); if (!root || !textarea || typeof document.addEventListener !== "function") { return; } const controller = { root, textarea, caseSensitive: false, matches: [], matchIndex: -1, matchLayer: createTextareaMatchLayer(textarea, TEXTAREA_LINE_HEIGHT), disposed: false }; activeController = controller; const onKeyDown = (event) => { handleGlobalKeydown(controller, event); }; document.addEventListener("keydown", onKeyDown); const onTextChange = () => { if (!isPanelOpen(controller) || usesInlineSurface(controller) || controller.debounceTimer !== undefined || textarea.value === controller.searchedText) { return; } controller.debounceTimer = window.setTimeout(() => { controller.debounceTimer = undefined; const current = controller.matches[controller.matchIndex]; refreshMatchesAfterEdit(controller, current ? current.from : 0, false); }, SEARCH_DEBOUNCE_MS); }; root.addEventListener("tlf:editor-state-change", onTextChange); const onModeChange = (event) => { const mode = event.detail?.mode; if (mode !== "edit") { close(controller); } }; root.addEventListener("tlf:editor-mode-change", onModeChange); TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => { controller.disposed = true; close(controller); document.removeEventListener("keydown", onKeyDown); root.removeEventListener("tlf:editor-state-change", onTextChange); root.removeEventListener("tlf:editor-mode-change", onModeChange); if (activeController === controller) { activeController = undefined; } }); } MarkdownSearch.bind = bind; function isEditing(controller) { return TabLocalFiles.isDocumentEditing(); } function usesInlineSurface(controller) { return controller.root.dataset.tlfEditVariant === "inline"; } function isPanelOpen(controller) { return Boolean(controller.panel && !controller.panel.hidden); } function handleGlobalKeydown(controller, event) { if (controller.disposed || TabLocalFiles.isTextCompositionEvent(event)) { return; } const mod = event.metaKey || event.ctrlKey; const key = event.key.toLowerCase(); if (mod && !event.altKey && !event.shiftKey && key === "f") { if (!isEditing(controller)) { return; } event.preventDefault(); open(controller); return; } if (!isPanelOpen(controller)) { return; } if (mod && key === "g") { event.preventDefault(); navigate(controller, event.shiftKey ? -1 : 1); return; } if (event.key === "Escape") { close(controller); } } function open(controller) { ensurePanel(controller); const panel = controller.panel; const input = controller.input; if (!panel || !input) { return; } panel.hidden = false; const { selectionStart, selectionEnd, value } = controller.textarea; if (selectionEnd > selectionStart) { const selected = value.slice(selectionStart, selectionEnd); if (selected.trim() && !selected.includes("\n")) { input.value = selected; } } input.focus(); input.select(); runSearch(controller); } function close(controller) { if (!controller.panel || controller.panel.hidden) { return; } const current = controller.panel.contains(document.activeElement) ? controller.matches[controller.matchIndex] : undefined; controller.panel.hidden = true; controller.matches = []; controller.matchIndex = -1; if (controller.debounceTimer !== undefined) { window.clearTimeout(controller.debounceTimer); controller.debounceTimer = undefined; } controller.matchLayer.clear(); TabLocalFiles.MarkdownInlineEdit.clearInlineSearch(); if (isEditing(controller) && !usesInlineSurface(controller)) { if (current) { try { controller.textarea.setSelectionRange(current.from, current.to); } catch { } } controller.textarea.focus(); } } function ensurePanel(controller) { if (controller.panel) { return; } const body = controller.root.querySelector("[data-tlf-editor-body]"); if (!body) { return; } const panel = document.createElement("div"); panel.className = "tlf-search-panel"; panel.setAttribute("data-tlf-search-panel", ""); panel.hidden = true; panel.innerHTML = `
0 / 0
`; body.appendChild(panel); const input = panel.querySelector("[data-tlf-search-input]"); const count = panel.querySelector("[data-tlf-search-count]"); controller.panel = panel; controller.input = input || undefined; controller.count = count || undefined; controller.replaceRow = panel.querySelector("[data-tlf-search-replace-row]") || undefined; controller.replaceInput = panel.querySelector("[data-tlf-search-replace-input]") || undefined; controller.toggleButton = panel.querySelector("[data-tlf-search-toggle]") || undefined; controller.caseButton = panel.querySelector("[data-tlf-search-case]") || undefined; input?.addEventListener("input", () => { if (controller.debounceTimer !== undefined) { window.clearTimeout(controller.debounceTimer); } controller.debounceTimer = window.setTimeout(() => { controller.debounceTimer = undefined; runSearch(controller); }, SEARCH_DEBOUNCE_MS); }); input?.addEventListener("keydown", (event) => { if (TabLocalFiles.isTextCompositionEvent(event)) { return; } if (event.key === "Enter") { event.preventDefault(); navigate(controller, event.shiftKey ? -1 : 1); } else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(controller); } }); controller.replaceInput?.addEventListener("keydown", (event) => { if (TabLocalFiles.isTextCompositionEvent(event)) { return; } if (event.key === "Enter") { event.preventDefault(); replaceCurrent(controller); } else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(controller); } }); controller.toggleButton?.addEventListener("click", () => toggleReplaceRow(controller)); controller.caseButton?.addEventListener("click", () => { controller.caseSensitive = !controller.caseSensitive; controller.caseButton?.classList.toggle("is-active", controller.caseSensitive); controller.caseButton?.setAttribute("aria-pressed", controller.caseSensitive ? "true" : "false"); runSearch(controller); }); panel.querySelector("[data-tlf-search-prev]")?.addEventListener("click", () => navigate(controller, -1)); panel.querySelector("[data-tlf-search-next]")?.addEventListener("click", () => navigate(controller, 1)); panel.querySelector("[data-tlf-search-close]")?.addEventListener("click", () => close(controller)); panel.querySelector("[data-tlf-search-replace-one]")?.addEventListener("click", () => replaceCurrent(controller)); panel.querySelector("[data-tlf-search-replace-all]")?.addEventListener("click", () => replaceAll(controller)); } function toggleReplaceRow(controller) { const row = controller.replaceRow; if (!row) { return; } row.hidden = !row.hidden; controller.toggleButton?.classList.toggle("is-open", !row.hidden); controller.toggleButton?.setAttribute("aria-expanded", row.hidden ? "false" : "true"); if (!row.hidden) { controller.replaceInput?.focus(); } } function searchOptions(controller) { return { caseSensitive: controller.caseSensitive }; } function replaceCurrent(controller) { const query = controller.input?.value || ""; if (!query) { return; } const replacement = controller.replaceInput?.value ?? ""; if (usesInlineSurface(controller)) { const status = TabLocalFiles.MarkdownInlineEdit.replaceInlineSearch(replacement) || { total: 0, index: -1 }; updateCount(controller, status.total, status.index); return; } if (controller.matchIndex < 0 || !controller.matches.length) { return; } const match = controller.matches[controller.matchIndex]; TabLocalFiles.TextEditor.applyUndoableEdit(controller.textarea, { replacement, rangeStart: match.from, rangeEnd: match.to, selectionStart: match.from, selectionEnd: match.from + replacement.length }); refreshMatchesAfterEdit(controller, match.from + replacement.length); controller.replaceInput?.focus(); } function replaceAll(controller) { const query = controller.input?.value || ""; if (!query) { return; } const replacement = controller.replaceInput?.value ?? ""; if (usesInlineSurface(controller)) { const status = TabLocalFiles.MarkdownInlineEdit.replaceAllInlineSearch(replacement) || { total: 0, index: -1 }; updateCount(controller, status.total, status.index); return; } const value = controller.textarea.value; const result = replaceAllText(value, query, replacement, searchOptions(controller)); if (!result.count) { return; } const scrollTop = controller.textarea.scrollTop; TabLocalFiles.TextEditor.applyUndoableEdit(controller.textarea, { replacement: result.text, rangeStart: 0, rangeEnd: value.length, selectionStart: 0, selectionEnd: 0 }); controller.textarea.scrollTop = scrollTop; refreshMatchesAfterEdit(controller, 0); controller.replaceInput?.focus(); } function refreshMatchesAfterEdit(controller, threshold, reveal = true) { const query = controller.input?.value || ""; controller.searchedText = controller.textarea.value; controller.matches = collectTextMatches(controller.searchedText, query, searchOptions(controller)); if (!controller.matches.length) { controller.matchIndex = -1; controller.matchLayer.clear(); updateCount(controller, 0, -1); return; } const next = controller.matches.findIndex((match) => match.from >= threshold); controller.matchIndex = next === -1 ? 0 : next; controller.matchLayer.paint(controller.matches, controller.matchIndex, reveal); updateCount(controller, controller.matches.length, controller.matchIndex); } function runSearch(controller) { const query = controller.input?.value || ""; if (usesInlineSurface(controller)) { controller.matches = []; controller.matchIndex = -1; controller.matchLayer.clear(); const status = TabLocalFiles.MarkdownInlineEdit.searchInline(query, searchOptions(controller)) || { total: 0, index: -1 }; updateCount(controller, status.total, status.index); return; } TabLocalFiles.MarkdownInlineEdit.clearInlineSearch(); controller.searchedText = controller.textarea.value; controller.matches = collectTextMatches(controller.searchedText, query, searchOptions(controller)); controller.matchIndex = controller.matches.length ? 0 : -1; controller.matchLayer.paint(controller.matches, controller.matchIndex, true); updateCount(controller, controller.matches.length, controller.matchIndex); } function navigate(controller, direction) { if (usesInlineSurface(controller)) { const status = TabLocalFiles.MarkdownInlineEdit.navigateInlineSearch(direction) || { total: 0, index: -1 }; updateCount(controller, status.total, status.index); return; } if (!controller.matches.length) { return; } controller.matchIndex = (controller.matchIndex + direction + controller.matches.length) % controller.matches.length; controller.matchLayer.paint(controller.matches, controller.matchIndex, true); updateCount(controller, controller.matches.length, controller.matchIndex); } function updateCount(controller, total, index) { if (controller.count) { controller.count.textContent = `${total ? index + 1 : 0} / ${total}`; } } })(MarkdownSearch = TabLocalFiles.MarkdownSearch || (TabLocalFiles.MarkdownSearch = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var MarkdownExport; (function (MarkdownExport) { const LONG_IMAGE_LAYOUT_WIDTH = 1200; function documentTitle() { return document.querySelector("[data-tlf-file-toolbar]")?.dataset.tlfFileName || "document"; } MarkdownExport.documentTitle = documentTitle; function exportFileName(title, extension) { return `${title.replace(/\.(md|mdx|markdown)$/i, "")}.${extension}`; } MarkdownExport.exportFileName = exportFileName; async function exportPdf() { if (!TabLocalFiles.CdpSession.isAvailable()) { TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportUnavailable"), "error"); TabLocalFiles.recordCurrentFileExportResult({ format: "pdf", result: "failed", failureReason: "unavailable" }); return; } TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportPdfWorking")); let session; try { session = await TabLocalFiles.CdpSession.openForCurrentTab(); const bytes = await TabLocalFiles.PageExport.printToPdf(session); TabLocalFiles.downloadBlob(bytesToBlob(bytes, "application/pdf"), exportFileName(documentTitle(), "pdf")); TabLocalFiles.recordCurrentFileExportResult({ format: "pdf", result: "success" }); TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportPdfDone"), "success"); } catch (error) { console.error("[Previewer] PDF export failed.", error); TabLocalFiles.recordCurrentFileExportResult({ format: "pdf", result: "failed", failureReason: "error", error }); TabLocalFiles.showToast(failureToast("markdown.exportPdfFailed", error), "error"); } finally { session?.close(); } } MarkdownExport.exportPdf = exportPdf; async function exportLongImage() { if (!TabLocalFiles.CdpSession.isAvailable()) { TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportUnavailable"), "error"); TabLocalFiles.recordCurrentFileExportResult({ format: "long_image", result: "failed", failureReason: "unavailable" }); return; } TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportImageWorking")); let session; try { session = await TabLocalFiles.CdpSession.openForCurrentTab(); const capture = await TabLocalFiles.PageExport.captureLongImage(session, { layoutWidth: LONG_IMAGE_LAYOUT_WIDTH, deviceScaleFactor: window.devicePixelRatio || 1, measure: () => ({ width: Math.min(document.documentElement.clientWidth || LONG_IMAGE_LAYOUT_WIDTH, LONG_IMAGE_LAYOUT_WIDTH), height: document.documentElement.scrollHeight }) }); if (!capture) { TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportImageTooTall"), "error"); TabLocalFiles.recordCurrentFileExportResult({ format: "long_image", result: "failed", failureReason: "too_tall" }); return; } const blob = await stitchCapture(capture); TabLocalFiles.downloadBlob(blob, exportFileName(documentTitle(), "png")); TabLocalFiles.recordCurrentFileExportResult({ format: "long_image", result: "success" }); TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportImageDone"), "success"); } catch (error) { console.error("[Previewer] Long image export failed.", error); TabLocalFiles.recordCurrentFileExportResult({ format: "long_image", result: "failed", failureReason: "error", error }); TabLocalFiles.showToast(failureToast("markdown.exportImageFailed", error), "error"); } finally { session?.close(); } } MarkdownExport.exportLongImage = exportLongImage; function failureToast(key, error) { const reason = error instanceof Error ? error.message : String(error || ""); return reason ? `${TabLocalFiles.t(key)}: ${reason}` : TabLocalFiles.t(key); } async function stitchCapture(capture) { if (capture.slices.length === 1) { return bytesToBlob(TabLocalFiles.PageExport.decodeBase64(capture.slices[0].data), "image/png"); } const canvas = document.createElement("canvas"); canvas.width = Math.round(capture.width * capture.scale); canvas.height = Math.round(capture.height * capture.scale); const context = canvas.getContext("2d"); if (!context) { throw new Error("Canvas is unavailable"); } context.fillStyle = "#ffffff"; context.fillRect(0, 0, canvas.width, canvas.height); for (const slice of capture.slices) { const image = await loadImageElement(bytesToBlob(TabLocalFiles.PageExport.decodeBase64(slice.data), "image/png")); context.drawImage(image, 0, Math.round(slice.y * capture.scale)); } return TabLocalFiles.canvasToPngBlob(canvas); } function bytesToBlob(bytes, type) { return new Blob([bytes], type ? { type } : undefined); } function loadImageElement(blob) { const url = URL.createObjectURL(blob); return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => { URL.revokeObjectURL(url); resolve(image); }; image.onerror = () => { URL.revokeObjectURL(url); reject(new Error("Unable to decode the image")); }; image.src = url; }); } })(MarkdownExport = TabLocalFiles.MarkdownExport || (TabLocalFiles.MarkdownExport = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var Markdown; (function (Markdown) { const EDITOR_ROOT_ID = "tlf-markdown-editor"; const TOC_PANEL_ID = "tlf-markdown-toc"; const TOC_WIDTH_STORAGE_KEY = "tab-local-files:markdown-toc-width"; const TOC_DEFAULT_WIDTH = 224; const TOC_MIN_WIDTH = 160; const TOC_MAX_WIDTH = 480; const TOC_MIN_DOCUMENT_WIDTH = 360; const FONT_SIZE_VALUE_ID = "tlf-markdown-font-size"; const FONT_SIZE_STEPS = ["medium", "large", "xlarge"]; const FONT_SIZE_LABEL_KEYS = { medium: "markdown.fontMedium", large: "markdown.fontLarge", xlarge: "markdown.fontXLarge" }; const HEADING_RE = /^(#{1,6})\s+(.+?)\s*#*\s*$/; const FENCE_RE = /^\s*(```|~~~)/; const HORIZONTAL_RULE_RE = /^\s{0,3}([-*_])(\s*\1){2,}\s*$/; const BLOCKQUOTE_RE = /^\s{0,3}>\s?/; const LIST_LINE_RE = /^(\s*)([-+*]|\d+[.)])\s+(.+)$/; const TABLE_ALIGNMENT_RE = /^:?-{3,}:?$/; const MARKDOWN_CODE_SELECTOR = `#${EDITOR_ROOT_ID} [data-tlf-editor-preview] pre code`; const MERMAID_DIAGRAM_SELECTOR = "[data-tlf-mermaid-diagram]"; const MINDMAP_DIAGRAM_SELECTOR = "[data-tlf-mindmap-diagram]"; const INLINE_CODE_BARE_URL_PATTERN = /^(?:https?:\/\/|www\.)[^\s<>"`]+$/i; const INLINE_CODE_ANGLE_AUTOLINK_PATTERN = /^<((?:https?:\/\/)[^\s<>"`]+)>$/i; const MERMAID_MINDMAP_PREFIX = /^mindmap(?:\s|$)/i; const MERMAID_DIRECTIVE_SUFFIXES = [ /\s*::icon\([^)]*\)\s*$/i, /\s*:::[\w-]+(?:\s+[\w-]+)*\s*$/, ]; const MERMAID_LABEL_PATTERNS = [ /^[A-Za-z_][\w-]*\s*\(\(([\s\S]+)\)\)$/, /^[A-Za-z_][\w-]*\s*\(([\s\S]+)\)$/, /^[A-Za-z_][\w-]*\s*\[([\s\S]+)\]$/, /^[A-Za-z_][\w-]*\s*\{\{([\s\S]+)\}\}$/, /^[A-Za-z_][\w-]*\s*\)([\s\S]+)\($/, /^\(\(([\s\S]+)\)\)$/, /^\(([\s\S]+)\)$/, /^\[([\s\S]+)\]$/, /^\{\{([\s\S]+)\}\}$/, /^\)([\s\S]+)\($/, ]; const ALLOWED_HTML_TAGS = new Set([ "a", "abbr", "b", "blockquote", "br", "code", "del", "details", "div", "em", "hr", "i", "img", "kbd", "li", "mark", "ol", "p", "pre", "s", "span", "strong", "sub", "summary", "sup", "table", "tbody", "td", "th", "thead", "tr", "ul" ]); const VOID_HTML_TAGS = new Set(["br", "hr", "img"]); const DEFERRED_IMAGE_ATTRS = ' loading="lazy" decoding="async"'; const DANGEROUS_HTML_BLOCK_RE = /<(script|style|iframe|object|embed|form|input|button|textarea|select|option|meta|link|base|svg|math)\b[\s\S]*?<\/\1\s*>/gi; const DANGEROUS_HTML_TAG_RE = /<\/?(script|style|iframe|object|embed|form|input|button|textarea|select|option|meta|link|base|svg|math)\b[^>]*>/gi; const CJK_TEXT_CHARS = "\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFAFF"; const CJK_PUNCTUATION_CHARS = "\\u3000-\\u303F\\uFF00-\\uFFEF"; const CJK_AUTOLINK_BOUNDARY_CHARS = `${CJK_TEXT_CHARS}${CJK_PUNCTUATION_CHARS}`; const ASCII_PUNCTUATION_BEFORE_CJK = ")\\]\\},.;:!?"; const URL_PATH_STOP_CHARS = `\\s<>"'\`\\)\\]\\}${CJK_TEXT_CHARS}\\u3001\\u3002\\uFF0C\\uFF1B\\uFF1A\\uFF01\\uFF1F\\uFF09\\uFF3D\\uFF5D\\u300B\\u300D\\u300F`; const BARE_URL_BEFORE_CJK_BOUNDARY_PATTERN = new RegExp(`((?:https?:\\/\\/|www\\.)[A-Za-z0-9][A-Za-z0-9.-]*(?::\\d+)?(?:\\/[^${URL_PATH_STOP_CHARS}]*)?)(?=(?:[${CJK_AUTOLINK_BOUNDARY_CHARS}]|[${ASCII_PUNCTUATION_BEFORE_CJK}][${CJK_AUTOLINK_BOUNDARY_CHARS}]))`, "g"); const CONTROL_ESCAPED_LATEX_COMMAND_REPLACEMENTS = [ [/\u0008oldsymbol(?=\s*\{)/g, "\\boldsymbol"], [/\u0008egin(?=\s*\{)/g, "\\begin"], [/\u0008eta(?![A-Za-z])/g, "\\beta"], [/\u0008ar(?=\s*\{)/g, "\\bar"], [/\u0008inom(?=\s*\{)/g, "\\binom"], [/\u000Crac(?=\s*\{)/g, "\\frac"], [/\u000Corall(?![A-Za-z])/g, "\\forall"], [/\u0009ext(?=\s*\{)/g, "\\text"], [/\u0009au(?![A-Za-z])/g, "\\tau"], [/\u0009frac(?=\s*\{)/g, "\\tfrac"], [/\u0009herefore(?![A-Za-z])/g, "\\therefore"], [/\u0009ilde(?=\s*\{)/g, "\\tilde"], [/\u0009imes(?![A-Za-z])/g, "\\times"], [/\u0009heta(?![A-Za-z])/g, "\\theta"], [/\u0009riangle(?![A-Za-z])/g, "\\triangle"], [/\u000Aabla(?![A-Za-z])/g, "\\nabla"], [/\u000Aeq(?![A-Za-z])/g, "\\neq"], [/\u000Aotin(?![A-Za-z])/g, "\\notin"], [/\u000Dangle(?![A-Za-z])/g, "\\rangle"], [/\u000Dho(?![A-Za-z])/g, "\\rho"], [/\u000Dightarrow(?![A-Za-z])/g, "\\rightarrow"], ]; const LINE_BREAK_RIGHT_FRAGMENT_PATTERN = /\\(?:left|right)(?![A-Za-z])|\n[ \t]*ight(?=\s*[\]\)\}\|.])/g; let markedMathRegistered = false; let markedFootnotesRegistered = false; let currentFootnotes; let diagramIdSeed = 0; function taskListMarkerEdit(source, ordinal, checked, selectionStart = 0, selectionEnd = selectionStart) { if (!Number.isInteger(ordinal) || ordinal < 0) { return undefined; } const marker = taskListMarker(source, ordinal); if (!marker) { return undefined; } return { replacement: checked ? "x" : " ", rangeStart: marker.markerOffset, rangeEnd: marker.markerOffset + 1, selectionStart: Math.min(Math.max(selectionStart, 0), source.length), selectionEnd: Math.min(Math.max(selectionEnd, 0), source.length) }; } Markdown.taskListMarkerEdit = taskListMarkerEdit; function taskListMarker(source, ordinal) { const marked = window.marked; if (!marked) { return undefined; } registerMarkedMath(marked); registerMarkedFootnotes(marked); registerMarkedSvg(marked); const { frontmatter, body } = TabLocalFiles.MarkdownInlineEdit.splitFrontmatter(source); const candidates = taskMarkerCandidates(body); if (candidates.length === 0) { return undefined; } const tagPrefix = uniqueTaskMarkerTagPrefix(body); const taggedBody = tagTaskMarkerCandidates(body, candidates, tagPrefix); const parsedCandidateIndexes = parsedTaskCandidateIndexes(taggedBody, marked, tagPrefix); const candidateIndex = parsedCandidateIndexes?.[ordinal]; const markerOffset = candidateIndex === undefined ? undefined : candidates[candidateIndex]; if (markerOffset === undefined) { return undefined; } return { markerOffset: frontmatter.length + markerOffset }; } const TASK_MARKER_TAG_START = "\uE000tlf-task-"; const TASK_MARKER_TAG_END = "\uE001"; function uniqueTaskMarkerTagPrefix(source) { let salt = 0; let prefix = `${TASK_MARKER_TAG_START}${salt.toString(36)}-`; while (source.includes(prefix)) { salt += 1; prefix = `${TASK_MARKER_TAG_START}${salt.toString(36)}-`; } return prefix; } function tagTaskMarkerCandidates(source, offsets, tagPrefix) { const parts = []; let cursor = 0; offsets.forEach((offset, index) => { const insertionOffset = offset + 2; parts.push(source.slice(cursor, insertionOffset), ` ${tagPrefix}${index.toString(36)}${TASK_MARKER_TAG_END} `); cursor = insertionOffset; }); parts.push(source.slice(cursor)); return parts.join(""); } function parsedTaskCandidateIndexes(source, marked, tagPrefix) { const indexes = []; let valid = true; const renderer = new marked.Renderer(); renderer.checkbox = () => ""; renderer.listitem = function (token) { if (typeof token.checked === "boolean") { const text = token.text || ""; const tagEnd = text.indexOf(TASK_MARKER_TAG_END, tagPrefix.length); const encodedIndex = text.startsWith(tagPrefix) && tagEnd >= 0 ? text.slice(tagPrefix.length, tagEnd) : ""; const candidateIndex = encodedIndex ? Number.parseInt(encodedIndex, 36) : Number.NaN; if (!Number.isSafeInteger(candidateIndex)) { valid = false; } else { indexes.push(candidateIndex); } } return this.parser?.parse(token.tokens || []) || ""; }; marked.parse(preprocessMarkdown(source), { async: false, gfm: true, renderer }); return valid ? indexes : undefined; } function taskMarkerCandidates(source) { const offsets = []; const pattern = /^(?:(?:[ \t]*>[ \t]?)|(?:[ \t]*(?:[-+*]|\d+[.)])[ \t]+))*[ \t]*(?:[-+*]|\d+[.)])[ \t]+\[([ xX])\][ \t]+(?=[^ \t\r\n])/gm; for (const match of source.matchAll(pattern)) { if (match.index !== undefined) { offsets.push(match.index + match[0].lastIndexOf("[") + 1); } } return offsets; } function markdownTaskRevision(source) { let hash = 0x811c9dc5; for (let index = 0; index < source.length; index += 1) { hash ^= source.charCodeAt(index); hash = Math.imul(hash, 0x01000193); } return `${source.length.toString(36)}-${(hash >>> 0).toString(36)}`; } function render(context) { if (TabLocalFiles.LargeFile.isPreview(context)) { return { title: context.fileTitle, bodyHtml: TabLocalFiles.LargeFile.renderTextPreview(context, "Markdown", context.rawText), contentClassName: "tlf-markdown tlf-large-file-document", toolbar: TabLocalFiles.rawPreviewToolbar(TabLocalFiles.LargeFile.toolbarStatus(context, "Markdown")), afterMount: () => TabLocalFiles.LargeTextViewer.bind(context) }; } const parsed = markdownToHtml(context.rawText, context.sourceUrl); const liveSyncPath = TabLocalFiles.isEditableLocalFileSourceUrl(context.sourceUrl) ? context.decodedPath : undefined; return { title: TabLocalFiles.DocumentTitle.resolve(titleCandidate(context.rawText, parsed.outline), context.fileTitle), bodyHtml: `
${renderTocPanel(tocOutline(parsed))} ${TabLocalFiles.TextEditor.render({ rootId: EDITOR_ROOT_ID, initialText: context.rawText, previewHtml: renderMarkdownPreviewContent(parsed.html), textareaLabel: TabLocalFiles.t("markdown.contentLabel"), layout: "split", spellcheck: false, highlightLanguage: "md" })}
`, contentClassName: "tlf-markdown tlf-editable-document", toolbar: markdownToolbarContribution(), afterMount: () => { applyDocumentTitle(context, context.rawText, parsed.outline); TabLocalFiles.TextEditor.bind({ rootId: EDITOR_ROOT_ID, fileName: context.fileName, mimeType: "text/markdown", initialText: context.rawText, sourcePath: TabLocalFiles.isEditableLocalFileSourceUrl(context.sourceUrl) ? context.decodedPath : undefined, mountId: context.mountId, saveMode: "original", autoSave: true, highlightLanguage: "md", renderPreview: (value) => renderMarkdownPreviewContent(markdownToHtml(value, context.sourceUrl).html), afterPreviewRender: (value) => { syncHeadingSurfaces(value, context); renderMarkdownPreviewEnhancements(value); TabLocalFiles.MarkdownInlineEdit.onPreviewRendered(value); } }); const imageCaptureEnabled = TabLocalFiles.isEditableLocalFileSourceUrl(context.sourceUrl) && TabLocalFiles.TabLocalWorkspace.isAvailable(); TabLocalFiles.MarkdownInlineEdit.bind({ rootId: EDITOR_ROOT_ID, mountId: context.mountId, resolveImageUrl: (src) => resolveMarkdownImageUrl(src, context.sourceUrl), renderHtml: (html, block) => renderHtmlFragment(html, block, context.sourceUrl), renderAlertShell: (markerLine) => renderAlertShellHtml(markerLine, context.sourceUrl), uploadImage: imageCaptureEnabled ? (file) => TabLocalFiles.MarkdownImagePaste.saveImageToAssets(context.decodedPath, file) : undefined, forceSourceVariant: TabLocalFiles.LargeFile.markdownForcesSourceVariant(context) }); if (imageCaptureEnabled) { TabLocalFiles.MarkdownImagePaste.bind({ rootId: EDITOR_ROOT_ID, mountId: context.mountId, decodedPath: context.decodedPath }); } TabLocalFiles.MarkdownSmartPaste.bind({ rootId: EDITOR_ROOT_ID, mountId: context.mountId }); if (liveSyncPath) { TabLocalFiles.LiveSync.bind({ rootId: EDITOR_ROOT_ID, sourcePath: liveSyncPath, mountId: context.mountId }); } TabLocalFiles.MarkdownRichCopy.bind({ rootId: EDITOR_ROOT_ID, mountId: context.mountId }); TabLocalFiles.MarkdownCodeCopy.bind({ rootId: EDITOR_ROOT_ID, mountId: context.mountId }); TabLocalFiles.MarkdownSearch.bind({ rootId: EDITOR_ROOT_ID, mountId: context.mountId }); bindMarkdownViewControls(); renderMarkdownPreviewEnhancements(context.rawText); TabLocalFiles.registerDocumentMountCleanup(context.mountId, TabLocalFiles.Theme.onChange(() => renderMarkdownDiagramsForTheme(true))); bindMarkdownTocNavigation(); bindMarkdownTocScrollSpy(); bindMarkdownPreviewAnchors(); bindMarkdownPreviewTaskCheckboxes(); bindMarkdownFormatActions(); bindMarkdownFormatShortcuts(); TabLocalFiles.DocumentTranslation.bind(context, { syntax: "markdown", editorRootId: EDITOR_ROOT_ID, extension: ".md" }); } }; } Markdown.render = render; function markdownToolbarContribution() { const toolbar = TabLocalFiles.editableTextToolbar(); toolbar.contextPreview = [ TabLocalFiles.toolbarAction("markdown.toc", TabLocalFiles.t("markdown.toc"), "list-tree", { pressed: false, showLabel: true }), TabLocalFiles.contextDivider("markdown.divider-width"), TabLocalFiles.segmentedToolbar("markdown.width", TabLocalFiles.t("markdown.readingWidth"), [ { key: "fixed", label: TabLocalFiles.t("markdown.widthFixed"), pressed: true }, { key: "full", label: TabLocalFiles.t("markdown.widthFull") } ]), TabLocalFiles.contextDivider("markdown.divider-font"), TabLocalFiles.contextLabel("markdown.font-label", TabLocalFiles.t("markdown.fontSize")), TabLocalFiles.toolbarAction("markdown.font-decrease", TabLocalFiles.t("markdown.fontDecrease"), "zoom-out"), TabLocalFiles.toolbarCustom("markdown.font-size", TabLocalFiles.renderStateSlot(FONT_SIZE_STEPS.map((step) => ({ key: step, html: TabLocalFiles.escapeHtml(TabLocalFiles.t(FONT_SIZE_LABEL_KEYS[step])) })), FONT_SIZE_STEPS[0], { elementId: FONT_SIZE_VALUE_ID, className: "tlf-context-value", live: true, ariaLabel: TabLocalFiles.t("markdown.fontSize") })), TabLocalFiles.toolbarAction("markdown.font-increase", TabLocalFiles.t("markdown.fontIncrease"), "zoom-in") ]; const formatAction = (id, labelKey, icon) => { const spec = FORMAT_ACTION_SHORTCUTS[id]; const label = spec ? TabLocalFiles.shortcutLabel(labelKey, spec) : TabLocalFiles.t(labelKey); return TabLocalFiles.toolbarAction(id, label, icon, { requiresEditorFocus: true }); }; toolbar.contextEditLeading = [ TabLocalFiles.segmentedToolbar("markdown.editmode", TabLocalFiles.t("markdown.editModeSwitch"), [ { key: "source", label: TabLocalFiles.t("markdown.editSource") }, { key: "split", label: TabLocalFiles.t("markdown.editSplit") }, { key: "inline", label: TabLocalFiles.t("markdown.editInline"), pressed: true } ]), TabLocalFiles.contextDivider("markdown.divider-editmode") ]; toolbar.contextEdit = [ formatAction("markdown.h1", "markdown.heading1", "heading-1"), formatAction("markdown.h2", "markdown.heading2", "heading-2"), formatAction("markdown.h3", "markdown.heading3", "heading-3"), formatAction("markdown.h4", "markdown.heading4", "heading-4"), formatAction("markdown.h5", "markdown.heading5", "heading-5"), TabLocalFiles.contextDivider("markdown.divider-inline"), formatAction("markdown.bold", "markdown.bold", "bold"), formatAction("markdown.italic", "markdown.italic", "italic"), formatAction("markdown.strike", "markdown.strikethrough", "strikethrough"), formatAction("markdown.inline-code", "markdown.inlineCode", "code"), TabLocalFiles.contextDivider("markdown.divider-list"), formatAction("markdown.ul", "markdown.unorderedList", "list"), formatAction("markdown.ol", "markdown.orderedList", "list-ordered"), formatAction("markdown.task", "markdown.taskList", "list-checks") ]; toolbar.contextEditOverflow = [ formatAction("markdown.quote", "markdown.blockquote", "quote"), formatAction("markdown.codeblock", "markdown.codeBlock", "square-code"), formatAction("markdown.link", "markdown.insertLink", "link"), formatAction("markdown.image", "markdown.insertImage", "image"), formatAction("markdown.table", "markdown.insertTable", "table"), formatAction("markdown.format-tables", "markdown.formatTables", "table-properties"), formatAction("markdown.hr", "markdown.horizontalRule", "minus"), formatAction("markdown.clear", "markdown.clearFormat", "eraser") ]; toolbar.exportItems = [ TabLocalFiles.toolbarAction("markdown.export-html", TabLocalFiles.t("markdown.exportHtml"), "code"), TabLocalFiles.toolbarAction("markdown.export-pdf", TabLocalFiles.t("markdown.exportPdf"), "download"), TabLocalFiles.toolbarAction("markdown.export-image", TabLocalFiles.t("markdown.exportImage"), "image"), TabLocalFiles.DocumentTranslation.exportItem() ]; return toolbar; } async function exportStandaloneHtml(preview) { const content = preview?.querySelector(".tlf-markdown-preview-content"); if (!content) { TabLocalFiles.recordCurrentFileExportResult({ format: "html", result: "failed", failureReason: "no_content" }); return; } try { const body = content.cloneNode(true); TabLocalFiles.MarkdownRichCopy.stripPreviewControls(body); const title = document.querySelector("[data-tlf-file-toolbar]")?.dataset.tlfFileName || "document"; const html = await buildStandaloneHtmlDocument(title, body, body.querySelector(".katex") !== null); TabLocalFiles.downloadTextFile(`${title.replace(/\.mdx?$/i, "")}.html`, html, "text/html"); TabLocalFiles.recordCurrentFileExportResult({ format: "html", result: "success" }); TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportHtmlDone"), "success"); } catch (error) { console.error("[Previewer] HTML export failed.", error); TabLocalFiles.recordCurrentFileExportResult({ format: "html", result: "failed", failureReason: "error", error }); TabLocalFiles.showToast(TabLocalFiles.t("markdown.exportHtmlFailed"), "error"); } } Markdown.exportStandaloneHtml = exportStandaloneHtml; const EXPORT_BODY_CLASS = "tlf-markdown"; const EXPORT_ARTICLE_CLASS = "tlf-markdown-preview-content"; async function buildStandaloneHtmlDocument(title, bodyElement, includeMathFonts) { const bodyHtml = bodyElement.innerHTML; const styles = await collectExportStyles(exportProbe(bodyElement), includeMathFonts); return [ "", '', "", '', '', `${TabLocalFiles.escapeHtml(title)}`, ``, "", ``, `
${bodyHtml}
`, "", "", "" ].join("\n"); } function exportProbe(bodyElement) { const probe = document.createElement("div"); probe.className = EXPORT_BODY_CLASS; const article = document.createElement("article"); article.className = EXPORT_ARTICLE_CLASS; article.innerHTML = bodyElement.innerHTML; probe.appendChild(article); return probe; } async function collectExportStyles(body, includeMathFonts) { const EXPORT_BASE_CSS = [ "body{margin:0;padding:32px 24px;background:#fff;color:rgba(0,0,0,.85);", "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'PingFang SC','Microsoft YaHei',sans-serif;}", ".tlf-markdown-preview-content{max-width:720px;margin:0 auto;}" ].join(""); const parts = [EXPORT_BASE_CSS]; for (const sheet of Array.from(document.styleSheets)) { let rules; try { rules = sheet.cssRules; } catch { continue; } if (rules) { collectExportRules(rules, body, includeMathFonts, parts); } } return inlineFontUrls(parts.join("\n")); } function collectExportRules(rules, body, includeMathFonts, parts) { for (const rule of Array.from(rules)) { const grouped = rule.cssRules; const isLayer = rule.constructor.name === "CSSLayerBlockRule"; const isMedia = rule.constructor.name === "CSSMediaRule"; if (isLayer && grouped) { collectExportRules(grouped, body, includeMathFonts, parts); continue; } const isSupports = rule.constructor.name === "CSSSupportsRule"; if ((isMedia || isSupports) && grouped) { const inner = []; collectExportRules(grouped, body, includeMathFonts, inner); if (inner.length > 0) { const condition = rule.conditionText; parts.push(`${isMedia ? "@media" : "@supports"} ${condition}{${inner.join("\n")}}`); } continue; } if (isKatexFontRule(rule)) { if (includeMathFonts) { parts.push(rule.cssText); } continue; } const selector = rule.selectorText; if (selector === undefined) { parts.push(rule.cssText); continue; } if (selectorAppliesToExport(selector, body)) { parts.push(rule.cssText); } } } function selectorAppliesToExport(selectorText, body) { return selectorText.split(",").some((part) => { const selector = part.trim(); if (!selector) { return false; } if (/^(:root|html|body|\*)(\[[^\]]*\])*$/.test(selector)) { return true; } const matchable = selector .replace(/::[a-z-]+(\([^)]*\))?/gi, "") .replace(/:(hover|focus|focus-visible|active|visited|disabled|checked|target)\b/gi, "") .trim(); if (!matchable) { return true; } try { return body.matches(matchable) || body.querySelector(matchable) !== null; } catch { return true; } }); } function isKatexFontRule(rule) { return rule.constructor.name === "CSSFontFaceRule" && /KaTeX/i.test(rule.cssText); } async function inlineFontUrls(css) { const urls = new Set(); for (const match of css.matchAll(/url\((["']?)([^)"']+\.woff2)\1\)/gi)) { urls.add(match[2]); } if (urls.size === 0) { return css; } const replacements = new Map(); await Promise.all(Array.from(urls).map(async (url) => { try { const absolute = new URL(url, document.baseURI).toString(); const response = await fetch(absolute); const buffer = await response.arrayBuffer(); replacements.set(url, `data:font/woff2;base64,${base64FromBuffer(buffer)}`); } catch { } })); return css.replace(/url\((["']?)([^)"']+\.woff2)\1\)/gi, (whole, _quote, url) => { const data = replacements.get(url); return data ? `url(${data})` : whole; }); } function base64FromBuffer(buffer) { const bytes = new Uint8Array(buffer); let binary = ""; const CHUNK = 0x8000; for (let index = 0; index < bytes.length; index += CHUNK) { binary += String.fromCharCode(...bytes.subarray(index, index + CHUNK)); } return btoa(binary); } function renderMarkdownPreviewContent(html) { return `
${html}
`; } function renderTocPanel(outline) { return TabLocalFiles.renderDocnav({ elementId: TOC_PANEL_ID, className: "tlf-markdown-toc", ariaLabel: TabLocalFiles.t("markdown.toc"), hidden: true, extraAttributes: "data-tlf-md-toc", width: { min: TOC_MIN_WIDTH, max: TOC_MAX_WIDTH, defaultWidth: TOC_DEFAULT_WIDTH, storageKey: TOC_WIDTH_STORAGE_KEY, reserve: TOC_MIN_DOCUMENT_WIDTH }, resizeAriaLabel: TabLocalFiles.t("markdown.tocResizeAria"), resizeTitle: TabLocalFiles.t("markdown.tocResizeTitle"), bodyHtml: `
${TabLocalFiles.escapeHtml(TabLocalFiles.t("outline.title"))}
${TabLocalFiles.renderOutline(outline, TabLocalFiles.EXTENSION_CONFIG.maxOutlineLevel, { collapsible: true })}
` }); } function bindMarkdownViewControls() { const root = document.getElementById(EDITOR_ROOT_ID); const preview = root?.querySelector("[data-tlf-editor-preview]"); const tocPanel = document.getElementById(TOC_PANEL_ID); const fontValue = document.getElementById(FONT_SIZE_VALUE_ID); if (!preview) { return; } const inlineHost = root?.querySelector("[data-tlf-editor-inline]"); const readingSurfaces = inlineHost ? [preview, inlineHost] : [preview]; TabLocalFiles.onToolbarAction("markdown.toc", ({ button }) => { if (!tocPanel) { return; } tocPanel.hidden = !tocPanel.hidden; button.setAttribute("aria-pressed", String(!tocPanel.hidden)); if (!tocPanel.hidden && typeof tocPanel.dispatchEvent === "function") { tocPanel.dispatchEvent(new CustomEvent("tlf:toc-visible")); } }); const applyWidth = (key, remember) => { readingSurfaces.forEach((surface) => surface.classList.toggle("tlf-md-width-full", key === "full")); TabLocalFiles.syncSegmentedToolbar("markdown.width", key); if (remember) { TabLocalFiles.Preferences.update({ preview: { markdownWidth: key } }); } }; TabLocalFiles.onToolbarAction("markdown.export-html", () => { void exportStandaloneHtml(preview); }); TabLocalFiles.onToolbarAction("markdown.export-pdf", () => { void TabLocalFiles.MarkdownExport.exportPdf(); }); TabLocalFiles.onToolbarAction("markdown.export-image", () => { void TabLocalFiles.MarkdownExport.exportLongImage(); }); TabLocalFiles.onToolbarAction("markdown.width.fixed", () => applyWidth("fixed", true)); TabLocalFiles.onToolbarAction("markdown.width.full", () => applyWidth("full", true)); applyWidth(TabLocalFiles.Preferences.markdownReadingWidth(), false); let fontIndex = Math.max(0, FONT_SIZE_STEPS.indexOf(TabLocalFiles.Preferences.markdownFontSize())); const applyFontSize = (remember) => { fontIndex = Math.min(FONT_SIZE_STEPS.length - 1, Math.max(0, fontIndex)); const step = FONT_SIZE_STEPS[fontIndex]; readingSurfaces.forEach((surface) => { surface.dataset.tlfMdFont = step; }); TabLocalFiles.setStateSlot(fontValue, step); if (remember) { TabLocalFiles.Preferences.update({ preview: { markdownFontSize: step } }); } }; TabLocalFiles.onToolbarAction("markdown.font-decrease", () => { fontIndex -= 1; applyFontSize(true); }); TabLocalFiles.onToolbarAction("markdown.font-increase", () => { fontIndex += 1; applyFontSize(true); }); applyFontSize(false); } const FORMAT_ACTION_SHORTCUTS = { "markdown.link": { mod: true, key: "K" }, "markdown.h1": { mod: true, alt: true, key: "1" }, "markdown.h2": { mod: true, alt: true, key: "2" }, "markdown.h3": { mod: true, alt: true, key: "3" }, "markdown.h4": { mod: true, alt: true, key: "4" }, "markdown.h5": { mod: true, alt: true, key: "5" }, "markdown.strike": { mod: true, shift: true, key: "X" }, "markdown.ol": { mod: true, shift: true, key: "7" }, "markdown.ul": { mod: true, shift: true, key: "8" }, "markdown.quote": { mod: true, shift: true, key: "9" }, "markdown.codeblock": { mod: true, shift: true, key: "K" }, "markdown.inline-code": { mod: true, shift: true, key: "C" }, "markdown.bold": { mod: true, key: "B" }, "markdown.italic": { mod: true, key: "I" } }; function formatActionForShortcut(input) { const { alt, shift, code } = input; if (alt && shift) { return undefined; } if (alt) { const heading = /^Digit([1-5])$/.exec(code); return heading ? `markdown.h${heading[1]}` : undefined; } if (shift) { if (code === "KeyX") { return "markdown.strike"; } if (code === "Digit7") { return "markdown.ol"; } if (code === "Digit8") { return "markdown.ul"; } if (code === "Digit9") { return "markdown.quote"; } if (code === "KeyK") { return "markdown.codeblock"; } if (code === "KeyC") { return "markdown.inline-code"; } return undefined; } return code === "KeyK" ? "markdown.link" : undefined; } Markdown.formatActionForShortcut = formatActionForShortcut; const WIDE_CHAR_RANGES = [ [0x1100, 0x115f], [0x2e80, 0x303e], [0x3041, 0x33ff], [0x3400, 0x4dbf], [0x4e00, 0x9fff], [0xa000, 0xa4cf], [0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe30, 0xfe4f], [0xff00, 0xff60], [0xffe0, 0xffe6], [0x1f300, 0x1faff], [0x20000, 0x3fffd] ]; function documentStatistics(source) { const text = source ?? ""; const characters = text.length; if (characters === 0) { return { lines: 0, words: 0, characters: 0 }; } const cjk = text.match(new RegExp(`[${CJK_TEXT_CHARS}]`, "gu"))?.length ?? 0; const nonCjkWords = text .replace(new RegExp(`[${CJK_TEXT_CHARS}]`, "gu"), " ") .split(/\s+/) .filter(Boolean).length; return { lines: text.split("\n").length, words: cjk + nonCjkWords, characters }; } Markdown.documentStatistics = documentStatistics; function markdownDisplayWidth(text) { let width = 0; for (const char of text) { const codePoint = char.codePointAt(0) || 0; width += WIDE_CHAR_RANGES.some(([from, to]) => codePoint >= from && codePoint <= to) ? 2 : 1; } return width; } Markdown.markdownDisplayWidth = markdownDisplayWidth; const TABLE_MIN_COLUMN_WIDTH = 3; function splitTableRowCells(line) { let content = line.trim(); if (content.startsWith("|")) { content = content.slice(1); } if (endsWithUnescapedPipe(content)) { content = content.slice(0, -1); } const cells = []; let current = ""; for (let index = 0; index < content.length; index += 1) { const char = content[index]; if (char === "\\" && content[index + 1] === "|") { current += "\\|"; index += 1; continue; } if (char === "|") { cells.push(current.trim()); current = ""; continue; } current += char; } cells.push(current.trim()); return cells; } function endsWithUnescapedPipe(content) { if (!content.endsWith("|")) { return false; } let backslashes = 0; let index = content.length - 2; while (index >= 0 && content[index] === "\\") { backslashes += 1; index -= 1; } return backslashes % 2 === 0; } function separatorAlignments(line) { const cells = splitTableRowCells(line); if (!cells.length) { return undefined; } const alignments = []; for (const cell of cells) { const match = /^(:?)-+(:?)$/.exec(cell); if (!match) { return undefined; } alignments.push(match[1] && match[2] ? "center" : match[1] ? "left" : match[2] ? "right" : "none"); } return alignments; } function padTableCell(text, width, alignment) { const pad = Math.max(0, width - markdownDisplayWidth(text)); if (alignment === "right") { return " ".repeat(pad) + text; } if (alignment === "center") { const left = Math.floor(pad / 2); return " ".repeat(left) + text + " ".repeat(pad - left); } return text + " ".repeat(pad); } function tableSeparatorCell(width, alignment) { if (alignment === "center") { return `:${"-".repeat(Math.max(1, width - 2))}:`; } if (alignment === "left") { return `:${"-".repeat(Math.max(1, width - 1))}`; } if (alignment === "right") { return `${"-".repeat(Math.max(1, width - 1))}:`; } return "-".repeat(Math.max(1, width)); } function rebuildTableBlock(lines, alignments, indent) { const rows = lines.map((line, index) => (index === 1 ? undefined : splitTableRowCells(line))); const columnCount = Math.max(alignments.length, ...rows.map((row) => (row ? row.length : 0))); const widths = []; for (let column = 0; column < columnCount; column += 1) { let width = TABLE_MIN_COLUMN_WIDTH; for (const row of rows) { if (row && row[column] !== undefined) { width = Math.max(width, markdownDisplayWidth(row[column])); } } widths.push(width); } return lines.map((_line, index) => { if (index === 1) { const cells = widths.map((width, column) => tableSeparatorCell(width, alignments[column] ?? "none")); return `${indent}| ${cells.join(" | ")} |`; } const row = rows[index] || []; const cells = widths.map((width, column) => padTableCell(row[column] ?? "", width, alignments[column] ?? "none")); return `${indent}| ${cells.join(" | ")} |`; }); } function formatMarkdownTables(source) { const rawLines = source.split("\n"); const endings = rawLines.map((line) => (line.endsWith("\r") ? "\r" : "")); const lines = rawLines.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line)); const output = [...lines]; let activeFence; let index = 0; while (index < lines.length) { const line = lines[index]; if (activeFence) { if (isClosingCodeFence(line, activeFence)) { activeFence = undefined; } index += 1; continue; } const opening = matchOpeningCodeFence(line); if (opening) { activeFence = opening; index += 1; continue; } if (line.trim().startsWith("|")) { let end = index; while (end < lines.length && lines[end].trim().startsWith("|")) { end += 1; } const block = lines.slice(index, end); const alignments = block.length >= 2 ? separatorAlignments(block[1]) : undefined; if (alignments) { const indentMatch = /^\s*/.exec(block[0]); const rebuilt = rebuildTableBlock(block, alignments, indentMatch ? indentMatch[0] : ""); for (let offset = 0; offset < rebuilt.length; offset += 1) { output[index + offset] = rebuilt[offset]; } } index = end; continue; } index += 1; } return output.map((line, lineIndex) => line + endings[lineIndex]).join("\n"); } Markdown.formatMarkdownTables = formatMarkdownTables; function applyTableFormatToTextarea() { const root = document.getElementById(EDITOR_ROOT_ID); const textarea = root?.querySelector("[data-tlf-editor-textarea]"); if (!textarea) { return; } const value = textarea.value; const formatted = formatMarkdownTables(value); if (formatted === value) { return; } const scrollTop = textarea.scrollTop; const caret = Math.min(textarea.selectionStart ?? 0, formatted.length); TabLocalFiles.TextEditor.applyUndoableEdit(textarea, { replacement: formatted, rangeStart: 0, rangeEnd: value.length, selectionStart: caret, selectionEnd: caret }); textarea.scrollTop = scrollTop; } function bindMarkdownFormatShortcuts() { const root = document.getElementById(EDITOR_ROOT_ID); if (!root || typeof root.addEventListener !== "function") { return; } root.addEventListener("keydown", (event) => { if (!(event.metaKey || event.ctrlKey) || !TabLocalFiles.isDocumentEditing()) { return; } if (TabLocalFiles.isTextCompositionEvent(event)) { return; } const action = formatActionForShortcut({ alt: event.altKey, shift: event.shiftKey, code: event.code }); if (!action) { return; } const target = event.target; if (target instanceof HTMLInputElement || target instanceof HTMLSelectElement) { return; } if (target instanceof HTMLTextAreaElement && !target.hasAttribute("data-tlf-editor-textarea")) { return; } if (TabLocalFiles.triggerToolbarAction(action)) { event.preventDefault(); } }); } function bindMarkdownFormatActions() { const transforms = { "markdown.bold": wrapInlineTransform("**", TabLocalFiles.t("markdown.bold")), "markdown.italic": wrapInlineTransform("_", TabLocalFiles.t("markdown.italic")), "markdown.strike": wrapInlineTransform("~~", TabLocalFiles.t("markdown.strikethrough")), "markdown.inline-code": wrapInlineTransform("`", TabLocalFiles.t("markdown.inlineCode")), "markdown.h1": linePrefixTransform("# "), "markdown.h2": linePrefixTransform("## "), "markdown.h3": linePrefixTransform("### "), "markdown.h4": linePrefixTransform("#### "), "markdown.h5": linePrefixTransform("##### "), "markdown.ul": linePrefixTransform("- "), "markdown.ol": linePrefixTransform("1. "), "markdown.task": linePrefixTransform("- [ ] "), "markdown.quote": linePrefixTransform("> "), "markdown.codeblock": snippetTransform("\n```\n$SEL\n```\n", TabLocalFiles.t("markdown.codeBlock")), "markdown.link": snippetTransform("[$SEL](https://)", TabLocalFiles.t("markdown.insertLink")), "markdown.image": snippetTransform("![$SEL](./image.png)", TabLocalFiles.t("markdown.insertImage")), "markdown.table": snippetTransform(`\n| ${TabLocalFiles.t("markdown.tableHeader")} | ${TabLocalFiles.t("markdown.tableHeader")} |\n| --- | --- |\n| ${TabLocalFiles.t("markdown.tableCell")} | ${TabLocalFiles.t("markdown.tableCell")} |\n`, ""), "markdown.hr": snippetTransform("\n\n---\n\n", ""), "markdown.clear": clearFormatTransform() }; Object.entries(transforms).forEach(([action, transform]) => { TabLocalFiles.onToolbarAction(action, () => { if (TabLocalFiles.MarkdownInlineEdit.runToolbarAction(action)) { return; } TabLocalFiles.TextEditor.transformSelection(EDITOR_ROOT_ID, transform); }); }); TabLocalFiles.onToolbarAction("markdown.format-tables", () => { if (TabLocalFiles.MarkdownInlineEdit.runToolbarAction("markdown.format-tables")) { return; } applyTableFormatToTextarea(); }); } function wrapInlineTransform(marker, placeholder) { return (value, start, end) => { const markerLength = marker.length; const selected = value.slice(start, end); const before = value.slice(Math.max(0, start - markerLength), start); const after = value.slice(end, end + markerLength); if (before === marker && after === marker) { return { replacement: selected, rangeStart: start - markerLength, rangeEnd: end + markerLength, selectionStart: start - markerLength, selectionEnd: end - markerLength }; } if (selected.length >= markerLength * 2 && selected.startsWith(marker) && selected.endsWith(marker)) { const inner = selected.slice(markerLength, selected.length - markerLength); return { replacement: inner, rangeStart: start, rangeEnd: end, selectionStart: start, selectionEnd: start + inner.length }; } const text = selected || placeholder; return { replacement: `${marker}${text}${marker}`, rangeStart: start, rangeEnd: end, selectionStart: start + markerLength, selectionEnd: start + markerLength + text.length }; }; } Markdown.wrapInlineTransform = wrapInlineTransform; const LINE_PREFIX_PATTERN = /^(\s*)(#{1,6}\s+|>\s?|[-+*]\s+\[[ xX]\]\s+|[-+*]\s+|\d+[.)]\s+)?/; function linePrefixKind(prefix) { if (/^#{1,6}\s+$/.test(prefix)) { return `heading-${prefix.trim().length}`; } if (/^>\s?$/.test(prefix)) { return "quote"; } if (/^[-+*]\s+\[[ xX]\]\s+$/.test(prefix)) { return "task"; } if (/^[-+*]\s+$/.test(prefix)) { return "ul"; } if (/^\d+[.)]\s+$/.test(prefix)) { return "ol"; } return "none"; } function linePrefixTransform(prefix) { const targetKind = linePrefixKind(prefix); return (value, start, end) => { const blockStart = value.lastIndexOf("\n", Math.max(0, start - 1)) + 1; const blockEndRaw = value.indexOf("\n", Math.max(start, end)); const blockEnd = blockEndRaw === -1 ? value.length : blockEndRaw; const lines = value.slice(blockStart, blockEnd).split("\n"); const contentLines = lines.filter((line) => line.trim()); const allTargeted = contentLines.length > 0 && contentLines.every((line) => { const match = line.match(LINE_PREFIX_PATTERN); return linePrefixKind(match?.[2] || "") === targetKind; }); let orderIndex = 0; const replacement = lines .map((line) => { if (!line.trim()) { return line; } orderIndex += 1; const match = line.match(LINE_PREFIX_PATTERN); const indent = match?.[1] || ""; const rest = line.slice((match?.[0] || "").length); if (allTargeted) { return `${indent}${rest}`; } const linePrefix = targetKind === "ol" ? `${orderIndex}. ` : prefix; return `${indent}${linePrefix}${rest}`; }) .join("\n"); return { replacement, rangeStart: blockStart, rangeEnd: blockEnd, selectionStart: blockStart, selectionEnd: blockStart + replacement.length }; }; } Markdown.linePrefixTransform = linePrefixTransform; function snippetTransform(template, placeholder) { return (value, start, end) => { const selected = value.slice(start, end) || placeholder; const selectionIndex = template.indexOf("$SEL"); const replacement = selectionIndex === -1 ? template : template.replace("$SEL", selected); const selectionStart = selectionIndex === -1 ? start + replacement.length : start + selectionIndex; const selectionEnd = selectionIndex === -1 ? start + replacement.length : start + selectionIndex + selected.length; return { replacement, rangeStart: start, rangeEnd: end, selectionStart, selectionEnd }; }; } Markdown.snippetTransform = snippetTransform; function clearFormatTransform() { return (value, start, end) => { const blockStart = value.lastIndexOf("\n", Math.max(0, start - 1)) + 1; const blockEndRaw = value.indexOf("\n", Math.max(start, end)); const blockEnd = blockEndRaw === -1 ? value.length : blockEndRaw; const replacement = value .slice(blockStart, blockEnd) .split("\n") .map((line) => { const match = line.match(LINE_PREFIX_PATTERN); const indent = match?.[1] || ""; const rest = line.slice((match?.[0] || "").length); return `${indent}${rest.replace(/(\*\*|__|~~|\*|_|`)/g, "")}`; }) .join("\n"); return { replacement, rangeStart: blockStart, rangeEnd: blockEnd, selectionStart: blockStart, selectionEnd: blockStart + replacement.length }; }; } Markdown.clearFormatTransform = clearFormatTransform; function syncHeadingSurfaces(value, context) { const parsed = markdownToHtml(value, context.sourceUrl); applyDocumentTitle(context, value, parsed.outline); const tocList = document.querySelector("[data-tlf-md-toc-list]"); if (!tocList) { return; } const collapsedIds = new Set(); tocList.querySelectorAll('[data-tlf-toc-collapsed="true"]').forEach((node) => { const id = node.dataset.tlfTocNode; if (id) { collapsedIds.add(id); } }); tocList.innerHTML = TabLocalFiles.renderOutline(tocOutline(parsed), TabLocalFiles.EXTENSION_CONFIG.maxOutlineLevel, { collapsible: true, collapsedIds }); } function titleCandidate(source, outline) { return (TabLocalFiles.DocumentTitle.fromFrontmatter(source) || outline.find((item) => item.level === 1)?.text || TabLocalFiles.DocumentTitle.fromLeadingHeading(source)); } function applyDocumentTitle(context, source, outline) { TabLocalFiles.DocumentTitle.apply(context, titleCandidate(source, outline), context.fileTitle); } function toggleWholeOutline(tocPanel, button) { const nodes = Array.from(tocPanel.querySelectorAll("[data-tlf-toc-node]")).filter((node) => node.querySelector("[data-tlf-toc-toggle]")); if (nodes.length === 0) { return; } const collapse = nodes.some((node) => node.dataset.tlfTocCollapsed !== "true"); nodes.forEach((node) => { if (collapse) { node.dataset.tlfTocCollapsed = "true"; } else { delete node.dataset.tlfTocCollapsed; } node.querySelector("[data-tlf-toc-toggle]")?.setAttribute("aria-expanded", String(!collapse)); }); const label = TabLocalFiles.t(collapse ? "outline.expandAll" : "outline.collapseAll"); button.setAttribute("aria-label", label); button.setAttribute("title", label); button.innerHTML = TabLocalFiles.renderLucideIcon(collapse ? "chevrons-up-down" : "chevrons-down-up"); } function bindMarkdownTocNavigation() { const tocPanel = document.getElementById(TOC_PANEL_ID); if (!tocPanel || tocPanel.dataset.tlfMarkdownOutlineBound === "true") { return; } tocPanel.dataset.tlfMarkdownOutlineBound = "true"; tocPanel.addEventListener("click", (event) => { const foldAll = event.target?.closest("[data-tlf-md-toc-fold]"); if (foldAll) { event.preventDefault(); toggleWholeOutline(tocPanel, foldAll); return; } const toggle = event.target?.closest("[data-tlf-toc-toggle]"); if (toggle) { event.preventDefault(); const node = toggle.closest("[data-tlf-toc-node]"); if (node) { const expand = node.dataset.tlfTocCollapsed === "true"; if (expand) { delete node.dataset.tlfTocCollapsed; } else { node.dataset.tlfTocCollapsed = "true"; } toggle.setAttribute("aria-expanded", String(expand)); } return; } const link = event.target?.closest(".tlf-toc-link[href^='#']"); if (!link) { return; } const targetId = decodeHashId(link.getAttribute("href") || ""); const target = targetId ? document.getElementById(targetId) : null; if (!target) { return; } event.preventDefault(); setCurrentOutlineLink(link); target.scrollIntoView({ behavior: "smooth", block: "start" }); }); } function bindMarkdownPreviewAnchors() { const root = document.getElementById(EDITOR_ROOT_ID); const preview = root?.querySelector("[data-tlf-editor-preview]"); if (!preview || preview.dataset.tlfMarkdownAnchorsBound === "true") { return; } preview.dataset.tlfMarkdownAnchorsBound = "true"; preview.addEventListener("click", (event) => { const link = event.target?.closest("a[href^='#']"); if (!link) { return; } const target = findAnchorTarget(preview, link.getAttribute("href") || ""); if (!target) { return; } event.preventDefault(); target.scrollIntoView({ behavior: "smooth", block: "start" }); }); } function bindMarkdownPreviewTaskCheckboxes() { const root = document.getElementById(EDITOR_ROOT_ID); const preview = root?.querySelector("[data-tlf-editor-preview]"); const textarea = root?.querySelector("[data-tlf-editor-textarea]"); if (!preview || !textarea || preview.dataset.tlfMarkdownTasksBound === "true") { return; } preview.dataset.tlfMarkdownTasksBound = "true"; preview.addEventListener("change", (event) => { const checkbox = event.target?.closest("input[data-tlf-markdown-task]"); if (!checkbox || checkbox.disabled) { return; } const currentBody = TabLocalFiles.MarkdownInlineEdit.splitFrontmatter(textarea.value).body; if (checkbox.dataset.tlfMarkdownTaskRevision !== markdownTaskRevision(currentBody)) { TabLocalFiles.TextEditor.refreshPreview(EDITOR_ROOT_ID); return; } const ordinal = Number(checkbox.dataset.tlfMarkdownTask); const edit = taskListMarkerEdit(textarea.value, ordinal, checkbox.checked, textarea.selectionStart, textarea.selectionEnd); if (!edit) { TabLocalFiles.TextEditor.refreshPreview(EDITOR_ROOT_ID); return; } TabLocalFiles.TextEditor.applyUndoableEditorEdit(EDITOR_ROOT_ID, edit); }); } function findAnchorTarget(preview, hash) { const targetId = decodeHashId(hash); if (!targetId) { return null; } const exact = document.getElementById(targetId); if (exact) { return exact; } const normalizedId = normalizeAnchorId(targetId); return (Array.from(preview.querySelectorAll("[id]")).find((candidate) => normalizeAnchorId(candidate.id) === normalizedId) || null); } function normalizeAnchorId(id) { return id.toLowerCase().replace(/-+/g, "-"); } function decodeHashId(hash) { const rawId = hash.startsWith("#") ? hash.slice(1) : hash; try { return decodeURIComponent(rawId); } catch { return rawId; } } function tocIndexForScrollTop(offsets, scrollTop, threshold) { const readingLine = scrollTop + threshold; let current = -1; for (let index = 0; index < offsets.length; index += 1) { if (offsets[index] <= readingLine) { current = index; } else { break; } } return current; } Markdown.tocIndexForScrollTop = tocIndexForScrollTop; const TOC_SCROLL_SPY_THRESHOLD_PX = 80; const TOC_SCROLL_SPY_INTERVAL_MS = 100; function bindMarkdownTocScrollSpy() { const root = document.getElementById(EDITOR_ROOT_ID); const preview = root?.querySelector("[data-tlf-editor-preview]"); const tocPanel = document.querySelector(".tlf-markdown-toc"); if (!preview || !tocPanel || typeof preview.addEventListener !== "function") { return; } let pending = false; const update = () => { pending = false; if (tocPanel.hidden) { return; } const headings = Array.from(preview.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")); if (!headings.length) { return; } const previewTop = preview.getBoundingClientRect().top; const offsets = headings.map((heading) => heading.getBoundingClientRect().top - previewTop + preview.scrollTop); const index = tocIndexForScrollTop(offsets, preview.scrollTop, TOC_SCROLL_SPY_THRESHOLD_PX); if (index < 0) { tocPanel.querySelectorAll(".tlf-toc-link[aria-current]").forEach((link) => { link.removeAttribute("aria-current"); }); return; } const id = headings[index].id; const link = tocPanel.querySelector(`.tlf-toc-link[href="#${CSS.escape(id)}"]`); if (!link || link.getAttribute("aria-current") === "true") { return; } setCurrentOutlineLink(link); try { link.scrollIntoView({ block: "nearest" }); } catch { } }; preview.addEventListener("scroll", () => { if (pending) { return; } pending = true; window.setTimeout(update, TOC_SCROLL_SPY_INTERVAL_MS); }); tocPanel.addEventListener("tlf:toc-visible", update); } function setCurrentOutlineLink(currentLink) { const tocPanel = currentLink.closest(".tlf-markdown-toc"); tocPanel?.querySelectorAll(".tlf-toc-link[aria-current]").forEach((link) => { link.removeAttribute("aria-current"); }); currentLink.setAttribute("aria-current", "true"); } function markdownToHtml(source, sourceUrl) { const finish = TabLocalFiles.PerformanceMetrics.begin("markdown:to-html", { sourceChars: source.length }); const { frontmatter, body } = TabLocalFiles.MarkdownInlineEdit.splitFrontmatter(source); const marked = window.marked; const parsed = marked ? renderWithMarkedSetup(body, marked, sourceUrl, countNewlines(frontmatter)) : legacyMarkdownToHtml(body, sourceUrl); finish({ htmlChars: parsed.html.length, headings: parsed.outline.length }); return { ...parsed, hasFrontmatter: Boolean(frontmatter) }; } function headingIds(source) { const { body } = TabLocalFiles.MarkdownInlineEdit.splitFrontmatter(source); const marked = window.marked; const parsed = marked ? renderWithMarkedSetup(body, marked, "") : legacyMarkdownToHtml(body, ""); return parsed.outline.map((item) => item.id); } Markdown.headingIds = headingIds; function renderWithMarkedSetup(source, marked, sourceUrl, lineOffset = 0) { registerMarkedMath(marked); registerMarkedFootnotes(marked); registerMarkedSvg(marked); registerMarkedSourceLines(marked); registerMarkedSoftWraps(marked); const preprocessed = preprocessMarkdown(source); currentSourceLineMap = { hunks: preprocessed === source ? [] : TabLocalFiles.ExternalChanges.diffLineHunks(preprocessed, source), offset: lineOffset }; try { return renderWithMarked(preprocessed, marked, sourceUrl, markdownTaskRevision(source)); } finally { currentSourceLineMap = undefined; } } let currentSourceLineMap; let markedSourceLinesRegistered = false; const SOURCE_LINE_RENDERERS = ["heading", "paragraph", "code", "blockquote", "list", "listitem", "table", "html", "hr"]; function registerMarkedSourceLines(marked) { if (markedSourceLinesRegistered) { return; } marked.use({ hooks: { processAllTokens: (tokens) => { if ("links" in tokens) { annotateTokenLines(tokens); } return tokens; } } }); markedSourceLinesRegistered = true; } let markedSoftWrapsRegistered = false; function registerMarkedSoftWraps(marked) { if (markedSoftWrapsRegistered) { return; } marked.use({ hooks: { processAllTokens: (tokens) => { if ("links" in tokens) { markSoftWraps(tokens); } return tokens; } } }); markedSoftWrapsRegistered = true; } function markSoftWraps(tokens) { for (const token of tokens) { if (token.type === "blockquote") { continue; } if (token.type === "list") { for (const item of token.items ?? []) { markSoftWraps(item.tokens ?? []); } continue; } if ((token.type === "paragraph" || token.type === "text" || token.type === "tlfFootnoteDef") && token.tokens) { markInlineSoftWraps(token.tokens, (token.text ?? token.raw).split("\n")); } } } function markInlineSoftWraps(tokens, lines) { let boundary = 0; const visit = (inline) => { for (const token of inline) { if (token.type === "br") { if (/^ ?\n$/.test(token.raw) && TabLocalFiles.SoftWrap.joins(lines[boundary] ?? "", lines[boundary + 1] ?? "")) { token.tlfSoftWrap = true; } boundary += 1; } else if (token.tokens?.length) { visit(token.tokens); } else { boundary += countNewlines(token.raw); } } }; visit(tokens); } function countNewlines(value) { let count = 0; for (let index = value.indexOf("\n"); index !== -1; index = value.indexOf("\n", index + 1)) { count += 1; } return count; } function annotateTokenLines(tokens) { let line = 0; for (const token of tokens) { const raw = typeof token.raw === "string" ? token.raw : ""; const content = raw.replace(/\n+$/, ""); if (content) { token.tlfLines = [line, line + countNewlines(content) + 1]; const items = token.items; if (Array.isArray(items)) { let itemLine = line; for (const item of items) { const itemRaw = typeof item.raw === "string" ? item.raw : ""; const itemContent = itemRaw.replace(/\n+$/, ""); if (itemContent) { item.tlfLines = [itemLine, itemLine + countNewlines(itemContent) + 1]; } itemLine += countNewlines(itemRaw); } } } line += countNewlines(raw); } } Markdown.annotateTokenLines = annotateTokenLines; function withSourceLines(html, token, map) { const lines = token.tlfLines; if (!map || !Array.isArray(lines) || lines.length !== 2) { return html; } const [start, end] = lines; const fileStart = TabLocalFiles.ExternalChanges.mapLine(map.hunks, start, -1) + map.offset; const fileEnd = Math.max(fileStart + 1, TabLocalFiles.ExternalChanges.mapLine(map.hunks, end, 1) + map.offset); return html.replace(/^(\s*<[a-zA-Z][\w-]*)(?=[\s>])/, `$1 data-tlf-src-lines="${fileStart}-${fileEnd}"`); } Markdown.withSourceLines = withSourceLines; function wrapRenderersWithSourceLines(renderer) { const methods = renderer; for (const name of SOURCE_LINE_RENDERERS) { const original = methods[name]; if (typeof original !== "function") { continue; } methods[name] = function wrapped(token) { return withSourceLines(original.call(this, token), token, currentSourceLineMap); }; } } function tocOutline(parsed) { if (!parsed.hasFrontmatter) { return parsed.outline; } return [ { id: TabLocalFiles.MarkdownInlineEdit.FRONTMATTER_PANEL_ID, level: 1, text: TabLocalFiles.t("markdown.frontmatter") }, ...parsed.outline ]; } const MARKDOWN_ALERT_KINDS = { note: { icon: "info", labelKey: "markdown.alertNote" }, tip: { icon: "lightbulb", labelKey: "markdown.alertTip" }, important: { icon: "message-square-warning", labelKey: "markdown.alertImportant" }, warning: { icon: "triangle-alert", labelKey: "markdown.alertWarning" }, caution: { icon: "octagon-alert", labelKey: "markdown.alertCaution" }, abstract: { icon: "align-left", labelKey: "markdown.alertAbstract" }, info: { icon: "info", labelKey: "markdown.alertInfo" }, todo: { icon: "circle-check", labelKey: "markdown.alertTodo" }, success: { icon: "check", labelKey: "markdown.alertSuccess" }, question: { icon: "circle-help", labelKey: "markdown.alertQuestion" }, failure: { icon: "x", labelKey: "markdown.alertFailure" }, danger: { icon: "zap", labelKey: "markdown.alertDanger" }, bug: { icon: "octagon-alert", labelKey: "markdown.alertBug" }, example: { icon: "list", labelKey: "markdown.alertExample" }, quote: { icon: "quote", labelKey: "markdown.alertQuote" } }; const MARKDOWN_ALERT_ALIASES = { summary: "abstract", tldr: "abstract", hint: "tip", check: "success", done: "success", help: "question", faq: "question", attention: "warning", fail: "failure", missing: "failure", error: "danger", cite: "quote" }; function renderAlertShellHtml(markerLine, sourceUrl) { const html = markdownToHtml(`> ${markerLine}`, sourceUrl).html.trim(); return /^<(?:div|details) [^>]*class="tlf-alert /.test(html) ? html.replace(/ data-tlf-src-lines="[^"]*"/, "") : ""; } Markdown.renderAlertShellHtml = renderAlertShellHtml; function markdownAlertFromBlockquote(bodyHtml) { const match = /^\s*

\[!([A-Za-z][\w-]*)\]([+-]?)[ \t]*((?:(?!|<\/p>)[\s\S])*?)[ \t]*(\s*|<\/p>\s*)/.exec(bodyHtml); if (!match) { return undefined; } const type = match[1]; const lowerType = type.toLowerCase(); const kind = MARKDOWN_ALERT_ALIASES[lowerType] ?? (MARKDOWN_ALERT_KINDS[lowerType] ? lowerType : "note"); const rest = match[4].startsWith("${bodyHtml.slice(match[0].length)}` : bodyHtml.slice(match[0].length); return { kind, type, titleHtml: match[3] || undefined, fold: match[2] === "-" ? "closed" : match[2] === "+" ? "open" : undefined, bodyHtml: rest }; } Markdown.markdownAlertFromBlockquote = markdownAlertFromBlockquote; function renderMarkdownAlert(alert) { const descriptor = MARKDOWN_ALERT_KINDS[alert.kind]; const known = Boolean(MARKDOWN_ALERT_KINDS[alert.type.toLowerCase()] || MARKDOWN_ALERT_ALIASES[alert.type.toLowerCase()]); const title = alert.titleHtml ?? TabLocalFiles.escapeHtml(known ? TabLocalFiles.t(descriptor.labelKey) : alert.type.charAt(0).toUpperCase() + alert.type.slice(1)); const icon = TabLocalFiles.renderLucideIcon(descriptor.icon); const className = `tlf-alert tlf-alert-${alert.kind}`; if (alert.fold) { return (`

` + `${icon}${title}` + `${TabLocalFiles.renderLucideIcon("chevron-right")}` + alert.bodyHtml + `
\n`); } return `

${icon}${title}

${alert.bodyHtml}
\n`; } function renderFootnoteSection(state) { if (!state.order.length) { return ""; } const items = state.order .map((label) => { const body = state.defs.get(label); if (body === undefined) { return ""; } return (`
  • ` + `${body}` + `↩` + `
  • `); }) .join(""); if (!items) { return ""; } return `
      ${items}
    `; } function registerMarkedFootnotes(marked) { if (markedFootnotesRegistered) { return; } marked.use({ extensions: [ { name: "tlfFootnoteDef", level: "block", start: (source) => { const match = source.match(/^\[\^[^\]\s]+\]:/m); return match ? match.index : undefined; }, tokenizer(source) { const match = /^\[\^([^\]\s]+)\]:[ \t]*([^\n]*(?:\n(?:[ ]{4}|\t)[^\n]*|\n[ \t]*(?=\n(?:[ ]{4}|\t)))*)(?:\n+|$)/.exec(source); if (!match) { return undefined; } const label = match[1]; currentFootnotes?.labels.add(label); const text = match[2].replace(/\n(?:[ ]{4}|\t)/g, "\n").trim(); const token = { type: "tlfFootnoteDef", raw: match[0], text, label, tokens: [] }; this.lexer.inline(text, token.tokens); return token; }, renderer(token) { const state = currentFootnotes; if (state) { const label = String(token.label || ""); const body = Array.isArray(token.tokens) ? this.parser.parseInline(token.tokens) : TabLocalFiles.escapeHtml(String(token.text || "")); state.defs.set(label, body); } return ""; } }, { name: "tlfFootnoteRef", level: "inline", start: (source) => source.indexOf("[^"), tokenizer(source) { const match = /^\[\^([^\]\s]+)\](?!:)/.exec(source); if (!match) { return undefined; } if (!currentFootnotes?.labels.has(match[1])) { return undefined; } return { type: "tlfFootnoteRef", raw: match[0], label: match[1] }; }, renderer(token) { const state = currentFootnotes; const label = String(token.label || ""); if (!state) { return TabLocalFiles.escapeHtml(String(token.raw || "")); } let index = state.order.indexOf(label); if (index === -1) { state.order.push(label); index = state.order.length - 1; } const idAttr = state.referenced.has(label) ? "" : ` id="tlf-fnref-${TabLocalFiles.escapeAttr(label)}"`; state.referenced.add(label); return `${index + 1}`; } } ] }); markedFootnotesRegistered = true; } let markedSvgRegistered = false; function registerMarkedSvg(marked) { if (markedSvgRegistered) { return; } marked.use({ extensions: [ { name: "tlfSvgBlock", level: "block", start: (source) => source.match(/^/i.exec(source); if (!match) { return undefined; } return { type: "tlfSvgBlock", raw: match[0], text: match[0] }; }, renderer(token) { const svg = sanitizeSvgMarkup(String(token.text || "")); return svg ? `
    ${svg}
    \n` : ""; } }, { name: "tlfSvgInline", level: "inline", start: (source) => { const match = source.match(//i.exec(source); if (!match) { return undefined; } return { type: "tlfSvgInline", raw: match[0], text: match[0] }; }, renderer(token) { return sanitizeSvgMarkup(String(token.text || "")) ?? ""; } } ] }); markedSvgRegistered = true; } function renderWithMarked(source, marked, sourceUrl, taskRevision) { const outline = []; const usedIds = new Map(); const renderer = new marked.Renderer(); const defaultTable = renderer.table.bind(renderer); const editableTasks = TabLocalFiles.isEditableLocalFileSourceUrl(sourceUrl); const taskLabel = TabLocalFiles.t(editableTasks ? "markdown.taskToggle" : "markdown.taskReadonlyRemote"); let taskIndex = 0; let renderDiagramIndex = 0; renderer.br = (token) => (token.tlfSoftWrap === true ? '
    ' : "
    "); renderer.checkbox = (token) => { const checked = token.checked ? ' checked=""' : ""; const accessibility = ` aria-label="${TabLocalFiles.escapeAttr(taskLabel)}" title="${TabLocalFiles.escapeAttr(taskLabel)}"`; if (!editableTasks) { return ``; } const index = taskIndex; taskIndex += 1; return ``; }; renderer.heading = function heading(token) { const level = Math.min(Math.max(Number(token.depth) || 1, 1), 6); const text = plainTextFromTokens(token.tokens, token.text || ""); const id = uniqueHeadingId(text, usedIds); outline.push({ id, level, text }); return `${parseMarkedInlineTokens(this, token.tokens, token.text || "")}`; }; renderer.code = (token) => { const language = normalizeCodeLanguage(token.lang || ""); const code = token.escaped ? token.text || "" : token.text || ""; if (language === "mermaid") { renderDiagramIndex += 1; return renderDiagramBlock(code, isMermaidMindmapSyntax(code), renderDiagramIndex); } if (language === "svg") { const svg = sanitizeSvgMarkup(code); if (svg) { return `
    ${svg}
    `; } } const className = language ? ` class="language-${TabLocalFiles.escapeAttr(language)}"` : ""; return `
    ${TabLocalFiles.escapeHtml(code)}
    `; }; renderer.codespan = (token) => renderInlineCode(token.text || ""); renderer.blockquote = function blockquote(token) { const body = this.parser && token.tokens ? this.parser.parse(token.tokens) : ""; const alert = body ? markdownAlertFromBlockquote(body) : undefined; if (!alert) { return `
    \n${body}
    \n`; } return renderMarkdownAlert(alert); }; renderer.html = (token) => sanitizeRawHtml(token.text || token.raw || "", sourceUrl); renderer.link = function link(token) { const href = resolveMarkdownLinkUrl(token.href || "", sourceUrl); const title = token.title ? ` title="${TabLocalFiles.escapeAttr(token.title)}"` : ""; return `${parseMarkedInlineTokens(this, token.tokens, token.text || "")}`; }; renderer.image = (token) => { const src = resolveMarkdownImageUrl(token.href || "", sourceUrl); const title = token.title ? ` title="${TabLocalFiles.escapeAttr(token.title)}"` : ""; return `${TabLocalFiles.escapeAttr(token.text || `; }; const defaultParagraph = renderer.paragraph.bind(renderer); renderer.paragraph = (token) => { const html = defaultParagraph(token); const onlyImage = token.tokens?.length === 1 && token.tokens[0].type === "image"; return onlyImage ? html.replace(/^

    /, '

    ') : html; }; renderer.table = function table(token) { return `

    ${defaultTable.call(this, token)}
    `; }; const footnotes = { labels: new Set(), defs: new Map(), order: [], referenced: new Set() }; currentFootnotes = footnotes; wrapRenderersWithSourceLines(renderer); let html; try { html = marked.parse(source, { async: false, breaks: true, gfm: true, renderer }); } finally { currentFootnotes = undefined; } return { html: typeof html === "string" ? html + renderFootnoteSection(footnotes) : legacyMarkdownToHtml(source, sourceUrl).html, outline }; } function renderDiagramBlock(code, mindmap, index) { const id = `tlf-markdown-diagram-${Date.now().toString(36)}-${diagramIdSeed += 1}-${index}`; const kind = mindmap ? "mindmap" : "mermaid"; const label = mindmap ? "Mindmap" : "Mermaid"; const markdownSource = mindmap ? convertMermaidMindmapToMarkdown(code) : code.trim(); const source = mindmap ? markdownSource : code.trim(); const dataAttribute = mindmap ? "data-tlf-mindmap-diagram" : "data-tlf-mermaid-diagram"; return `
    ${label}
    ${TabLocalFiles.escapeHtml(code.trim())}
    `; } function escapeScriptRawText(value) { return value.replace(/<\//g, "<\\/"); } function isMermaidMindmapSyntax(value) { return MERMAID_MINDMAP_PREFIX.test(value.trimStart()); } function convertMermaidMindmapToMarkdown(value) { if (!isMermaidMindmapSyntax(value)) { return value.trim(); } const lines = value .split(/\r?\n/) .filter((line) => line.trim()) .slice(1); const levelStack = []; const markdownLines = []; for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine || trimmedLine.startsWith("%%")) { continue; } const label = extractMindmapLabel(trimmedLine); if (!label) { continue; } const indent = countLeadingIndent(line); while (levelStack.length > 1 && indent < levelStack[levelStack.length - 1]) { levelStack.pop(); } if (!levelStack.length) { levelStack.push(indent); } else if (indent > levelStack[levelStack.length - 1]) { levelStack.push(indent); } markdownLines.push(`${" ".repeat(Math.max(levelStack.length - 1, 0))}- ${label}`); } return markdownLines.join("\n"); } function countLeadingIndent(line) { let width = 0; for (const char of line) { if (char === " ") { width += 1; } else if (char === "\t") { width += 2; } else { break; } } return width; } function extractMindmapLabel(value) { const cleanedValue = stripMermaidDirectives(value); if (!cleanedValue || cleanedValue.startsWith("::")) { return ""; } for (const pattern of MERMAID_LABEL_PATTERNS) { const matched = cleanedValue.match(pattern); if (matched?.[1]) { return matched[1].trim(); } } return cleanedValue.trim(); } function stripMermaidDirectives(value) { let nextValue = value.trim(); let didChange = true; while (didChange) { didChange = false; for (const pattern of MERMAID_DIRECTIVE_SUFFIXES) { const replaced = nextValue.replace(pattern, ""); if (replaced !== nextValue) { nextValue = replaced.trim(); didChange = true; } } } return nextValue; } function renderInlineCode(value) { const link = inlineCodeLink(value); if (!link) { return `${TabLocalFiles.escapeHtml(value)}`; } return `${TabLocalFiles.escapeHtml(link.label)}`; } function inlineCodeLink(value) { const codeText = value.trim(); if (!codeText || /[\r\n]/.test(codeText)) { return undefined; } const angleAutolinkMatch = codeText.match(INLINE_CODE_ANGLE_AUTOLINK_PATTERN); if (angleAutolinkMatch) { return { href: angleAutolinkMatch[1], label: angleAutolinkMatch[1] }; } if (INLINE_CODE_BARE_URL_PATTERN.test(codeText)) { return { href: normalizeInlineCodeLinkHref(codeText), label: codeText }; } return undefined; } function normalizeInlineCodeLinkHref(url) { return /^www\./i.test(url) ? `https://${url}` : url; } function preprocessMarkdown(source) { return transformMarkdownOutsideFences(source, (chunk) => transformOutsideCodeSpans(chunk, (text) => addCjkAutolinkBoundaries(normalizeMarkdownMathDelimiters(text)))); } function transformOutsideCodeSpans(chunk, transform) { const spans = []; let masked = ""; let index = 0; while (index < chunk.length) { const char = chunk[index]; if (char === "\\") { masked += chunk.slice(index, index + 2); index += 2; continue; } if (char !== "`") { masked += char; index += 1; continue; } const runEnd = backtickRunEnd(chunk, index); const close = closingBacktickRun(chunk, runEnd, runEnd - index); if (close < 0) { masked += chunk.slice(index, runEnd); index = runEnd; continue; } const end = close + (runEnd - index); masked += `\uE010${spans.length}\uE011`; spans.push(chunk.slice(index, end)); index = end; } return transform(masked).replace(/\uE010(\d+)\uE011/g, (_match, n) => spans[Number(n)]); } function backtickRunEnd(text, start) { let end = start; while (text[end] === "`") { end += 1; } return end; } function closingBacktickRun(text, from, length) { let index = from; while (index < text.length) { if (text[index] === "`") { const end = backtickRunEnd(text, index); if (end - index === length) { return index; } index = end; continue; } if (text[index] === "\n" && /^\n[ \t]*\n/.test(text.slice(index, index + 64))) { return -1; } index += 1; } return -1; } function getLineEnd(content, lineStart) { const lineEnd = content.indexOf("\n", lineStart); return lineEnd === -1 ? content.length : lineEnd; } function matchOpeningCodeFence(line) { const match = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/); if (!match) { return undefined; } const fence = match[1]; return { marker: fence[0], length: fence.length }; } function isClosingCodeFence(line, openingFence) { const match = line.match(/^[ \t]{0,3}(`{3,}|~{3,})[ \t]*$/); return Boolean(match && match[1][0] === openingFence.marker && match[1].length >= openingFence.length); } function transformMarkdownOutsideFences(source, transform) { const normalized = source.replace(/\r\n?/g, "\n"); const parts = []; let cursor = 0; let chunkStart = 0; let activeFence; while (cursor < normalized.length) { const lineEnd = getLineEnd(normalized, cursor); const line = normalized.slice(cursor, lineEnd); if (!activeFence) { const opening = matchOpeningCodeFence(line); if (opening) { parts.push(transform(normalized.slice(chunkStart, cursor))); activeFence = opening; chunkStart = cursor; } } else if (isClosingCodeFence(line, activeFence)) { const fenceEnd = lineEnd === normalized.length ? lineEnd : lineEnd + 1; parts.push(normalized.slice(chunkStart, fenceEnd)); activeFence = undefined; chunkStart = fenceEnd; } cursor = lineEnd === normalized.length ? lineEnd : lineEnd + 1; } if (chunkStart < normalized.length) { const tail = normalized.slice(chunkStart); parts.push(activeFence ? tail : transform(tail)); } return parts.join(""); } function normalizeMarkdownMathDelimiters(content) { const displayNormalized = content.replace(/(^|\n)([ \t]*)\\\[([\s\S]*?)\\\]/g, (_match, lineStart, linePrefix, mathContent) => `${lineStart}${normalizeDisplayMathBlock(mathContent, linePrefix)}`).replace(/(^|\n)([ \t]*)\$\$([\s\S]*?)\$\$/g, (_match, lineStart, linePrefix, mathContent) => `${lineStart}${normalizeDisplayMathBlock(mathContent, linePrefix)}`); return displayNormalized.replace(/\\\(([\s\S]*?)\\\)/g, (_match, mathContent) => `$${normalizeLatexMathContent(mathContent).trim()}$`); } function normalizeDisplayMathBlock(mathContent, linePrefix = "") { const normalizedMathContent = normalizeLatexMathContent(mathContent).trim(); if (!linePrefix) { return `$$\n${normalizedMathContent}\n$$`; } const prefixedMathContent = normalizedMathContent .split("\n") .map((line) => `${linePrefix}${line}`) .join("\n"); return `${linePrefix}$$\n${prefixedMathContent}\n${linePrefix}$$`; } function normalizeLatexMathContent(mathContent) { const normalizedDelimiters = restoreControlEscapedLatexCommands(mathContent) .replace(/\r\s*ight(?=\s*[\]\)\}\|.])/g, "\\right") .replace(/(^|[^\\])right(?=\s*[\]\)\}\|.])/g, "$1\\right") .replace(/(^|[^\\])left(?=\s*[\[\(\{\|.])/g, "$1\\left"); return restoreLineBreakEscapedRightDelimiters(normalizedDelimiters); } function restoreControlEscapedLatexCommands(mathContent) { return CONTROL_ESCAPED_LATEX_COMMAND_REPLACEMENTS.reduce((normalizedContent, [pattern, replacement]) => normalizedContent.replace(pattern, replacement), mathContent); } function restoreLineBreakEscapedRightDelimiters(mathContent) { let openLeftDelimiterCount = 0; let cursor = 0; let restoredContent = ""; LINE_BREAK_RIGHT_FRAGMENT_PATTERN.lastIndex = 0; let match; while ((match = LINE_BREAK_RIGHT_FRAGMENT_PATTERN.exec(mathContent)) !== null) { const token = match[0]; restoredContent += mathContent.slice(cursor, match.index); if (token === "\\left") { openLeftDelimiterCount += 1; restoredContent += token; } else if (token === "\\right") { if (openLeftDelimiterCount > 0) { openLeftDelimiterCount -= 1; } restoredContent += token; } else if (openLeftDelimiterCount > 0) { openLeftDelimiterCount -= 1; restoredContent += "\\right"; } else { restoredContent += token; } cursor = match.index + token.length; } return restoredContent + mathContent.slice(cursor); } function addCjkAutolinkBoundaries(content) { return content.replace(BARE_URL_BEFORE_CJK_BOUNDARY_PATTERN, (match, url, offset, source) => { const previousChar = source[offset - 1]; if (previousChar === "(" || previousChar === "<") { return match; } return `${url}`; }); } const ALLOWED_SVG_TAGS = new Map([ "svg", "g", "defs", "desc", "title", "symbol", "use", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon", "text", "tspan", "textPath", "marker", "pattern", "clipPath", "mask", "linearGradient", "radialGradient", "stop", "style", "switch", "filter", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence" ].map((name) => [name.toLowerCase(), name])); function sanitizeSvgCss(css) { return css .replace(/@import[^;]*(;|$)/gi, "") .replace(/url\(\s*(['"]?)(?!#)[^)]*\)/gi, "none"); } function sanitizeSvgMarkup(source) { const rootMatch = //i.exec(source); if (!rootMatch) { return undefined; } const withoutBlocks = rootMatch[0] .replace(//g, "") .replace(/<(script|foreignObject|animate|animateMotion|animateTransform|set|iframe|object|embed)\b[\s\S]*?<\/\1\s*>/gi, "") .replace(/<\/?(script|foreignObject|animate|animateMotion|animateTransform|set|iframe|object|embed)\b[^>]*>/gi, "") .replace(/(]*>)([\s\S]*?)(<\/style\s*>)/gi, (_all, open, css, close) => `${open}${sanitizeSvgCss(css)}${close}`); const sanitized = withoutBlocks.replace(/<\/?[A-Za-z][^>]*>/g, (tag) => sanitizeSvgTag(tag)); return /]*?)(\/?)\s*>$/); if (!match) { return ""; } const canonical = ALLOWED_SVG_TAGS.get(match[1].toLowerCase()); if (!canonical) { return ""; } if (/^<\//.test(tag)) { return ``; } const attributes = sanitizeSvgAttributes(match[2] || ""); return `<${canonical}${attributes}${match[3] ? "/" : ""}>`; } function sanitizeSvgAttributes(rawAttributes) { const attributes = []; const attrRe = /([^\s=\/"'<>`]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g; let match; while ((match = attrRe.exec(rawAttributes)) !== null) { const name = match[1]; const lower = name.toLowerCase(); const value = match[2] ?? match[3] ?? match[4] ?? ""; if (lower.startsWith("on")) { continue; } if (/javascript:|vbscript:|data:/i.test(value)) { continue; } if ((lower === "href" || lower === "xlink:href") && !value.startsWith("#")) { continue; } const normalizedValue = lower === "style" ? sanitizeSvgCss(value) : value; attributes.push(`${name}="${TabLocalFiles.escapeAttr(normalizedValue)}"`); } return attributes.length ? ` ${attributes.join(" ")}` : ""; } function sanitizeRawHtml(value, sourceUrl) { const parts = []; const svgRe = //gi; let cursor = 0; let match; while ((match = svgRe.exec(value)) !== null) { parts.push(sanitizeNonSvgHtml(value.slice(cursor, match.index), sourceUrl)); parts.push(sanitizeSvgMarkup(match[0]) ?? ""); cursor = match.index + match[0].length; } parts.push(sanitizeNonSvgHtml(value.slice(cursor), sourceUrl)); return parts.join(""); } function sanitizeNonSvgHtml(value, sourceUrl) { const withoutDangerousBlocks = value .replace(DANGEROUS_HTML_BLOCK_RE, "") .replace(DANGEROUS_HTML_TAG_RE, ""); return withoutDangerousBlocks.replace(/|<\/?[A-Za-z][^>]*>/g, (tag) => sanitizeHtmlTag(tag, sourceUrl)); } function sanitizeHtmlTag(tag, sourceUrl) { if (tag.startsWith(" ${formatSubtitleTimestamp(cue.endMs, format)}`; return format === "srt" ? `${position + 1}\n${times}\n${text}` : `${times}\n${text}`; }); if (format === "vtt") { return blocks.length ? `WEBVTT\n\n${blocks.join("\n\n")}\n` : "WEBVTT\n"; } return blocks.length ? `${blocks.join("\n\n")}\n` : ""; } CodeText.serializeSubtitleCues = serializeSubtitleCues; function serializableCueText(text) { return text.replace(/\n\s*\n/g, "\n").replace(/^\n+|\n+$/g, ""); } function formatSubtitleTimestamp(ms, format) { const clamped = Math.max(0, Math.round(ms)); const hours = Math.floor(clamped / 3600000); const minutes = Math.floor((clamped % 3600000) / 60000); const seconds = Math.floor((clamped % 60000) / 1000); const millis = clamped % 1000; const separator = format === "srt" ? "," : "."; return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}${separator}${String(millis).padStart(3, "0")}`; } const SHIFTABLE_TIMESTAMP_PATTERN = /(?:(\d{1,3}):)?(\d{1,2}):(\d{2})([,.])(\d{3})/g; function shiftSubtitleSourceText(value, extension, offsetMs) { if (extension !== ".srt" && extension !== ".vtt") { return undefined; } let shiftedCount = 0; let clamped = false; const lines = value.split(/(\r\n|\r|\n)/); for (let index = 0; index < lines.length; index += 1) { const line = lines[index]; if (!line.includes("-->")) { continue; } let lineShifted = false; lines[index] = line.replace(SHIFTABLE_TIMESTAMP_PATTERN, (_match, hours, minutes, seconds, separator, millis) => { const ms = ((Number(hours || "0") * 60 + Number(minutes)) * 60 + Number(seconds)) * 1000 + Number(millis); const next = ms + offsetMs; if (next < 0) { clamped = true; } lineShifted = true; return formatShiftedTimestamp(Math.max(0, next), separator); }); if (lineShifted) { shiftedCount += 1; } } return { text: lines.join(""), shiftedCount, clamped }; } CodeText.shiftSubtitleSourceText = shiftSubtitleSourceText; function formatShiftedTimestamp(ms, separator) { const hours = Math.floor(ms / 3600000); const minutes = Math.floor((ms % 3600000) / 60000); const seconds = Math.floor((ms % 60000) / 1000); const millis = ms % 1000; return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}${separator}${String(millis).padStart(3, "0")}`; } function shiftSubtitleCues(cues, offsetMs) { let clamped = false; const shifted = cues.map((cue) => { const startMs = cue.startMs + offsetMs; const endMs = cue.endMs + offsetMs; if (startMs < 0 || endMs < 0) { clamped = true; } const nextStart = Math.max(0, startMs); return { ...cue, startMs: nextStart, endMs: Math.max(nextStart, endMs) }; }); return { cues: shifted, clamped }; } CodeText.shiftSubtitleCues = shiftSubtitleCues; function cleanupSubtitleCues(cues, kind) { if (kind === "duplicates") { return mergeDuplicateCues(cues); } let changedCount = 0; const cleaned = []; for (const cue of cues) { const text = cleanCueText(cue.text, kind); if (text === cue.text) { cleaned.push(cue); continue; } changedCount += 1; if (text) { cleaned.push({ ...cue, text }); } } return { cues: reindexCues(cleaned), changedCount }; } CodeText.cleanupSubtitleCues = cleanupSubtitleCues; function cleanCueText(text, kind) { let next = text; if (kind === "tags") { next = next.replace(/<[^>\n]+>/g, "").replace(/\{[^}\n]*\}/g, ""); } else if (kind === "annotations") { next = next.replace(/\[[^\]\n]*\]|\([^)\n]*\)|([^)\n]*)|【[^】\n]*】/g, ""); } else if (kind === "music") { next = next .split("\n") .filter((line) => !/[♪♫🎵]/.test(line)) .join("\n"); } return next .split("\n") .map((line) => line.replace(/[ \t]{2,}/g, " ").trim()) .filter((line, index, lines) => line !== "" || (index > 0 && index < lines.length - 1)) .join("\n") .trim(); } function mergeDuplicateCues(cues) { const sorted = [...cues].sort((a, b) => a.startMs - b.startMs || a.index - b.index); const merged = []; let changedCount = 0; for (const cue of sorted) { if (!cue.text.trim()) { changedCount += 1; continue; } const previous = merged[merged.length - 1]; if (previous && previous.text === cue.text && cue.startMs - previous.endMs < 500) { previous.endMs = Math.max(previous.endMs, cue.endMs); changedCount += 1; continue; } merged.push({ ...cue }); } return { cues: reindexCues(merged), changedCount }; } function reindexCues(cues) { return cues.map((cue, position) => (cue.index === position + 1 ? cue : { ...cue, index: position + 1 })); } const ANALYSIS_CUE_SAMPLE_LIMIT = 1000; function countSubtitleLineScripts(text) { return countScripts(text); } CodeText.countSubtitleLineScripts = countSubtitleLineScripts; function analyzeSubtitleCues(cues) { let firstStartMs = Number.POSITIVE_INFINITY; let lastEndMs = 0; for (const cue of cues) { if (cue.startMs < firstStartMs) { firstStartMs = cue.startMs; } if (cue.endMs > lastEndMs) { lastEndMs = cue.endMs; } } if (!Number.isFinite(firstStartMs)) { firstStartMs = 0; } const sample = cues.length > ANALYSIS_CUE_SAMPLE_LIMIT ? cues.slice(0, ANALYSIS_CUE_SAMPLE_LIMIT) : cues; const totals = { han: 0, kana: 0, hangul: 0, latin: 0, cyrillic: 0, arabic: 0 }; let bilingualCues = 0; for (const cue of sample) { const counts = countScripts(cue.text); totals.han += counts.han; totals.kana += counts.kana; totals.hangul += counts.hangul; totals.latin += counts.latin; totals.cyrillic += counts.cyrillic; totals.arabic += counts.arabic; if (isBilingualCueText(cue.text)) { bilingualCues += 1; } } return { firstStartMs, lastEndMs, languages: rankScripts(totals), bilingual: sample.length > 0 && bilingualCues >= Math.max(2, Math.ceil(sample.length * 0.3)) }; } CodeText.analyzeSubtitleCues = analyzeSubtitleCues; function countScripts(text) { const counts = { han: 0, kana: 0, hangul: 0, latin: 0, cyrillic: 0, arabic: 0 }; for (const char of text) { const code = char.codePointAt(0) || 0; if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3400 && code <= 0x4dbf)) { counts.han += 1; } else if (code >= 0x3040 && code <= 0x30ff) { counts.kana += 1; } else if (code >= 0xac00 && code <= 0xd7af) { counts.hangul += 1; } else if ((code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a) || (code >= 0xc0 && code <= 0x24f)) { counts.latin += 1; } else if (code >= 0x400 && code <= 0x4ff) { counts.cyrillic += 1; } else if (code >= 0x600 && code <= 0x6ff) { counts.arabic += 1; } } return counts; } function rankScripts(totals) { const japaneseMeaningful = totals.kana >= 10 && totals.kana * 20 >= totals.han; const buckets = []; if (japaneseMeaningful) { buckets.push(["japanese", totals.han + totals.kana]); } else { buckets.push(["chinese", totals.han], ["japanese", totals.kana]); } buckets.push(["korean", totals.hangul], ["latin", totals.latin], ["cyrillic", totals.cyrillic], ["arabic", totals.arabic]); const total = buckets.reduce((sum, [, count]) => sum + count, 0); if (!total) { return []; } return buckets .filter(([, count]) => count > 0 && count * 10 >= total) .sort((a, b) => b[1] - a[1]) .map(([key]) => key); } function isBilingualCueText(text) { let hasCjkLine = false; let hasLatinLine = false; for (const line of text.split("\n")) { const counts = countScripts(line); const cjk = counts.han + counts.kana + counts.hangul; if (cjk >= 2 && cjk >= counts.latin) { hasCjkLine = true; } else if (counts.latin >= 2 && counts.latin > cjk) { hasLatinLine = true; } } return hasCjkLine && hasLatinLine; } function formatSubtitleTimeMs(ms, withHours = false) { const sign = ms < 0 ? "-" : ""; const absMs = Math.abs(ms); const hours = Math.floor(absMs / 3600000); const minutes = Math.floor((absMs % 3600000) / 60000); const seconds = Math.floor((absMs % 60000) / 1000); const millis = Math.floor(absMs % 1000); if (hours > 0 || withHours) { return `${sign}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(millis).padStart(3, "0")}`; } return `${sign}${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(millis).padStart(3, "0")}`; } CodeText.formatSubtitleTimeMs = formatSubtitleTimeMs; function subtitleTimeFormatter(cues) { let longest = 0; for (const cue of cues) { if (cue.endMs > longest) { longest = cue.endMs; } } const withHours = longest >= 3600000; return (ms) => formatSubtitleTimeMs(ms, withHours); } CodeText.subtitleTimeFormatter = subtitleTimeFormatter; function parseSrt(value) { const cues = []; const blocks = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split(/\n\s*\n/); let cueIndex = 0; for (const block of blocks) { const lines = block.trim().split("\n"); if (lines.length < 2) { continue; } const firstLine = lines[0].trim(); const arrowIndex = firstLine.includes("-->") ? 0 : lines.length > 1 && lines[1].includes("-->") ? 1 : -1; if (arrowIndex < 0) { continue; } const times = parseSrtTimeLine(lines[arrowIndex]); if (!times) { continue; } cueIndex += 1; const text = lines.slice(arrowIndex + 1).join("\n").trim(); cues.push({ index: cueIndex, startMs: times.start, endMs: times.end, text }); } return cues; } function parseSrtTimeLine(line) { const match = line.match(/^(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})\s*-->\s*(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})/); if (!match) { return undefined; } const start = timeComponentsToMs(match[1], match[2], match[3], match[4]); const end = timeComponentsToMs(match[5], match[6], match[7], match[8]); if (start === undefined || end === undefined) { return undefined; } return { start, end }; } function parseVtt(value) { const cues = []; const lines = normalizeLines(value); let index = 0; let inHeader = true; let cueIndex = 0; while (index < lines.length) { const line = lines[index]; index += 1; if (inHeader) { if (line.toLowerCase().startsWith("webvtt")) { continue; } if (line.trim() === "") { inHeader = false; } continue; } if (line.trim() === "") { continue; } let timeLine = line; if (!line.includes("-->")) { if (index < lines.length && lines[index].includes("-->")) { timeLine = lines[index]; index += 1; } else { continue; } } const times = parseVttTimeLine(timeLine); if (!times) { continue; } const textLines = []; while (index < lines.length && lines[index].trim() !== "") { textLines.push(lines[index]); index += 1; } cueIndex += 1; cues.push({ index: cueIndex, startMs: times.start, endMs: times.end, text: textLines.join("\n").trim() }); } return cues; } function parseVttTimeLine(line) { const match = line.match(/^(?:(\d{2}):)?(\d{2}):(\d{2})[,.](\d{3})\s*-->\s*(?:(\d{2}):)?(\d{2}):(\d{2})[,.](\d{3})/); if (!match) { return undefined; } const start = timeComponentsToMs(match[1] || "0", match[2], match[3], match[4]); const end = timeComponentsToMs(match[5] || "0", match[6], match[7], match[8]); if (start === undefined || end === undefined) { return undefined; } return { start, end }; } function parseAss(value) { const cues = []; const lines = normalizeLines(value); let inEvents = false; let formatIndex = -1; let textIndex = -1; let startIndex = -1; let endIndex = -1; let cueIndex = 0; for (const line of lines) { if (line.toLowerCase().startsWith("[events]")) { inEvents = true; continue; } if (line.startsWith("[")) { inEvents = false; continue; } if (!inEvents) { continue; } if (line.toLowerCase().startsWith("format:")) { const headers = line.slice(7).split(",").map((cell) => cell.trim().toLowerCase()); formatIndex = 0; textIndex = headers.indexOf("text"); startIndex = headers.indexOf("start"); endIndex = headers.indexOf("end"); continue; } if (!line.toLowerCase().startsWith("dialogue:")) { continue; } const parts = line.slice(9).split(","); if (textIndex < 0 || textIndex >= parts.length) { continue; } const startMs = startIndex >= 0 ? parseAssTime(parts[startIndex]) : undefined; const endMs = endIndex >= 0 ? parseAssTime(parts[endIndex]) : undefined; const text = stripAssTags(parts.slice(textIndex).join(",")).replace(/\\N/gi, "\n").replace(/\\n/gi, " "); cueIndex += 1; cues.push({ index: cueIndex, startMs: startMs ?? 0, endMs: endMs ?? 0, text }); } return cues; } function parseAssTime(value) { const match = value.trim().match(/^(\d+):(\d{2}):(\d{2})\.(\d{2})$/); if (!match) { return undefined; } const centis = match[4].padEnd(3, "0"); return timeComponentsToMs(match[1], match[2], match[3], centis); } function stripAssTags(text) { return text.replace(/\{[^}]*\}/g, ""); } function parseSbv(value) { const cues = []; const lines = normalizeLines(value); let index = 0; let cueIndex = 0; while (index < lines.length) { const line = lines[index]; index += 1; const times = parseSbvTimeLine(line); if (!times) { continue; } const textLines = []; while (index < lines.length && lines[index].trim() !== "") { textLines.push(lines[index]); index += 1; } cueIndex += 1; cues.push({ index: cueIndex, startMs: times.start, endMs: times.end, text: textLines.join("\n").trim() }); } return cues; } function parseSbvTimeLine(line) { const match = line.match(/^(\d{1,2}):(\d{2}):(\d{2})\.(\d{3})\s*,\s*(\d{1,2}):(\d{2}):(\d{2})\.(\d{3})$/); if (!match) { return undefined; } const start = timeComponentsToMs(match[1], match[2], match[3], match[4]); const end = timeComponentsToMs(match[5], match[6], match[7], match[8]); if (start === undefined || end === undefined) { return undefined; } return { start, end }; } function parseSub(value) { const lines = normalizeLines(value); const first = lines.find((line) => line.trim() !== "") || ""; if (/^\{\d+\}\{\d+\}/.test(first)) { return { cues: parseMicroDvd(value), format: "MicroDVD" }; } if (/^\[SUBTITLE\]/i.test(first) || /\d{2}:\d{2}:\d{2}\.\d{2}/.test(first)) { return { cues: parseSubViewer(value), format: "SubViewer" }; } return undefined; } function parseMicroDvd(value) { const cues = []; const lines = normalizeLines(value); const firstContentIndex = lines.findIndex((line) => line.trim() !== ""); const frameRateDeclaration = firstContentIndex >= 0 ? lines[firstContentIndex].trim().match(/^\{([01])\}\{\1\}(\d+(?:\.\d+)?)$/) : null; const declaredFrameRate = frameRateDeclaration ? Number(frameRateDeclaration[2]) : Number.NaN; const frameRate = Number.isFinite(declaredFrameRate) && declaredFrameRate > 0 ? declaredFrameRate : 24; let cueIndex = 0; for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { if (lineIndex === firstContentIndex && frameRateDeclaration) { continue; } const line = lines[lineIndex]; const match = line.match(/^\{(\d+)\}\{(\d+)\}(.*)$/); if (!match) { continue; } cueIndex += 1; cues.push({ index: cueIndex, startMs: Number(match[1]) * 1000 / frameRate, endMs: Number(match[2]) * 1000 / frameRate, text: match[3].replace(/\|/g, "\n") }); } return cues; } function parseSubViewer(value) { const cues = []; const lines = normalizeLines(value); let index = 0; let cueIndex = 0; while (index < lines.length) { const line = lines[index]; index += 1; const match = line.match(/^(\d{2}):(\d{2}):(\d{2})\.(\d{2})\s*,\s*(\d{2}):(\d{2}):(\d{2})\.(\d{2})$/); if (!match) { continue; } const start = timeComponentsToMs(match[1], match[2], match[3], match[4].padEnd(3, "0")); const end = timeComponentsToMs(match[5], match[6], match[7], match[8].padEnd(3, "0")); const textLines = []; while (index < lines.length && lines[index].trim() !== "") { textLines.push(lines[index].replace(/\[br\]/gi, "\n")); index += 1; } cueIndex += 1; cues.push({ index: cueIndex, startMs: start ?? 0, endMs: end ?? 0, text: textLines.join("\n").trim() }); } return cues; } function parseMpl2(value) { const cues = []; const lines = normalizeLines(value); let cueIndex = 0; for (const line of lines) { const match = line.match(/^\[(\d+)\]\[(\d+)\](.*)$/); if (!match) { continue; } cueIndex += 1; cues.push({ index: cueIndex, startMs: Number(match[1]) * 100, endMs: Number(match[2]) * 100, text: match[3].replace(/\|/g, "\n") }); } return cues; } function parseLrc(value) { const cues = []; const lines = normalizeLines(value); let cueIndex = 0; for (const line of lines) { const tags = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\]/g); const text = line.replace(/\[\d{2}:\d{2}\.\d{2,3}\]/g, "").trim(); if (!tags || !text) { continue; } for (const tag of tags) { const match = tag.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\]/); if (!match) { continue; } const millis = match[3].length === 2 ? `${match[3]}0` : match[3]; const ms = timeComponentsToMs("0", match[1], match[2], millis); if (ms === undefined) { continue; } cueIndex += 1; cues.push({ index: cueIndex, startMs: ms, endMs: ms, text }); } } cues.sort((a, b) => a.startMs - b.startMs || a.index - b.index); for (let i = 0; i < cues.length; i += 1) { cues[i].index = i + 1; const next = cues[i + 1]; cues[i].endMs = next ? Math.min(cues[i].startMs + 5000, next.startMs) : cues[i].startMs + 5000; } return cues; } function parseSmi(value) { const syncRe = //gi; let match; const segments = []; while ((match = syncRe.exec(value)) !== null) { segments.push({ startMs: Number(match[1]), tagStart: match.index, contentStart: match.index + match[0].length }); } const cues = []; for (let i = 0; i < segments.length; i += 1) { const contentEnd = i + 1 < segments.length ? segments[i + 1].tagStart : value.length; const html = value.slice(segments[i].contentStart, contentEnd); const text = stripHtmlTags(html).replace(/ /gi, " ").trim(); if (!text) { continue; } cues.push({ index: cues.length + 1, startMs: segments[i].startMs, endMs: i + 1 < segments.length ? segments[i + 1].startMs : segments[i].startMs + 5000, text }); } return cues; } function parseTtml(value) { const cues = []; let index = 0; try { const parser = new DOMParser(); const doc = parser.parseFromString(value, "application/xml"); const paragraphs = doc.querySelectorAll("p, span"); paragraphs.forEach((element) => { const begin = element.getAttribute("begin"); if (!begin) { return; } const startMs = parseTtmlTime(begin); const dur = element.getAttribute("dur"); const end = element.getAttribute("end"); const durMs = dur ? parseTtmlTime(dur) : undefined; const endMs = durMs !== undefined && startMs !== undefined ? startMs + durMs : end ? (parseTtmlTime(end) ?? 0) : (startMs ?? 0) + 5000; if (startMs === undefined) { return; } index += 1; cues.push({ index, startMs, endMs, text: stripHtmlTags(element.innerHTML).replace(/ /gi, " ").trim() }); }); } catch { } return cues; } function parseTtmlTime(value) { const match = value.match(/^(?:(\d{2,}):)?(\d{2}):(\d{2})(?:\.(\d{3})|:(\d{2,3}))?$/); if (match) { const millis = match[4] ?? (match[5] ? match[5].padEnd(3, "0") : "000"); return timeComponentsToMs(match[1] || "0", match[2], match[3], millis); } const seconds = Number(value); if (Number.isFinite(seconds)) { return seconds * 1000; } return undefined; } function parseIdx(value) { const cues = []; const lines = normalizeLines(value); let cueIndex = 0; for (const line of lines) { const match = line.match(/^timestamp:\s*(\d{2}):(\d{2}):(\d{2}):(\d{3})\s*,\s*filepos:\s*([0-9a-fx]+)/i); if (!match) { continue; } const ms = timeComponentsToMs(match[1], match[2], match[3], match[4]); if (ms === undefined) { continue; } cueIndex += 1; cues.push({ index: cueIndex, startMs: ms, endMs: ms, text: `filepos ${match[5]}` }); } return cues; } function normalizeLines(value) { return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); } function stripHtmlTags(html) { return html.replace(//gi, "\n").replace(/<[^>]+>/g, ""); } function timeComponentsToMs(hours, minutes, seconds, millis) { const h = Number(hours); const m = Number(minutes); const s = Number(seconds); const ms = Number(millis.padEnd(3, "0").slice(0, 3)); if (!Number.isFinite(h) || !Number.isFinite(m) || !Number.isFinite(s) || !Number.isFinite(ms)) { return undefined; } if (m < 0 || m >= 60 || s < 0 || s >= 60 || ms < 0 || ms >= 1000) { return undefined; } return ((h * 60 + m) * 60 + s) * 1000 + ms; } })(CodeText = TabLocalFiles.CodeText || (TabLocalFiles.CodeText = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var CodeText; (function (CodeText) { const SUBTITLE_TIMELINE_CUE_LIMIT = 2000; function renderBinarySubtitleNotice(context) { return { title: context.fileName, bodyHtml: `

    ${TabLocalFiles.escapeHtml(context.fileName)}

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.subtitleBinaryNotice"))}

    `, contentClassName: "tlf-unsupported-file", toolbar: TabLocalFiles.filePreviewToolbar(), }; } const SUBTITLE_LANGUAGE_LABEL_KEYS = { chinese: "code.subtitleLangChinese", japanese: "code.subtitleLangJapanese", korean: "code.subtitleLangKorean", latin: "code.subtitleLangLatin", cyrillic: "code.subtitleLangCyrillic", arabic: "code.subtitleLangArabic" }; const SUBTITLE_INFO_BODY_ID = "tlf-subtitle-info-body"; function subtitleInfoStatus(extension, value, viewState) { const parsed = CodeText.parseSubtitles(value, extension); const hasFacts = Boolean(parsed && parsed.cues.length); return [{ id: SUBTITLE_INFO_BODY_ID, html: parsed && hasFacts ? renderSubtitleInfoBody(parsed, viewState) : "" }]; } function renderSubtitleInfoBody(result, viewState) { const analysis = CodeText.analyzeSubtitleCues(result.cues); const rows = [ subtitleInfoRow("code.subtitleInfoFormat", `${TabLocalFiles.escapeHtml(result.format)}`), subtitleInfoRow("code.subtitleInfoCues", TabLocalFiles.escapeHtml(String(result.cues.length))), subtitleInfoRow("code.subtitleInfoDuration", TabLocalFiles.escapeHtml(CodeText.formatSubtitleTimeMs(analysis.lastEndMs))) ]; const languageLabels = analysis.languages .map((language) => SUBTITLE_LANGUAGE_LABEL_KEYS[language]) .filter((key) => Boolean(key)) .map((key) => TabLocalFiles.t(key)); if (languageLabels.length || analysis.bilingual) { const bilingual = analysis.bilingual ? `${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.subtitleBilingual"))}` : ""; rows.push(subtitleInfoRow("code.subtitleInfoLanguage", `${TabLocalFiles.escapeHtml(languageLabels.join(" · "))}${bilingual}`)); } let transcode = ""; if (viewState.subtitleEncoding) { rows.push(subtitleInfoRow("code.subtitleInfoEncoding", `${TabLocalFiles.escapeHtml(viewState.subtitleEncoding.toUpperCase())}`)); if (viewState.subtitleEncoding !== "utf-8" && viewState.subtitleTranscodable) { transcode = ` `; } } return `
    ${rows.join("")}
    ${transcode} `; } function subtitleInfoRow(labelKey, valueHtml) { return `
    ${TabLocalFiles.escapeHtml(TabLocalFiles.t(labelKey))}
    ${valueHtml}
    `; } function syncSubtitleInfo(extension, value, viewState) { if (!CodeText.isSubtitleExtension(extension)) { return; } const body = document.getElementById(SUBTITLE_INFO_BODY_ID); if (!body) { return; } const parsed = CodeText.parseSubtitles(value, extension); body.innerHTML = parsed && parsed.cues.length ? renderSubtitleInfoBody(parsed, viewState) : ""; } function renderSubtitleTextView(result) { const displayed = result.cues.slice(0, SUBTITLE_TIMELINE_CUE_LIMIT); const truncationNote = result.cues.length > displayed.length ? `

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.subtitleTextTruncated", { shown: displayed.length, total: result.cues.length }))}

    ` : ""; const paragraphs = displayed .map((cue) => `

    ${TabLocalFiles.escapeHtml(cue.text).replace(/\n/g, "
    ")}

    `) .join(""); return `
    ${truncationNote}
    ${paragraphs}
    `; } function bindSubtitleCopyAll(extension, viewState) { TabLocalFiles.onToolbarAction("copy-all", () => { const text = TabLocalFiles.TextEditor.getText(CodeText.EDITOR_ROOT_ID) || ""; const editing = TabLocalFiles.isDocumentEditing(); const payload = !editing && viewState.subtitleView === "text" ? CodeText.subtitlePlainText(text, extension) ?? text : text; void TabLocalFiles.copyPlainTextToClipboard(payload) .then(() => { TabLocalFiles.showToast(TabLocalFiles.t("common.copiedToClipboard"), "success"); }) .catch(() => { TabLocalFiles.showToast(TabLocalFiles.t("common.copyFailed"), "error"); }); }); } function subtitleCueHighlightHtml(text, query) { const needle = query.trim().toLowerCase(); if (!needle) { return TabLocalFiles.escapeHtml(text).replace(/\n/g, "
    "); } const lower = text.toLowerCase(); let html = ""; let from = 0; let index = lower.indexOf(needle, from); while (index >= 0) { html += TabLocalFiles.escapeHtml(text.slice(from, index)); html += `${TabLocalFiles.escapeHtml(text.slice(index, index + needle.length))}`; from = index + needle.length; index = lower.indexOf(needle, from); } html += TabLocalFiles.escapeHtml(text.slice(from)); return html.replace(/\n/g, "
    "); } CodeText.subtitleCueHighlightHtml = subtitleCueHighlightHtml; const SUBTITLE_EXPORT_TARGETS = ["srt", "vtt", "txt"]; const SUBTITLE_SHIFT_INPUT_ID = "tlf-subtitle-shift-offset"; function renderSubtitleShiftMenu() { const label = TabLocalFiles.t("code.subtitleShiftMenu"); return `
    ${TabLocalFiles.renderLucideIcon("rotate-cw-fading-clock")} ${TabLocalFiles.escapeHtml(label)} ${TabLocalFiles.renderLucideIcon("chevron-down")}

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.subtitleShiftHint"))}

    `; } function bindSubtitleShiftAction(context, extension, viewState) { TabLocalFiles.onToolbarAction("code.subtitle-shift.apply", (detail) => { const input = document.getElementById(SUBTITLE_SHIFT_INPUT_ID); const offset = Math.round(Number(input?.value)); if (!input || input.value.trim() === "" || !Number.isFinite(offset) || offset === 0) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleShiftInvalid"), "error"); return; } detail.button.closest("details")?.removeAttribute("open"); const text = TabLocalFiles.TextEditor.getText(CodeText.EDITOR_ROOT_ID) || ""; if ((extension === ".srt" || extension === ".vtt") && viewState.subtitleTranscodable) { const shifted = CodeText.shiftSubtitleSourceText(text, extension, offset); if (!shifted || shifted.shiftedCount === 0) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleExportUnparseable"), "error"); return; } void TabLocalFiles.TextEditor.saveReplacement(CodeText.EDITOR_ROOT_ID, shifted.text) .then((saved) => { if (!saved || !TabLocalFiles.isCurrentDocumentMount(context.mountId)) { return; } TabLocalFiles.showToast(TabLocalFiles.t(shifted.clamped ? "code.subtitleShiftedClamped" : "code.subtitleShifted", { count: shifted.shiftedCount }), "success"); }) .catch(() => { if (TabLocalFiles.isCurrentDocumentMount(context.mountId)) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleShiftFailed"), "error"); } }); return; } const parsed = CodeText.parseSubtitles(text, extension); if (!parsed || parsed.cues.length === 0) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleExportUnparseable"), "error"); return; } const shifted = CodeText.shiftSubtitleCues(parsed.cues, offset); const target = extension === ".vtt" ? "vtt" : "srt"; const fileName = TabLocalFiles.fileNameWithExtension(context.fileName, `.${target}`); void TabLocalFiles.saveTextAsFile(CodeText.serializeSubtitleCues(shifted.cues, target), fileName, TabLocalFiles.mimeTypeFromExtension(`.${target}`)) .then(() => TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleShiftExported", { fileName }), "success")) .catch((error) => { if (error instanceof DOMException && error.name === "AbortError") { return; } TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleShiftFailed"), "error"); }); }); } const SUBTITLE_CLEANUP_KINDS = [ { kind: "tags", labelKey: "code.subtitleCleanTags", icon: "code" }, { kind: "annotations", labelKey: "code.subtitleCleanAnnotations", icon: "quote" }, { kind: "music", labelKey: "code.subtitleCleanMusic", icon: "volume-2" }, { kind: "duplicates", labelKey: "code.subtitleCleanDuplicates", icon: "list-checks" } ]; function renderSubtitleCleanMenu() { const label = TabLocalFiles.t("code.subtitleCleanMenu"); const items = SUBTITLE_CLEANUP_KINDS.map((item) => ` `).join(""); return `
    ${TabLocalFiles.renderLucideIcon("eraser")} ${TabLocalFiles.escapeHtml(label)} ${TabLocalFiles.renderLucideIcon("chevron-down")}
    `; } function bindSubtitleCleanActions(context, extension, viewState) { SUBTITLE_CLEANUP_KINDS.forEach(({ kind }) => { TabLocalFiles.onToolbarAction(`code.subtitle-clean.${kind}`, (detail) => { detail.button.closest("details")?.removeAttribute("open"); const text = TabLocalFiles.TextEditor.getText(CodeText.EDITOR_ROOT_ID) || ""; const parsed = CodeText.parseSubtitles(text, extension); if (!parsed || parsed.cues.length === 0) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleExportUnparseable"), "error"); return; } const cleaned = CodeText.cleanupSubtitleCues(parsed.cues, kind); if (cleaned.changedCount === 0) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleCleanNothing"), "neutral"); return; } if (extension === ".srt" && viewState.subtitleTranscodable) { const next = CodeText.serializeSubtitleCues(cleaned.cues, "srt"); void TabLocalFiles.TextEditor.saveReplacement(CodeText.EDITOR_ROOT_ID, next) .then((saved) => { if (!saved || !TabLocalFiles.isCurrentDocumentMount(context.mountId)) { return; } TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleCleaned", { count: cleaned.changedCount }), "success"); }) .catch(() => { if (TabLocalFiles.isCurrentDocumentMount(context.mountId)) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleCleanFailed"), "error"); } }); return; } const fileName = TabLocalFiles.fileNameWithExtension(context.fileName, ".srt"); void TabLocalFiles.saveTextAsFile(CodeText.serializeSubtitleCues(cleaned.cues, "srt"), fileName, TabLocalFiles.mimeTypeFromExtension(".srt")) .then(() => TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleCleanExported", { fileName }), "success")) .catch((error) => { if (error instanceof DOMException && error.name === "AbortError") { return; } TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleCleanFailed"), "error"); }); }); }); } function bindSubtitleExportActions(context, extension) { SUBTITLE_EXPORT_TARGETS.forEach((target) => { TabLocalFiles.onToolbarAction(`code.subtitle-export.${target}`, (detail) => { detail.button.closest("details")?.removeAttribute("open"); const text = TabLocalFiles.TextEditor.getText(CodeText.EDITOR_ROOT_ID) || ""; const parsed = CodeText.parseSubtitles(text, extension); if (!parsed || parsed.cues.length === 0) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleExportUnparseable"), "error"); TabLocalFiles.recordCurrentFileExportResult({ format: target, result: "failed", failureReason: "no_content" }); return; } const payload = target === "txt" ? parsed.cues.map((cue) => cue.text).join("\n\n") : CodeText.serializeSubtitleCues(parsed.cues, target); const fileName = TabLocalFiles.fileNameWithExtension(context.fileName, `.${target}`); void TabLocalFiles.saveTextAsFile(payload, fileName, TabLocalFiles.mimeTypeFromExtension(`.${target}`)) .then(() => { TabLocalFiles.recordCurrentFileExportResult({ format: target, result: "success" }); TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleExported", { fileName }), "success"); }) .catch((error) => { if (error instanceof DOMException && error.name === "AbortError") { TabLocalFiles.recordCurrentFileExportResult({ format: target, result: "cancelled" }); return; } TabLocalFiles.recordCurrentFileExportResult({ format: target, result: "failed", failureReason: "error", error }); TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleExportFailed"), "error"); }); }); }); } function bindSubtitleTranscodeAction(context, extension, viewState) { if (typeof document.addEventListener !== "function") { return; } const onClick = (event) => { const button = event.target?.closest("[data-tlf-subtitle-transcode]"); if (!button || button.disabled) { return; } button.disabled = true; void TabLocalFiles.TextEditor.save(CodeText.EDITOR_ROOT_ID) .then((saved) => { if (!TabLocalFiles.isCurrentDocumentMount(context.mountId)) { return; } if (!saved) { button.disabled = false; return; } TabLocalFiles.recordDetectedTextEncoding("utf-8"); viewState.subtitleEncoding = "utf-8"; TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleTranscoded"), "success"); TabLocalFiles.TextEditor.refreshPreview(CodeText.EDITOR_ROOT_ID); }) .catch(() => { if (!TabLocalFiles.isCurrentDocumentMount(context.mountId)) { return; } button.disabled = false; TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleTranscodeFailed"), "error"); }); }; document.addEventListener("click", onClick); TabLocalFiles.registerDocumentMountCleanup(context.mountId, () => { document.removeEventListener("click", onClick); }); } function subtitleCueTime(cue) { return `${CodeText.formatSubtitleTimeMs(cue.startMs, true)} → ${CodeText.formatSubtitleTimeMs(cue.endMs, true)}`; } function renderSubtitleTimeline(result, extension, viewState) { const query = viewState?.subtitleQuery || ""; const filtered = CodeText.filterSubtitleCues(result.cues, query); const textQuery = filtered.mode === "text" ? query : ""; const displayed = filtered.cues.slice(0, SUBTITLE_TIMELINE_CUE_LIMIT); const truncationNote = filtered.cues.length > displayed.length ? `

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.subtitleTimelineTruncated", { shown: displayed.length, total: filtered.cues.length }))}

    ` : ""; const formatTime = CodeText.subtitleTimeFormatter(result.cues); const items = displayed .map((cue, position) => `
  • ${TabLocalFiles.escapeHtml(formatTime(cue.startMs))} ${TabLocalFiles.escapeHtml(formatTime(cue.endMs))}
    ${subtitleCueHighlightHtml(cue.text, textQuery)}
  • `) .join(""); return `
    ${truncationNote}
      ${items}
    `; } function subtitleTimelineSearch(viewState) { let index = -1; const refresh = () => TabLocalFiles.TextEditor.refreshPreview(CodeText.EDITOR_ROOT_ID); const timeline = () => document.querySelector(`#${CodeText.EDITOR_ROOT_ID} [data-tlf-subtitle-preview]`); const rows = () => Array.from(document.querySelectorAll(`#${CodeText.EDITOR_ROOT_ID} .tlf-subtitle-cue`)); const focusRow = (position) => { const items = rows(); items.forEach((row, at) => { if (at === position) { row.dataset.tlfSubtitleCurrent = ""; } else { delete row.dataset.tlfSubtitleCurrent; } }); index = items.length ? position : -1; items[position]?.scrollIntoView?.({ block: "center" }); return { total: items.length, index }; }; return { isActive: () => viewState.subtitleView === "timeline" && Boolean(timeline()), placeholder: () => TabLocalFiles.t("code.subtitleSearchPlaceholder"), search(query) { const next = query || ""; if (viewState.subtitleQuery !== next) { viewState.subtitleQuery = next; refresh(); } if (!next.trim()) { index = -1; return { total: 0, index }; } return focusRow(0); }, navigate(direction) { const total = rows().length; if (!total || !viewState.subtitleQuery.trim()) { return { total: 0, index: -1 }; } return focusRow((index + direction + total) % total); }, close() { index = -1; if (viewState.subtitleQuery) { viewState.subtitleQuery = ""; refresh(); } } }; } function bindSubtitleCueCopy() { const root = document.getElementById(CodeText.EDITOR_ROOT_ID); if (!root) { return; } root.addEventListener("click", (event) => { const button = event.target?.closest("[data-tlf-subtitle-copy]"); if (!button || !root.contains(button)) { return; } void TabLocalFiles.copyPlainTextToClipboard(button.dataset.tlfSubtitleCopy || "") .then(() => TabLocalFiles.showToast(TabLocalFiles.t("common.copiedToClipboard"), "success")) .catch(() => TabLocalFiles.showToast(TabLocalFiles.t("common.copyFailed"), "error")); }); } CodeText.subtitleSubRenderer = { id: "subtitle", matches: (extension) => CodeText.isSubtitleExtension(extension), interceptRender: (context) => CodeText.isProbablyBinarySubtitleText(context.rawText) ? renderBinarySubtitleNotice(context) : undefined, renderPreview: (value, extension, _fileName, viewState) => { if (!viewState || viewState.subtitleView === "source") { return undefined; } const parsed = CodeText.parseSubtitles(value, extension); if (!parsed || parsed.cues.length === 0) { return undefined; } return viewState.subtitleView === "text" ? renderSubtitleTextView(parsed) : renderSubtitleTimeline(parsed, extension, viewState); }, contributeToolbar: (toolbar, { extension, rawText, viewState }) => { toolbar.contextPreview = [ TabLocalFiles.segmentedToolbar("code.subtitle-view", TabLocalFiles.t("code.subtitleViewMode"), [ { key: "timeline", label: TabLocalFiles.t("code.subtitleTimeline"), icon: "list", pressed: true }, { key: "text", label: TabLocalFiles.t("code.subtitleTextView"), icon: "text" }, { key: "source", label: TabLocalFiles.t("code.subtitleSource"), icon: "code" } ]) ]; toolbar.status = subtitleInfoStatus(extension, rawText, viewState); toolbar.exportItems = [ TabLocalFiles.toolbarAction("code.subtitle-export.srt", TabLocalFiles.t("code.subtitleExportSrt"), "list"), TabLocalFiles.toolbarAction("code.subtitle-export.vtt", TabLocalFiles.t("code.subtitleExportVtt"), "code"), TabLocalFiles.toolbarAction("code.subtitle-export.txt", TabLocalFiles.t("code.subtitleExportTxt"), "text"), TabLocalFiles.toolbarAction("code.subtitle-translate", TabLocalFiles.t("code.subtitleTranslate"), "languages", { labelText: TabLocalFiles.t("document.translateShort") }) ]; }, bind: ({ context, extension, viewState, refresh }) => { ["timeline", "text", "source"].forEach((view) => { TabLocalFiles.onToolbarAction(`code.subtitle-view.${view}`, () => { TabLocalFiles.CodeTextSearch.closePreviewSearch(); viewState.subtitleView = view; TabLocalFiles.syncSegmentedToolbar("code.subtitle-view", view); refresh(); }); }); bindSubtitleCopyAll(extension, viewState); bindSubtitleCueCopy(); bindSubtitleTranscodeAction(context, extension, viewState); bindSubtitleExportActions(context, extension); bindSubtitleShiftAction(context, extension, viewState); bindSubtitleCleanActions(context, extension, viewState); TabLocalFiles.SubtitleTranslation.bind(context, extension, viewState); }, afterPreviewRender: (extension, value, viewState) => syncSubtitleInfo(extension, value, viewState), previewSearch: (viewState) => subtitleTimelineSearch(viewState) }; })(CodeText = TabLocalFiles.CodeText || (TabLocalFiles.CodeText = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var CodeTextSearch; (function (CodeTextSearch) { const SEARCH_DEBOUNCE_MS = 150; const TEXTAREA_LINE_HEIGHT = 24; function replaceAllMatches(value, query, replacement) { const needle = query.toLowerCase(); if (!needle) { return { text: value, count: 0 }; } const haystack = value.toLowerCase(); let text = ""; let from = 0; let count = 0; let index = haystack.indexOf(needle); while (index !== -1) { text += value.slice(from, index) + replacement; from = index + needle.length; count += 1; index = haystack.indexOf(needle, from); } text += value.slice(from); return { text, count }; } CodeTextSearch.replaceAllMatches = replaceAllMatches; let activeController; function bind(options) { const root = document.getElementById(options.rootId); const textarea = root?.querySelector("[data-tlf-editor-textarea]"); if (!root || !textarea || typeof document.addEventListener !== "function") { return; } const controller = { rootId: options.rootId, root, textarea, previewSearch: options.previewSearch, mode: "edit", matches: [], matchIndex: -1, matchLayer: TabLocalFiles.MarkdownSearch.createTextareaMatchLayer(textarea, TEXTAREA_LINE_HEIGHT), disposed: false }; activeController = controller; const onKeyDown = (event) => { handleGlobalKeydown(controller, event); }; document.addEventListener("keydown", onKeyDown); const onTextChange = () => { if (!isPanelOpen(controller) || controller.mode !== "edit" || controller.debounceTimer !== undefined || textarea.value === controller.searchedText) { return; } controller.debounceTimer = window.setTimeout(() => { controller.debounceTimer = undefined; const current = controller.matches[controller.matchIndex]; runSearch(controller, current ? current.from : undefined, false); }, SEARCH_DEBOUNCE_MS); }; root.addEventListener("tlf:editor-state-change", onTextChange); const onModeChange = (event) => { if (event.detail?.mode !== controller.mode) { close(controller); } }; root.addEventListener("tlf:editor-mode-change", onModeChange); TabLocalFiles.registerDocumentMountCleanup(options.mountId, () => { controller.disposed = true; close(controller); document.removeEventListener("keydown", onKeyDown); root.removeEventListener("tlf:editor-state-change", onTextChange); root.removeEventListener("tlf:editor-mode-change", onModeChange); if (activeController === controller) { activeController = undefined; } }); } CodeTextSearch.bind = bind; function closePreviewSearch() { if (activeController && activeController.mode === "preview") { close(activeController); } } CodeTextSearch.closePreviewSearch = closePreviewSearch; function isEditing(controller) { return TabLocalFiles.isDocumentEditing(); } function isPanelOpen(controller) { return Boolean(controller.panel && !controller.panel.hidden); } function previewSearchAvailable(controller) { return !isEditing(controller) && Boolean(controller.previewSearch?.isActive()); } function handleGlobalKeydown(controller, event) { if (controller.disposed) { return; } const mod = event.metaKey || event.ctrlKey; const key = event.key.toLowerCase(); if (mod && !event.altKey && !event.shiftKey && key === "f") { if (!isEditing(controller) && !previewSearchAvailable(controller)) { return; } event.preventDefault(); open(controller); return; } if (!isPanelOpen(controller)) { return; } if (mod && key === "g") { event.preventDefault(); navigate(controller, event.shiftKey ? -1 : 1); return; } if (event.key === "Escape") { close(controller); } } function open(controller) { ensurePanel(controller); const panel = controller.panel; const input = controller.input; if (!panel || !input) { return; } controller.mode = isEditing(controller) ? "edit" : "preview"; panel.dataset.tlfSearchMode = controller.mode; if (controller.replaceRow) { controller.replaceRow.hidden = controller.mode === "preview"; } const placeholder = controller.mode === "preview" && controller.previewSearch ? controller.previewSearch.placeholder() : TabLocalFiles.t("code.searchPlaceholder"); input.placeholder = placeholder; input.setAttribute("aria-label", placeholder); panel.hidden = false; if (controller.mode === "edit") { const { selectionStart, selectionEnd, value } = controller.textarea; if (selectionEnd > selectionStart) { const selected = value.slice(selectionStart, selectionEnd); if (selected.trim() && !selected.includes("\n")) { input.value = selected; } } } input.focus(); input.select(); runSearch(controller); } function close(controller) { if (!controller.panel || controller.panel.hidden) { return; } const current = controller.panel.contains(document.activeElement) ? controller.matches[controller.matchIndex] : undefined; controller.panel.hidden = true; controller.matches = []; controller.matchIndex = -1; if (controller.debounceTimer !== undefined) { window.clearTimeout(controller.debounceTimer); controller.debounceTimer = undefined; } controller.matchLayer.clear(); if (controller.mode === "preview") { controller.previewSearch?.close(); return; } if (isEditing(controller)) { if (current) { try { controller.textarea.setSelectionRange(current.from, current.to); } catch { } } controller.textarea.focus(); } } function ensurePanel(controller) { if (controller.panel) { return; } const body = controller.root.querySelector("[data-tlf-editor-body]"); if (!body) { return; } const panel = document.createElement("div"); panel.className = "tlf-search-panel"; panel.setAttribute("data-tlf-search-panel", ""); panel.hidden = true; panel.innerHTML = `
    0 / 0
    `; body.appendChild(panel); controller.panel = panel; controller.input = panel.querySelector("[data-tlf-search-input]") || undefined; controller.replaceRow = panel.querySelector("[data-tlf-code-replace-row]") || undefined; controller.replaceInput = panel.querySelector("[data-tlf-replace-input]") || undefined; controller.count = panel.querySelector("[data-tlf-search-count]") || undefined; controller.input?.addEventListener("input", () => { if (controller.debounceTimer !== undefined) { window.clearTimeout(controller.debounceTimer); } controller.debounceTimer = window.setTimeout(() => { controller.debounceTimer = undefined; runSearch(controller); }, SEARCH_DEBOUNCE_MS); }); controller.input?.addEventListener("keydown", (event) => { if (TabLocalFiles.isTextCompositionEvent(event)) { return; } if (event.key === "Enter") { event.preventDefault(); navigate(controller, event.shiftKey ? -1 : 1); } else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(controller); } }); controller.replaceInput?.addEventListener("keydown", (event) => { if (TabLocalFiles.isTextCompositionEvent(event)) { return; } if (event.key === "Enter") { event.preventDefault(); replaceCurrent(controller); } else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(controller); } }); panel.querySelector("[data-tlf-search-prev]")?.addEventListener("click", () => navigate(controller, -1)); panel.querySelector("[data-tlf-search-next]")?.addEventListener("click", () => navigate(controller, 1)); panel.querySelector("[data-tlf-search-close]")?.addEventListener("click", () => close(controller)); panel.querySelector("[data-tlf-replace-one]")?.addEventListener("click", () => replaceCurrent(controller)); panel.querySelector("[data-tlf-replace-all]")?.addEventListener("click", () => replaceEverything(controller)); } function runSearch(controller, anchorOffset, reveal = true) { const query = controller.input?.value || ""; if (controller.mode === "preview") { const status = controller.previewSearch?.search(query) || { total: 0, index: -1 }; controller.matches = []; controller.matchIndex = status.index; updateCount(controller, status.total, status.index); return; } controller.searchedText = controller.textarea.value; controller.matches = TabLocalFiles.MarkdownSearch.collectTextMatches(controller.searchedText, query); if (!controller.matches.length) { controller.matchIndex = -1; } else if (anchorOffset === undefined) { controller.matchIndex = 0; } else { const at = controller.matches.findIndex((match) => match.from >= anchorOffset); controller.matchIndex = at >= 0 ? at : 0; } controller.matchLayer.paint(controller.matches, controller.matchIndex, reveal); updateCount(controller, controller.matches.length, controller.matchIndex); } function navigate(controller, direction) { if (controller.mode === "preview") { const status = controller.previewSearch?.navigate(direction) || { total: 0, index: -1 }; controller.matchIndex = status.index; updateCount(controller, status.total, status.index); return; } if (!controller.matches.length) { return; } controller.matchIndex = (controller.matchIndex + direction + controller.matches.length) % controller.matches.length; controller.matchLayer.paint(controller.matches, controller.matchIndex, true); updateCount(controller, controller.matches.length, controller.matchIndex); } function replaceCurrent(controller) { if (controller.mode !== "edit" || controller.matchIndex < 0 || !controller.matches.length) { return; } const match = controller.matches[controller.matchIndex]; const replacement = controller.replaceInput?.value ?? ""; TabLocalFiles.TextEditor.transformSelection(controller.rootId, () => ({ replacement, rangeStart: match.from, rangeEnd: match.to, selectionStart: match.from, selectionEnd: match.from + replacement.length })); runSearch(controller, match.from + replacement.length); controller.replaceInput?.focus(); } function replaceEverything(controller) { const query = controller.input?.value || ""; if (controller.mode !== "edit" || !query) { return; } const replacement = controller.replaceInput?.value ?? ""; let replacedCount = 0; TabLocalFiles.TextEditor.transformSelection(controller.rootId, (value, selectionStart) => { const result = replaceAllMatches(value, query, replacement); if (!result.count) { return undefined; } replacedCount = result.count; const caret = Math.min(selectionStart, result.text.length); return { replacement: result.text, rangeStart: 0, rangeEnd: value.length, selectionStart: caret, selectionEnd: caret }; }); if (replacedCount) { TabLocalFiles.showToast(TabLocalFiles.t("code.replacedCount", { count: replacedCount }), "success"); } runSearch(controller); controller.replaceInput?.focus(); } function updateCount(controller, total, index) { if (controller.count) { controller.count.textContent = `${total ? index + 1 : 0} / ${total}`; } } })(CodeTextSearch = TabLocalFiles.CodeTextSearch || (TabLocalFiles.CodeTextSearch = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var SubtitleTranslation; (function (SubtitleTranslation) { const TAB_TRANSLATION_EXTENSION_ID = "iobiajbbjgoiehijaledajjiainhmafb"; const TRANSLATE_MESSAGE = "tabTranslation.previewerTranslateBatch"; const CAPABILITY_MESSAGE = "tabTranslation.previewerTranslateCapability"; const MAX_BLOCKS_PER_BATCH = 48; const MAX_CHARACTERS_PER_BATCH = 40000; const MAX_CONCURRENT_BATCHES = 2; const REQUEST_TIMEOUT_MS = 100000; const OVERLAY_ID = "tlf-subtitle-translate-overlay"; const LANGUAGE_OPTIONS = [ { code: "cmn", label: "简体中文", fileTag: "zh", script: "han" }, { code: "cmn-Hant", label: "繁體中文", fileTag: "zh-Hant", script: "han" }, { code: "eng", label: "English", fileTag: "en", script: "latin" }, { code: "jpn", label: "日本語", fileTag: "ja", script: "kana" }, { code: "kor", label: "한국어", fileTag: "ko", script: "hangul" }, { code: "fra", label: "Français", fileTag: "fr", script: "latin" }, { code: "deu", label: "Deutsch", fileTag: "de", script: "latin" }, { code: "spa", label: "Español", fileTag: "es", script: "latin" }, { code: "rus", label: "Русский", fileTag: "ru", script: "cyrillic" } ]; function languageOption(code) { return LANGUAGE_OPTIONS.find((option) => option.code === code); } function cueNeedsTranslation(text, targetCode) { const option = languageOption(targetCode); if (!option || !text.trim()) { return false; } for (const line of text.split("\n")) { if (lineMatchesScript(line, option.script)) { return false; } } return true; } SubtitleTranslation.cueNeedsTranslation = cueNeedsTranslation; function lineMatchesScript(line, script) { const counts = TabLocalFiles.CodeText.countSubtitleLineScripts(line); const letters = counts.han + counts.kana + counts.hangul + counts.latin + counts.cyrillic + counts.arabic; if (!letters) { return false; } switch (script) { case "han": return counts.kana === 0 && counts.han * 2 >= letters; case "kana": return counts.kana > 0 && (counts.kana + counts.han) * 2 >= letters; case "hangul": return counts.hangul * 2 >= letters; case "latin": return counts.latin * 2 >= letters; case "cyrillic": return counts.cyrillic * 2 >= letters; } } function bilingualMergedText(original, translated) { return `${translated}\n${original}`; } SubtitleTranslation.bilingualMergedText = bilingualMergedText; function dedupeTranslationItems(cues) { const seen = new Set(); const items = []; for (const cue of cues) { if (seen.has(cue.text)) { continue; } seen.add(cue.text); items.push({ id: String(cue.index), text: cue.text }); } return items; } SubtitleTranslation.dedupeTranslationItems = dedupeTranslationItems; function planSubtitleTranslationBatches(items, maxBlocks, maxCharacters) { const batches = []; let current = []; let characters = 0; for (const item of items) { if (current.length > 0 && (current.length >= maxBlocks || characters + item.text.length > maxCharacters)) { batches.push(current); current = []; characters = 0; } current.push(item); characters += item.text.length; } if (current.length) { batches.push(current); } return batches; } SubtitleTranslation.planSubtitleTranslationBatches = planSubtitleTranslationBatches; function applyTranslationsToCues(cues, translationsByText, mode, targetCode) { return cues.map((cue) => { if (!cueNeedsTranslation(cue.text, targetCode)) { return cue; } const translated = translationsByText.get(cue.text)?.trim(); if (!translated) { return cue; } return { ...cue, text: mode === "bilingual" ? bilingualMergedText(cue.text, translated) : translated }; }); } SubtitleTranslation.applyTranslationsToCues = applyTranslationsToCues; function sendToTranslationService(payload) { return new Promise((resolve, reject) => { const runtime = globalThis .chrome?.runtime; if (!runtime?.sendMessage) { reject(new Error("runtime unavailable")); return; } let settled = false; const timeoutId = window.setTimeout(() => { if (!settled) { settled = true; reject(new Error("timeout")); } }, REQUEST_TIMEOUT_MS); try { runtime.sendMessage(TAB_TRANSLATION_EXTENSION_ID, payload, (response) => { if (settled) { return; } settled = true; window.clearTimeout(timeoutId); const lastError = runtime.lastError?.message; if (lastError || !response) { reject(new Error(lastError || "no response")); return; } resolve(response); }); } catch (error) { if (!settled) { settled = true; window.clearTimeout(timeoutId); reject(error instanceof Error ? error : new Error(String(error))); } } }); } let state; function bind(context, extension, viewState) { TabLocalFiles.onToolbarAction("code.subtitle-translate", () => { const text = TabLocalFiles.TextEditor.getText("tlf-code-editor") || ""; const parsed = TabLocalFiles.CodeText.parseSubtitles(text, extension); if (!parsed || parsed.cues.length === 0) { TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleExportUnparseable"), "error"); return; } state = { context, extension, cues: parsed.cues, sourceText: text, mode: "bilingual", targetCode: defaultTargetCode(parsed.cues), output: "new", overwriteAllowed: extension === ".srt" && viewState.subtitleTranscodable, running: false, cancelled: false, requestSequence: 0 }; openDialog(); }); if (context.mountId !== undefined) { TabLocalFiles.registerDocumentMountCleanup(context.mountId, () => { const run = state; if (!run || run.context.mountId !== context.mountId) { return; } if (run.running) { recordRunResult(run, "cancelled"); } run.cancelled = true; state = undefined; document.getElementById(OVERLAY_ID)?.remove(); }); } } SubtitleTranslation.bind = bind; function recordRunResult(run, result, failureReason) { TabLocalFiles.recordFileTranslateResult(run.context, { kind: "subtitle", result, failureReason, output: run.output === "overwrite" && run.overwriteAllowed ? "overwrite" : "export", targetCode: run.targetCode, blockCount: run.cues.filter((cue) => cueNeedsTranslation(cue.text, run.targetCode)).length, mode: run.mode }); } function defaultTargetCode(cues) { const analysis = TabLocalFiles.CodeText.analyzeSubtitleCues(cues); return analysis.languages[0] === "chinese" ? "eng" : "cmn"; } function openDialog() { const overlay = ensureOverlay(); const run = state; if (!overlay || !run) { return; } overlay.hidden = false; setRunning(false); syncForm(); syncSummary(); setError(""); setProgress(undefined); void probeCapability(run); overlay.querySelector("[data-tlf-translate-language]")?.focus(); } async function probeCapability(run) { try { const response = await sendToTranslationService({ type: CAPABILITY_MESSAGE }); if (!isRunMounted(run)) { return; } if (!response.ok) { setError(TabLocalFiles.t("code.subtitleTranslateUnavailable")); } } catch { if (isRunMounted(run)) { setError(TabLocalFiles.t("code.subtitleTranslateUnavailable")); } } } function ensureOverlay() { let overlay = document.getElementById(OVERLAY_ID); if (overlay) { return overlay; } overlay = document.createElement("div"); overlay.id = OVERLAY_ID; overlay.className = "tlf-subtitle-translate-overlay"; overlay.hidden = true; overlay.innerHTML = ` `; document.body.appendChild(overlay); overlay.addEventListener("click", (event) => { if (event.target === overlay) { requestClose(); return; } const target = event.target; if (target?.closest("[data-tlf-translate-cancel]")) { requestClose(); } else if (target?.closest("[data-tlf-translate-start]")) { void startTranslation(); } }); overlay.addEventListener("keydown", (event) => { if (event.key === "Escape") { event.stopPropagation(); requestClose(); } }); overlay.addEventListener("change", (event) => { const input = event.target; if (!state || !input) { return; } if (input instanceof HTMLSelectElement && input.dataset.tlfTranslateLanguage !== undefined) { state.targetCode = input.value; } else if (input instanceof HTMLInputElement && input.name === "tlf-translate-mode") { state.mode = input.value === "replace" ? "replace" : "bilingual"; } else if (input instanceof HTMLInputElement && input.name === "tlf-translate-output") { state.output = input.value === "overwrite" ? "overwrite" : "new"; } syncSummary(); }); return overlay; } function overlayElement(selector) { return document.getElementById(OVERLAY_ID)?.querySelector(selector) || null; } function syncForm() { if (!state) { return; } const language = overlayElement("[data-tlf-translate-language]"); if (language) { language.value = state.targetCode; } const overlay = document.getElementById(OVERLAY_ID); overlay?.querySelectorAll('input[name="tlf-translate-mode"]').forEach((input) => { input.checked = input.value === state?.mode; }); const overwriteInput = overlay?.querySelector('input[name="tlf-translate-output"][value="overwrite"]'); const overwriteChoice = overlayElement("[data-tlf-translate-overwrite-choice]"); const overwriteHint = overlayElement("[data-tlf-translate-overwrite-hint]"); if (overwriteInput) { overwriteInput.disabled = !state.overwriteAllowed; if (!state.overwriteAllowed) { state.output = "new"; } } overwriteChoice?.classList.toggle("tlf-subtitle-translate-choice-disabled", !state.overwriteAllowed); if (overwriteHint) { overwriteHint.hidden = state.overwriteAllowed; } overlay?.querySelectorAll('input[name="tlf-translate-output"]').forEach((input) => { input.checked = input.value === state?.output; }); setRunning(false); } function pendingCues() { if (!state) { return []; } const targetCode = state.targetCode; return state.cues.filter((cue) => cueNeedsTranslation(cue.text, targetCode)); } function syncSummary() { const summary = overlayElement("[data-tlf-translate-summary]"); const start = overlayElement("[data-tlf-translate-start]"); if (!summary || !state) { return; } const pending = pendingCues(); const uniqueCharacters = dedupeTranslationItems(pending).reduce((sum, item) => sum + item.text.length, 0); if (!pending.length) { summary.textContent = TabLocalFiles.t("code.subtitleTranslateNothing"); } else { summary.textContent = TabLocalFiles.t("code.subtitleTranslateSummary", { count: pending.length, characters: uniqueCharacters }); } if (start) { start.disabled = !pending.length || state.running; } } function setError(message) { const error = overlayElement("[data-tlf-translate-error]"); if (error) { error.textContent = message; error.hidden = !message; } } function setProgress(progress) { const container = overlayElement("[data-tlf-translate-progress]"); const bar = overlayElement("[data-tlf-translate-progress-bar]"); const text = overlayElement("[data-tlf-translate-progress-text]"); if (!container || !bar || !text) { return; } if (!progress) { container.hidden = true; bar.style.width = "0%"; text.textContent = ""; return; } container.hidden = false; const ratio = progress.total ? Math.round((progress.completed / progress.total) * 100) : 0; bar.style.width = `${ratio}%`; text.textContent = TabLocalFiles.t("code.subtitleTranslateRunning", { completed: progress.completed, total: progress.total }); } function setRunning(running) { if (state) { state.running = running; } const overlay = document.getElementById(OVERLAY_ID); overlay?.querySelectorAll("input, select").forEach((input) => { if (input instanceof HTMLInputElement && input.value === "overwrite" && !state?.overwriteAllowed) { input.disabled = true; return; } input.disabled = running; }); const start = overlayElement("[data-tlf-translate-start]"); if (start) { start.disabled = running || pendingCues().length === 0; } } function requestClose() { if (state?.running) { state.cancelled = true; return; } closeDialog(); } function closeDialog() { const overlay = document.getElementById(OVERLAY_ID); if (overlay) { overlay.hidden = true; } state = undefined; } function isRunMounted(run) { return (state === run && (run.context.mountId === undefined || TabLocalFiles.isCurrentDocumentMount(run.context.mountId))); } async function startTranslation() { if (!state || state.running) { return; } const run = state; const pending = pendingCues(); if (!pending.length) { return; } run.cancelled = false; setError(""); setRunning(true); const items = dedupeTranslationItems(pending); const textById = new Map(items.map((item) => [item.id, item.text])); const batches = planSubtitleTranslationBatches(items, MAX_BLOCKS_PER_BATCH, MAX_CHARACTERS_PER_BATCH); const translations = new Map(); let completed = 0; let workerFailed = false; let serviceRejected = false; setProgress({ completed, total: items.length }); try { let nextBatch = 0; const worker = async () => { while (nextBatch < batches.length) { if (run.cancelled || !isRunMounted(run)) { return; } const batch = batches[nextBatch]; nextBatch += 1; run.requestSequence += 1; let response; try { response = await sendToTranslationService({ type: TRANSLATE_MESSAGE, requestId: `subtitle-${run.requestSequence}`, targetCode: run.targetCode, blocks: batch }); } catch (error) { if (!run.cancelled) { workerFailed = true; } run.cancelled = true; throw error; } if (run.cancelled || !isRunMounted(run)) { return; } if (!response.ok) { workerFailed = true; run.cancelled = true; serviceRejected = true; throw new Error(response.error || "translation failed"); } for (const block of response.blocks || []) { const sourceText = textById.get(block.id); if (sourceText && block.text) { translations.set(sourceText, block.text); } } completed += batch.length; setProgress({ completed, total: items.length }); } }; await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT_BATCHES, batches.length) }, () => worker())); if (!isRunMounted(run)) { return; } if (run.cancelled) { recordRunResult(run, "cancelled"); setRunning(false); setProgress(undefined); closeDialog(); TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleTranslateCancelled"), "neutral"); return; } await persistTranslation(run, translations, pending.length); } catch (error) { const userCancelled = run.cancelled && !workerFailed; run.cancelled = true; if (!isRunMounted(run)) { return; } setRunning(false); setProgress(undefined); if (userCancelled) { recordRunResult(run, "cancelled"); closeDialog(); TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleTranslateCancelled"), "neutral"); return; } recordRunResult(run, "failed", serviceRejected ? "service_error" : workerFailed ? TabLocalFiles.translateTransportFailureReason(error) : "unknown"); const message = error instanceof Error ? error.message : String(error); setError(TabLocalFiles.t("code.subtitleTranslateFailed", { error: message })); syncSummary(); } } async function persistTranslation(run, translations, translatedCount) { const merged = applyTranslationsToCues(run.cues, translations, run.mode, run.targetCode); const option = languageOption(run.targetCode); const targetExtension = run.extension === ".vtt" ? ".vtt" : ".srt"; const serialized = TabLocalFiles.CodeText.serializeSubtitleCues(merged, targetExtension === ".vtt" ? "vtt" : "srt"); if (run.output === "overwrite" && run.overwriteAllowed) { if (!isRunMounted(run)) { return; } if (TabLocalFiles.TextEditor.getText("tlf-code-editor") !== run.sourceText) { recordRunResult(run, "failed", "source_changed"); setRunning(false); setProgress(undefined); setError(TabLocalFiles.t("code.subtitleTranslateSourceChanged")); return; } const saved = await TabLocalFiles.TextEditor.saveReplacement("tlf-code-editor", serialized, run.sourceText); if (!isRunMounted(run)) { return; } if (!saved) { recordRunResult(run, "failed", "save_failed"); setRunning(false); setProgress(undefined); return; } recordRunResult(run, "success"); closeDialog(); TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleTranslateDoneOverwrite", { count: translatedCount }), "success"); return; } const baseName = run.context.fileName.replace(/\.[^.]+$/, ""); const fileName = `${baseName}.${option?.fileTag || run.targetCode}${targetExtension}`; TabLocalFiles.saveDerivedTextFile(fileName, serialized, TabLocalFiles.mimeTypeFromExtension(targetExtension)); recordRunResult(run, "success"); closeDialog(); TabLocalFiles.showToast(TabLocalFiles.t("code.subtitleTranslateDoneExported", { count: translatedCount, fileName }), "success"); } })(SubtitleTranslation = TabLocalFiles.SubtitleTranslation || (TabLocalFiles.SubtitleTranslation = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var CodeText; (function (CodeText) { const JSONL_EXTENSIONS = new Set([".jsonl", ".ndjson"]); const JSONL_META_ID = "tlf-jsonl-meta"; const JSONL_STATUS_ID = "tlf-jsonl-status"; const JSONL_RECORD_DIGITS = 5; const INVALID_EXCERPT_LENGTH = 80; function isJsonLinesExtension(extension) { return JSONL_EXTENSIONS.has(extension); } CodeText.isJsonLinesExtension = isJsonLinesExtension; let lastParse; function parseJsonLines(value) { if (lastParse?.value === value) { return lastParse.parsed; } const entries = []; let invalid = 0; let firstInvalidLine; value.replace(/^\uFEFF/, "").split(/\r\n|\r|\n/).forEach((text, index) => { if (!text.trim()) { return; } const line = index + 1; try { entries.push({ line, value: JSON.parse(text) }); } catch { entries.push({ line, invalid: text }); invalid += 1; firstInvalidLine ?? (firstInvalidLine = line); } }); const parsed = { entries, records: entries.length - invalid, invalid, firstInvalidLine }; lastParse = { value, parsed }; return parsed; } function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } CodeText.jsonlSubRenderer = { id: "jsonl", matches: (extension) => isJsonLinesExtension(extension), renderPreview: (value, _extension, fileName, viewState) => { const view = viewState?.jsonlView ?? "table"; if (view === "source") { return undefined; } return view === "tree" ? renderJsonlTree(value, viewState?.jsonCollapsed ?? new Set()) : renderJsonlTable(value, fileName); }, contributeToolbar: (toolbar, { rawText }) => { toolbar.contextTrailing = [TabLocalFiles.toolbarCustom("code.jsonl-status", renderJsonlStatus(rawText))]; toolbar.contextPreview = [ TabLocalFiles.segmentedToolbar("code.jsonl-view", TabLocalFiles.t("code.jsonlViewMode"), [ { key: "table", label: TabLocalFiles.t("code.csvViewTable"), icon: "table", pressed: true }, { key: "tree", label: TabLocalFiles.t("code.jsonStyleTree"), icon: "list-tree" }, { key: "source", label: TabLocalFiles.t("code.csvViewRaw"), icon: "code" } ]), TabLocalFiles.toolbarCustom("code.jsonl-meta", renderJsonlMeta(rawText)) ]; }, bind: ({ viewState, refresh }) => { ["table", "tree", "source"].forEach((view) => { TabLocalFiles.onToolbarAction(`code.jsonl-view.${view}`, () => { viewState.jsonlView = view; TabLocalFiles.syncSegmentedToolbar("code.jsonl-view", view); refresh(); }); }); CodeText.bindJsonFoldControls(viewState); }, afterPreviewRender: (_extension, value) => syncJsonlStatus(value) }; function renderJsonlTable(value, fileName) { const { entries } = parseJsonLines(value); const shown = entries.slice(0, CodeText.DELIMITED_TABLE_ROW_LIMIT); const keys = []; const seen = new Set(); for (const entry of shown) { if (isRecord(entry.value)) { for (const key of Object.keys(entry.value)) { if (!seen.has(key)) { seen.add(key); keys.push(key); } } } } const columns = keys.slice(0, CodeText.DELIMITED_TABLE_COLUMN_LIMIT); const header = columns.length ? columns : [TabLocalFiles.t("code.jsonlValueColumn")]; const span = header.length; const rows = shown.map((entry) => { const line = `${entry.line}`; if (entry.invalid !== undefined) { const excerpt = entry.invalid.length > INVALID_EXCERPT_LENGTH ? `${entry.invalid.slice(0, INVALID_EXCERPT_LENGTH)}…` : entry.invalid; const message = TabLocalFiles.t("code.jsonlInvalidLine", { line: entry.line, text: excerpt }); return `${line}${TabLocalFiles.escapeHtml(message)}`; } const record = entry.value; if (isRecord(record) && columns.length) { const cells = columns .map((key) => `${Object.prototype.hasOwnProperty.call(record, key) ? renderJsonlCell(record[key]) : ""}`) .join(""); return `${line}${cells}`; } return `${line}${renderJsonlCell(record)}`; }); const notice = entries.length > shown.length || keys.length > columns.length ? `

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.tablePreviewLimited", { rows: shown.length, columns: span }))}

    ` : ""; return `
    ${header .map((label) => ``) .join("")} ${rows.join("")}
    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.jsonlLineColumn"))}${TabLocalFiles.escapeHtml(label)}
    ${notice}
    `; } function renderJsonlCell(value) { if (typeof value === "string") { return TabLocalFiles.escapeHtml(value); } if (typeof value === "object" && value !== null) { return TabLocalFiles.escapeHtml(JSON.stringify(value)); } return CodeText.renderJsonScalar(value); } function renderJsonlTree(value, collapsed) { const { entries } = parseJsonLines(value); const shown = entries.slice(0, CodeText.DELIMITED_TABLE_ROW_LIMIT); const items = shown .map((entry) => { const label = TabLocalFiles.t("code.jsonlLineLabel", { line: entry.line }); return entry.invalid !== undefined ? CodeText.renderJsonTreeLeaf(label, `${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.jsonlInvalid"))}`) : CodeText.renderJsonTreeItem(entry.value, label, String(entry.line), collapsed); }) .join(""); const notice = entries.length > shown.length ? `

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.jsonlTreeLimited", { count: shown.length }))}

    ` : ""; return ` ${notice}
      ${items}
    `; } function renderJsonlMeta(value) { const slot = TabLocalFiles.renderDigitSlot(String(parseJsonLines(value).records), JSONL_RECORD_DIGITS, { name: "jsonl-records" }); return `${TabLocalFiles.tHtml("code.jsonlRecords", { count: slot })}`; } function jsonlStatus(value) { const { invalid, firstInvalidLine } = parseJsonLines(value); return invalid ? { valid: false, label: TabLocalFiles.t("code.jsonlInvalidSummary", { count: invalid, line: firstInvalidLine ?? 0 }) } : { valid: true, label: TabLocalFiles.t("code.jsonlValid") }; } function renderJsonlStatus(value) { const { valid, label } = jsonlStatus(value); return ` ${TabLocalFiles.renderLucideIcon(valid ? "circle-check" : "circle-alert")} ${TabLocalFiles.escapeHtml(label)} `; } function syncJsonlStatus(value) { TabLocalFiles.setDigitSlot(document.getElementById(JSONL_META_ID), "jsonl-records", String(parseJsonLines(value).records)); const badge = document.getElementById(JSONL_STATUS_ID); if (!badge) { return; } const { valid, label } = jsonlStatus(value); badge.dataset.state = valid ? "valid" : "invalid"; badge.dataset.tlfTooltip = label; badge.innerHTML = `${TabLocalFiles.renderLucideIcon(valid ? "circle-check" : "circle-alert")}${TabLocalFiles.escapeHtml(label)}`; } })(CodeText = TabLocalFiles.CodeText || (TabLocalFiles.CodeText = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var CodeText; (function (CodeText) { CodeText.EDITOR_ROOT_ID = "tlf-code-editor"; const FORMAT_BUTTON_ID = "tlf-code-format-action"; const CODE_CONTENT_ID = "tlf-code-content"; const INDENT = " "; CodeText.DELIMITED_TABLE_ROW_LIMIT = 1000; CodeText.DELIMITED_TABLE_COLUMN_LIMIT = 100; const JSON_PREVIEW_INDENT = 2; const JSON_INDENT_UNIT = " ".repeat(JSON_PREVIEW_INDENT); const JSON_STRUCTURED_LINE_LIMIT = 5000; const MARKDOWN_SOURCE_EXTENSIONS = new Set([".mdx"]); const LANGUAGE_BY_EXTENSION = { ".ass": "Advanced SubStation Alpha", ".bib": "BibTeX", ".c": "C", ".cc": "C++", ".cjs": "JavaScript", ".cpp": "C++", ".csv": "CSV", ".css": "CSS", ".cxx": "C++", ".dfxp": "TTML (DFXP)", ".go": "Go", ".h": "C/C++ Header", ".hh": "C++ Header", ".hpp": "C++ Header", ".hxx": "C++ Header", ".idx": "VobSub IDX", ".ini": "INI", ".java": "Java", ".js": "JavaScript", ".json": "JSON", ".jsonc": "JSON with Comments", ".jsonl": "JSON Lines", ".jsx": "JSX", ".less": "Less", ".log": "Log", ".lrc": "LRC Lyrics", ".mdx": "MDX", ".mjs": "JavaScript", ".mpl": "MPL2", ".ndjson": "NDJSON", ".py": "Python", ".rb": "Ruby", ".rs": "Rust", ".rtf": "RTF", ".sbv": "SubViewer", ".scss": "Sass", ".sh": "Shell", ".smi": "SAMI", ".sql": "SQL", ".srt": "SubRip", ".ssa": "SubStation Alpha", ".sub": "MicroDVD / SubViewer", ".swift": "Swift", ".tex": "TeX", ".text": "Text", ".toml": "TOML", ".ts": "TypeScript", ".tsx": "TSX", ".tsv": "TSV", ".ttml": "TTML", ".txt": "Text", ".vtt": "WebVTT", ".vue": "Vue", ".xml": "XML", ".yaml": "YAML", ".yml": "YAML" }; const HIGHLIGHT_LANGUAGE_BY_EXTENSION = { ".ass": "plain", ".bib": "plain", ".c": "c", ".cc": "c", ".cjs": "js", ".cpp": "c", ".csv": "plain", ".css": "css", ".cxx": "c", ".dfxp": "plain", ".go": "go", ".h": "c", ".hh": "c", ".hpp": "c", ".hxx": "c", ".idx": "plain", ".ini": "ini", ".java": "java", ".js": "js", ".json": "json", ".jsonc": "json", ".jsonl": "json", ".jsx": "js", ".less": "css", ".log": "log", ".lrc": "plain", ".mdx": "md", ".mjs": "js", ".mpl": "plain", ".ndjson": "json", ".py": "py", ".rb": "plain", ".rs": "rs", ".rtf": "plain", ".sbv": "plain", ".scss": "css", ".sh": "bash", ".smi": "plain", ".sql": "sql", ".srt": "plain", ".ssa": "plain", ".sub": "plain", ".swift": "plain", ".tex": "plain", ".text": "plain", ".toml": "toml", ".ts": "ts", ".tsx": "ts", ".tsv": "plain", ".ttml": "plain", ".txt": "plain", ".vtt": "plain", ".vue": "html", ".xml": "xml", ".yaml": "yaml", ".yml": "yaml" }; const BRACE_EXPAND_EXTENSIONS = new Set([ ".c", ".cc", ".cjs", ".cpp", ".css", ".cxx", ".go", ".h", ".hh", ".hpp", ".hxx", ".java", ".js", ".jsx", ".less", ".mjs", ".rs", ".scss", ".swift", ".ts", ".tsx" ]); const MARKUP_EXPAND_EXTENSIONS = new Set([".xml"]); const DELIMITED_TEXT_EXTENSIONS = new Set([".csv", ".tsv"]); const DATA_TREE_EXTENSIONS = new Set([".yaml", ".yml", ".toml"]); const LOG_ERROR_LINE_PATTERN = /\b(error|err|fail|failed|failure|exception|fatal|panic)\b/i; const JSON_STATUS_ID = "tlf-json-status"; const CSV_META_ID = "tlf-csv-meta"; const CSV_ROW_DIGITS = 5; const CSV_COLUMN_DIGITS = 3; const HIGHLIGHT_SELECT_ID = "tlf-code-highlight-language"; const HIGHLIGHT_LANGUAGE_OPTIONS = [ { value: "plain", label: "Plain Text" }, { value: "asm", label: "Assembly" }, { value: "bash", label: "Bash / Shell" }, { value: "c", label: "C / C++" }, { value: "css", label: "CSS" }, { value: "diff", label: "Diff" }, { value: "docker", label: "Dockerfile" }, { value: "go", label: "Go" }, { value: "html", label: "HTML" }, { value: "http", label: "HTTP" }, { value: "ini", label: "INI" }, { value: "java", label: "Java" }, { value: "js", label: "JavaScript" }, { value: "json", label: "JSON" }, { value: "log", label: "Log" }, { value: "lua", label: "Lua" }, { value: "make", label: "Makefile" }, { value: "md", label: "Markdown" }, { value: "pl", label: "Perl" }, { value: "py", label: "Python" }, { value: "rs", label: "Rust" }, { value: "sql", label: "SQL" }, { value: "toml", label: "TOML" }, { value: "ts", label: "TypeScript" }, { value: "xml", label: "XML" }, { value: "yaml", label: "YAML" } ]; let codeSubRenderers; function codeSubRendererFor(extension) { codeSubRenderers || (codeSubRenderers = [ jsonSubRenderer, CodeText.jsonlSubRenderer, dataTreeSubRenderer, delimitedSubRenderer, logSubRenderer, CodeText.subtitleSubRenderer ]); return codeSubRenderers.find((entry) => entry.matches(extension)); } const jsonSubRenderer = { id: "json", matches: (extension) => extension === ".json", renderPreview: (value, extension, _fileName, viewState) => { if (!viewState || viewState.jsonStyle === "original") { return undefined; } return renderJsonStructuredPreview(value, extension, viewState); }, contributeToolbar: (toolbar) => { toolbar.contextTrailing = [TabLocalFiles.toolbarCustom("code.json-status", renderJsonStatusBadge())]; toolbar.contextPreview = [ TabLocalFiles.segmentedToolbar("code.json-style", TabLocalFiles.t("code.jsonRawStyle"), [ { key: "original", label: TabLocalFiles.t("code.jsonStyleOriginal"), pressed: true }, { key: "pretty", label: TabLocalFiles.t("code.jsonStylePretty") }, { key: "tree", label: TabLocalFiles.t("code.jsonStyleTree") }, { key: "compact", label: TabLocalFiles.t("code.jsonStyleCompact") } ]) ]; toolbar.contextEdit = [TabLocalFiles.toolbarCustom("code.json-format-menu", renderJsonFormatMenu())]; }, bind: ({ viewState, refresh }) => { ["original", "pretty", "tree", "compact"].forEach((style) => { TabLocalFiles.onToolbarAction(`code.json-style.${style}`, () => { if (jsonViewNeedsValidJson(style) && !isJsonParseable(TabLocalFiles.TextEditor.getText(CodeText.EDITOR_ROOT_ID) || "")) { TabLocalFiles.showToast(TabLocalFiles.t("code.invalidJson"), "error"); return; } viewState.jsonStyle = style; TabLocalFiles.syncSegmentedToolbar("code.json-style", style); refresh(); }); }); bindJsonFoldControls(viewState); bindJsonCopyAll(viewState); const applyJsonTransform = (transform, successKey, button) => { button.closest("details")?.removeAttribute("open"); const currentText = TabLocalFiles.TextEditor.getText(CodeText.EDITOR_ROOT_ID) || ""; const next = transform(currentText); if (next === undefined) { TabLocalFiles.showToast(TabLocalFiles.t("code.invalidJson"), "error"); return; } if (next !== currentText) { TabLocalFiles.TextEditor.setText(CodeText.EDITOR_ROOT_ID, next, true); } TabLocalFiles.showToast(TabLocalFiles.t(successKey), "success"); }; [2, 4].forEach((indent) => { TabLocalFiles.onToolbarAction(`code.json-format.pretty-${indent}`, (detail) => { applyJsonTransform((value) => formatJsonWithIndent(value, indent), "code.formatApplied", detail.button); }); }); TabLocalFiles.onToolbarAction("code.json-format.compact", (detail) => { applyJsonTransform(compactJson, "code.compactApplied", detail.button); }); }, afterPreviewRender: (extension, value) => syncJsonStatusBadge(extension, value) }; const dataTreeSubRenderer = { id: "data-tree", matches: (extension) => isDataTreeExtension(extension), renderPreview: (_value, _extension, _fileName, viewState) => { if (viewState?.dataView !== "tree") { return undefined; } return `
    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.dataTreeLoading"))}
    `; }, contributeToolbar: (toolbar) => { toolbar.contextPreview = [ TabLocalFiles.segmentedToolbar("code.data-view", TabLocalFiles.t("code.dataViewMode"), [ { key: "raw", label: TabLocalFiles.t("code.csvViewRaw"), icon: "code", pressed: true }, { key: "tree", label: TabLocalFiles.t("code.jsonStyleTree"), icon: "list-tree" } ]) ]; }, bind: ({ viewState, refresh }) => { ["raw", "tree"].forEach((view) => { TabLocalFiles.onToolbarAction(`code.data-view.${view}`, () => { viewState.dataView = view; TabLocalFiles.syncSegmentedToolbar("code.data-view", view); refresh(); }); }); bindJsonFoldControls(viewState); }, afterPreviewRender: (extension, value, viewState) => { void fillDataTreePanel(extension, value, viewState); } }; const delimitedSubRenderer = { id: "delimited-table", matches: (extension) => isDelimitedTextExtension(extension), renderPreview: (value, extension, fileName, viewState) => viewState?.csvView === "raw" ? undefined : renderDelimitedTablePreview(value, extension, fileName), contributeToolbar: (toolbar, { extension, rawText }) => { toolbar.contextPreview = [ TabLocalFiles.segmentedToolbar("code.csv-view", TabLocalFiles.t("code.csvViewMode"), [ { key: "table", label: TabLocalFiles.t("code.csvViewTable"), icon: "table", pressed: true }, { key: "raw", label: TabLocalFiles.t("code.csvViewRaw"), icon: "code" } ]), TabLocalFiles.toolbarCustom("code.csv-meta", renderCsvMeta(extension, rawText)) ]; }, bind: ({ viewState, refresh }) => { ["table", "raw"].forEach((view) => { TabLocalFiles.onToolbarAction(`code.csv-view.${view}`, () => { viewState.csvView = view; TabLocalFiles.syncSegmentedToolbar("code.csv-view", view); refresh(); }); }); }, afterPreviewRender: (extension, value) => { bindDelimitedVirtualTable(extension, value); syncCsvMeta(extension, value); } }; const logSubRenderer = { id: "log", matches: (extension) => extension === ".log", renderPreview: (value, extension, _fileName, viewState) => { const withinByteBudget = value.length <= 1024 * 1024 && new TextEncoder().encode(value).byteLength <= 1024 * 1024; const sourceLines = withinByteBudget ? value.split(/\r\n|\r|\n/) : undefined; const canMapLines = sourceLines && sourceLines.length <= 20000; const lineMap = canMapLines ? sourceLines.flatMap((text, index) => !viewState?.errorOnly || LOG_ERROR_LINE_PATTERN.test(text) ? [index + 1] : []) : undefined; if (!viewState?.errorOnly) { return renderHighlightedCode(value, extension, viewState, false, lineMap); } const filtered = (sourceLines || value.split(/\r\n|\r|\n/)) .filter((text) => LOG_ERROR_LINE_PATTERN.test(text)).join("\n"); const note = `

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.errorLinesMatched", { count: filtered ? TabLocalFiles.lineCount(filtered) : 0 }))}

    `; return `${note}${renderHighlightedCode(filtered || TabLocalFiles.t("code.noErrorLines"), extension, viewState, false, lineMap?.length ? lineMap : undefined)}`; }, contributeToolbar: (toolbar, { extension, formatAction }) => { contributePlainSourceToolbar(toolbar, extension, formatAction); toolbar.contextPreview = [ ...(toolbar.contextPreview || []), TabLocalFiles.contextDivider("code.log-divider"), TabLocalFiles.toolbarAction("code.error-only", TabLocalFiles.t("code.errorLinesOnly"), "filter", { pressed: false, showLabel: true }) ]; }, bind: ({ viewState, refresh }) => { TabLocalFiles.onToolbarAction("code.error-only", () => { viewState.errorOnly = !viewState.errorOnly; document.querySelectorAll('[data-tlf-toolbar-action="code.error-only"]').forEach((button) => { button.setAttribute("aria-pressed", String(viewState.errorOnly)); }); refresh(); }); } }; function sourceLanguageLabel(extension) { return LANGUAGE_BY_EXTENSION[extension] || "Text"; } CodeText.sourceLanguageLabel = sourceLanguageLabel; function render(context) { const extension = TabLocalFiles.extensionFromFileName(context.fileName); const language = sourceLanguageLabel(extension); if (TabLocalFiles.LargeFile.isPreview(context)) { return { title: context.fileName, bodyHtml: TabLocalFiles.LargeFile.renderTextPreview(context, language, context.rawText), contentClassName: "tlf-code-text tlf-large-file-document", toolbar: TabLocalFiles.rawPreviewToolbar(TabLocalFiles.LargeFile.toolbarStatus(context, language)), afterMount: () => TabLocalFiles.LargeTextViewer.bind(context) }; } const intercepted = codeSubRendererFor(extension)?.interceptRender?.(context); if (intercepted) { return intercepted; } const formatAction = createFormatAction(context.rawText, extension); const viewState = { lineNumbers: TabLocalFiles.Preferences.showLineNumbers(), wordWrap: TabLocalFiles.Preferences.useWordWrap(), errorOnly: false, highlightLanguage: highlightLanguageForExtension(extension), jsonStyle: "original", jsonCollapsed: new Set(), csvView: "table", jsonlView: "table", dataView: "raw", subtitleView: "timeline", subtitleEncoding: CodeText.isSubtitleExtension(extension) ? TabLocalFiles.lastDetectedTextEncoding() : undefined, subtitleTranscodable: CodeText.isSubtitleExtension(extension) && TabLocalFiles.isEditableLocalFileSourceUrl(context.sourceUrl), subtitleQuery: "" }; return { title: TabLocalFiles.DocumentTitle.resolve(markdownSourceTitle(extension, context.rawText), context.fileName), bodyHtml: renderCodePreview(context, extension, viewState), contentClassName: "tlf-code-text", toolbar: codeTextToolbarContribution(extension, formatAction, context.rawText, viewState), afterMount: () => { applyDocumentTitle(context, extension, context.rawText); TabLocalFiles.TextEditor.bind({ rootId: CodeText.EDITOR_ROOT_ID, fileName: context.fileName, mimeType: TabLocalFiles.mimeTypeFromExtension(extension), initialText: context.rawText, sourcePath: TabLocalFiles.isEditableLocalFileSourceUrl(context.sourceUrl) ? context.decodedPath : undefined, mountId: context.mountId, saveMode: "original", autoSave: !CodeText.isSubtitleExtension(extension), requireMtime: CodeText.isSubtitleExtension(extension), highlightLanguage: editHighlightLanguageForExtension(extension), renderPreview: (value) => renderTextPreview(value, extension, context.fileName, viewState), afterPreviewRender: (value) => { if (!isDelimitedTextExtension(extension)) { applySyntaxHighlight(extension, viewState); } codeSubRendererFor(extension)?.afterPreviewRender?.(extension, value, viewState); applyDocumentTitle(context, extension, value); } }); bindFormatAction(extension); bindCodeViewControls(context, extension, viewState); if (TabLocalFiles.isEditableLocalFileSourceUrl(context.sourceUrl)) { TabLocalFiles.LiveSync.bind({ rootId: CodeText.EDITOR_ROOT_ID, sourcePath: context.decodedPath, mountId: context.mountId }); } activeEditorChrome = { mountId: context.mountId, viewState }; applyEditorChrome(viewState); TabLocalFiles.CodeTextSearch.bind({ rootId: CodeText.EDITOR_ROOT_ID, mountId: context.mountId, previewSearch: codeSubRendererFor(extension)?.previewSearch?.(viewState) }); if (isProseTextExtension(extension)) { TabLocalFiles.DocumentTranslation.bind(context, { syntax: "text", editorRootId: CodeText.EDITOR_ROOT_ID, extension }); } void refreshSourceFromFile(context.rawText, context.fileName, extension, context.sourceUrl, context.mountId); } }; } CodeText.render = render; function markdownSourceTitle(extension, source) { return MARKDOWN_SOURCE_EXTENSIONS.has(extension) ? TabLocalFiles.DocumentTitle.fromMarkdownSource(source) : undefined; } function applyDocumentTitle(context, extension, source) { if (MARKDOWN_SOURCE_EXTENSIONS.has(extension)) { TabLocalFiles.DocumentTitle.apply(context, markdownSourceTitle(extension, source), context.fileName); } } function renderCodePreview(context, extension, viewState) { return TabLocalFiles.TextEditor.render({ rootId: CodeText.EDITOR_ROOT_ID, shellClassName: "tlf-code-text-shell", initialText: context.rawText, previewHtml: renderTextPreview(context.rawText, extension, context.fileName, viewState), textareaLabel: isDelimitedTextExtension(extension) ? TabLocalFiles.t("code.delimitedContentLabel") : TabLocalFiles.t("code.contentLabel"), layout: "single", showLineNumbers: true, highlightLanguage: editHighlightLanguageForExtension(extension) }); } function editHighlightLanguageForExtension(extension) { if (isDelimitedTextExtension(extension)) { return undefined; } const language = highlightLanguageForExtension(extension); return language === "plain" ? undefined : language; } function renderJsonStatusBadge() { return ` `; } function renderJsonFormatMenu() { const options = [ { action: "code.json-format.pretty-2", label: TabLocalFiles.t("code.jsonFormatPretty2"), icon: "braces" }, { action: "code.json-format.pretty-4", label: TabLocalFiles.t("code.jsonFormatPretty4"), icon: "braces" }, { action: "code.json-format.compact", label: TabLocalFiles.t("code.jsonFormatCompact"), icon: "arrow-down-to-line" } ]; const label = TabLocalFiles.t("code.formatJson"); const items = options .map((option) => ` `) .join(""); return `
    ${TabLocalFiles.renderLucideIcon("braces")} ${TabLocalFiles.escapeHtml(label)} ${TabLocalFiles.renderLucideIcon("chevron-down")}
    `; } function codeTextToolbarContribution(extension, formatAction, rawText, viewState) { const toolbar = TabLocalFiles.editableTextToolbar(); const entry = codeSubRendererFor(extension); if (entry?.contributeToolbar) { entry.contributeToolbar(toolbar, { extension, rawText, viewState, formatAction }); } else { contributePlainSourceToolbar(toolbar, extension, formatAction); } if (isProseTextExtension(extension)) { toolbar.exportItems = [...(toolbar.exportItems || []), TabLocalFiles.DocumentTranslation.exportItem()]; } return toolbar; } function isProseTextExtension(extension) { return extension === ".txt" || extension === ".text"; } CodeText.isProseTextExtension = isProseTextExtension; function contributePlainSourceToolbar(toolbar, extension, formatAction) { toolbar.contextPreview = [ TabLocalFiles.toolbarCustom("code.highlight-language", renderHighlightLanguageSelect(extension)) ]; const editControls = []; if (formatAction) { editControls.push(TabLocalFiles.toolbarAction("code.format", formatAction.buttonText, "settings", { elementId: FORMAT_BUTTON_ID, showLabel: true })); } toolbar.contextEdit = editControls; } function renderHighlightLanguageSelect(extension) { const current = highlightLanguageForExtension(extension); const label = TabLocalFiles.t("code.highlightLanguage"); const options = HIGHLIGHT_LANGUAGE_OPTIONS.map((option) => ``).join(""); return ` `; } function bindHighlightLanguageSelect(viewState, refresh) { const select = document.getElementById(HIGHLIGHT_SELECT_ID); if (!select) { return; } select.addEventListener("change", () => { viewState.highlightLanguage = select.value || "plain"; refresh(); }); } function applyEditorChrome(viewState) { const root = document.getElementById(CodeText.EDITOR_ROOT_ID); if (!root) { return; } root.dataset.tlfHideLineNumbers = String(!viewState.lineNumbers); const textarea = root.querySelector("[data-tlf-editor-textarea]"); if (textarea) { textarea.wrap = viewState.wordWrap ? "soft" : "off"; textarea.classList.toggle("tlf-editor-textarea-nowrap", !viewState.wordWrap); } root .querySelector("[data-tlf-editor-code-stack]") ?.classList.toggle("tlf-editor-stack-nowrap", !viewState.wordWrap); } let activeEditorChrome; TabLocalFiles.Preferences.onChange(() => { const active = activeEditorChrome; if (!active || !TabLocalFiles.isCurrentDocumentMount(active.mountId)) { return; } active.viewState.lineNumbers = TabLocalFiles.Preferences.showLineNumbers(); active.viewState.wordWrap = TabLocalFiles.Preferences.useWordWrap(); applyEditorChrome(active.viewState); TabLocalFiles.TextEditor.refreshPreview(CodeText.EDITOR_ROOT_ID); }); function bindCodeViewControls(context, extension, viewState) { const refresh = () => { TabLocalFiles.TextEditor.refreshPreview(CodeText.EDITOR_ROOT_ID); }; bindHighlightLanguageSelect(viewState, refresh); codeSubRendererFor(extension)?.bind?.({ context, extension, viewState, refresh }); } function bindJsonCopyAll(viewState) { TabLocalFiles.onToolbarAction("copy-all", () => { const text = TabLocalFiles.TextEditor.getText(CodeText.EDITOR_ROOT_ID) || ""; const editing = TabLocalFiles.isDocumentEditing(); const payload = editing ? text : jsonCopyText(text, viewState.jsonStyle); void TabLocalFiles.copyPlainTextToClipboard(payload) .then(() => { TabLocalFiles.showToast(TabLocalFiles.t("common.copiedToClipboard"), "success"); }) .catch(() => { TabLocalFiles.showToast(TabLocalFiles.t("common.copyFailed"), "error"); }); }); } function jsonViewHasLineNumbers(style) { return style === "original" || style === "pretty"; } function bindJsonFoldControls(viewState) { const root = document.getElementById(CodeText.EDITOR_ROOT_ID); if (!root) { return; } root.addEventListener("click", (event) => { const button = event.target?.closest("[data-tlf-json-fold]"); if (!button || !root.contains(button)) { return; } const path = button.dataset.tlfJsonFold || ""; if (viewState.jsonCollapsed.has(path)) { viewState.jsonCollapsed.delete(path); } else { viewState.jsonCollapsed.add(path); } syncJsonFoldState(root, viewState.jsonCollapsed); }); } CodeText.bindJsonFoldControls = bindJsonFoldControls; function syncJsonFoldState(root, collapsed) { root.querySelectorAll("[data-tlf-json-fold]").forEach((button) => { button.setAttribute("aria-expanded", String(!collapsed.has(button.dataset.tlfJsonFold || ""))); }); root.querySelectorAll("[data-tlf-json-row]").forEach((row) => { const path = row.dataset.path; if (path) { row.dataset.collapsed = String(collapsed.has(path)); } row.hidden = !isJsonPathVisible(row.dataset.owner || "", collapsed); }); root.querySelectorAll("[data-tlf-json-node]").forEach((node) => { const path = node.dataset.tlfJsonNode || ""; const isCollapsed = collapsed.has(path); node.dataset.collapsed = String(isCollapsed); node.setAttribute("aria-expanded", String(!isCollapsed)); }); } function isJsonPathVisible(owner, collapsed) { if (!owner) { return true; } const parts = owner.split("/"); for (let index = 1; index <= parts.length; index += 1) { if (collapsed.has(parts.slice(0, index).join("/"))) { return false; } } return true; } function syncJsonStatusBadge(extension, value) { if (extension !== ".json") { return; } const badge = document.getElementById(JSON_STATUS_ID); if (!badge) { return; } const valid = parseJsonSafely(value) !== undefined; const label = TabLocalFiles.t(valid ? "code.jsonValid" : "code.jsonInvalid"); badge.hidden = false; badge.dataset.state = valid ? "valid" : "invalid"; badge.dataset.tlfTooltip = label; badge.innerHTML = `${TabLocalFiles.renderLucideIcon(valid ? "circle-check" : "circle-alert")}${TabLocalFiles.escapeHtml(label)}`; } function renderCsvMeta(extension, value) { const table = parseDelimitedTable(value, delimiterForExtension(extension)); return `${csvDimensionsHtml(table)}`; } function csvDimensionsHtml(table) { return TabLocalFiles.tHtml("code.csvDimensions", { rows: TabLocalFiles.renderDigitSlot(csvCountText(table?.totalRows), CSV_ROW_DIGITS, { name: "csv-rows" }), columns: TabLocalFiles.renderDigitSlot(csvCountText(table?.totalColumns), CSV_COLUMN_DIGITS, { name: "csv-columns" }) }); } function csvCountText(count) { return count === undefined ? "—" : String(count); } function syncCsvMeta(extension, value) { if (!isDelimitedTextExtension(extension)) { return; } const meta = document.getElementById(CSV_META_ID); if (!meta) { return; } const table = parseDelimitedTable(value, delimiterForExtension(extension)); TabLocalFiles.setDigitSlot(meta, "csv-rows", csvCountText(table?.totalRows)); TabLocalFiles.setDigitSlot(meta, "csv-columns", csvCountText(table?.totalColumns)); } function renderHighlightedCode(value, extension, viewState, forceHideLineNumbers = false, logLines) { const codeClassName = `shj-lang-${TabLocalFiles.escapeAttr(previewHighlightLanguage(extension, viewState))}`; const lineNumbers = !forceHideLineNumbers && (!viewState || viewState.lineNumbers) ? `` : ""; const wrapClass = !viewState || viewState.wordWrap ? "" : " tlf-code-nowrap"; const region = extension === ".log" ? logLines ? "log" : "text" : previewHighlightLanguage(extension, viewState) === "plain" ? "text" : "code"; const logMetadata = logLines ? ` data-tlf-log-lines="${logLines.join(",")}"` : ""; return `
    ${lineNumbers}
    ${TabLocalFiles.escapeHtml(value)}
    `; } function lineNumbersFor(value) { const count = Math.max(1, TabLocalFiles.lineCount(value.replace(/\n$/, ""))); return Array.from({ length: count }, (_unused, index) => String(index + 1)).join("\n"); } function renderTextPreview(value, extension, fileName, viewState) { const structured = codeSubRendererFor(extension)?.renderPreview?.(value, extension, fileName, viewState); if (structured !== undefined) { return structured; } return renderHighlightedCode(value, extension, viewState); } function isJsonParseable(value) { return parseJsonSafely(value) !== undefined; } function parseJsonSafely(value) { try { return JSON.parse(value); } catch { return undefined; } } function formatJsonWithIndent(value, indent) { const parsed = parseJsonSafely(value); return parsed === undefined ? undefined : JSON.stringify(parsed, null, indent); } function compactJson(value) { const parsed = parseJsonSafely(value); return parsed === undefined ? undefined : JSON.stringify(parsed); } function jsonViewNeedsValidJson(style) { return style === "pretty" || style === "tree"; } CodeText.jsonViewNeedsValidJson = jsonViewNeedsValidJson; function jsonCopyText(value, style) { if (style === "compact") { return compactJson(value) ?? value; } if (style === "pretty" || style === "tree") { return formatJsonWithIndent(value, JSON_PREVIEW_INDENT) ?? value; } return value; } CodeText.jsonCopyText = jsonCopyText; function renderJsonStructuredPreview(value, extension, viewState) { if (viewState.jsonStyle === "compact") { return renderHighlightedCode(compactJson(value) ?? value, extension, viewState, true); } const options = { lineNumbers: viewState.lineNumbers, wordWrap: viewState.wordWrap, collapsedPaths: viewState.jsonCollapsed }; if (viewState.jsonStyle === "tree") { return renderJsonTree(value, options); } const pretty = renderJsonPretty(value, options); if (pretty !== undefined) { return pretty; } return undefined; } function renderJsonPretty(value, options = {}) { const parsed = parseJsonSafely(value); if (parsed === undefined) { return undefined; } const rows = []; collectJsonRows(parsed, undefined, "0", "", 0, false, rows); const collapsed = new Set(options.collapsedPaths || []); const foldable = rows.length <= JSON_STRUCTURED_LINE_LIMIT; const showLineNumbers = options.lineNumbers !== false; const body = rows .map((row, index) => renderJsonRow(row, index + 1, collapsed, foldable, showLineNumbers)) .join(""); const note = foldable ? "" : `

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.jsonFoldLimited", { lines: JSON_STRUCTURED_LINE_LIMIT }))}

    `; const wrapClass = options.wordWrap === false ? " tlf-code-nowrap" : ""; const gutterClass = showLineNumbers ? "" : " tlf-json-doc-no-gutter"; return ` ${note}
    ${body}
    `; } CodeText.renderJsonPretty = renderJsonPretty; function renderJsonRow(row, lineNumber, collapsed, foldable, showLineNumbers) { const isCollapsed = Boolean(row.path) && collapsed.has(row.path || ""); const hidden = isJsonPathVisible(row.owner, collapsed) ? "" : " hidden"; const gutter = showLineNumbers ? `` : ""; const fold = row.path && foldable ? `` : ''; const summary = row.path !== undefined && row.closer !== undefined ? `${TabLocalFiles.escapeHtml(` ⋯ ${row.childCount || 0} `)}${TabLocalFiles.escapeHtml(row.closer)}` : ""; const pathAttribute = row.path ? ` data-path="${TabLocalFiles.escapeAttr(row.path)}" data-collapsed="${String(isCollapsed)}"` : ""; return `
    ${gutter}${fold}${JSON_INDENT_UNIT.repeat(row.depth)}${row.html}${summary}
    `; } function collectJsonRows(value, key, path, owner, depth, trailingComma, rows) { const prefix = key === undefined ? "" : `${TabLocalFiles.escapeHtml(JSON.stringify(key))}: `; const comma = trailingComma ? ',' : ""; if (!isJsonContainer(value)) { rows.push({ owner, depth, html: `${prefix}${renderJsonScalar(value)}${comma}` }); return; } const entries = Array.isArray(value) ? value.map((item) => [undefined, item]) : Object.entries(value); const [open, close] = Array.isArray(value) ? ["[", "]"] : ["{", "}"]; if (!entries.length) { rows.push({ owner, depth, html: `${prefix}${open}${close}${comma}` }); return; } rows.push({ owner, path, depth, childCount: entries.length, closer: `${close}${trailingComma ? "," : ""}`, html: `${prefix}${open}` }); entries.forEach(([childKey, childValue], index) => { collectJsonRows(childValue, childKey, `${path}/${index}`, path, depth + 1, index < entries.length - 1, rows); }); rows.push({ owner: path, depth, html: `${close}${comma}` }); } function renderJsonTree(value, options = {}) { const parsed = parseJsonSafely(value); if (parsed === undefined) { return undefined; } return renderParsedValueTree(parsed, options); } CodeText.renderJsonTree = renderJsonTree; function renderParsedValueTree(parsed, options = {}) { const collapsed = new Set(options.collapsedPaths || []); const root = renderJsonTreeItem(parsed, TabLocalFiles.t("code.jsonTreeRoot"), "0", collapsed); return `
      ${root}
    `; } CodeText.renderParsedValueTree = renderParsedValueTree; function renderJsonTreeItem(value, label, path, collapsed) { if (!isJsonContainer(value)) { return renderJsonTreeLeaf(label, renderJsonScalar(value)); } const labelHtml = `${TabLocalFiles.escapeHtml(label)}`; const spacer = ''; const isArray = Array.isArray(value); const entries = isArray ? value.map((item, index) => [String(index), item]) : Object.entries(value); const isCollapsed = collapsed.has(path); const meta = TabLocalFiles.t(isArray ? "code.jsonNodeItems" : "code.jsonNodeKeys", { count: entries.length }); const children = entries .map(([childKey, childValue], index) => renderJsonTreeItem(childValue, childKey, `${path}/${index}`, collapsed)) .join(""); const fold = entries.length ? `` : spacer; return `
  • ${fold} ${labelHtml} ${TabLocalFiles.escapeHtml(isArray ? `[${meta}]` : `{${meta}}`)}
      ${children}
  • `; } CodeText.renderJsonTreeItem = renderJsonTreeItem; function renderJsonTreeLeaf(label, valueHtml) { return `
  • ${TabLocalFiles.escapeHtml(label)}: ${valueHtml}
  • `; } CodeText.renderJsonTreeLeaf = renderJsonTreeLeaf; function isJsonContainer(value) { return Array.isArray(value) || (typeof value === "object" && value !== null); } function renderJsonScalar(value) { if (value === null) { return 'null'; } if (typeof value === "string") { return `${TabLocalFiles.escapeHtml(JSON.stringify(value))}`; } if (typeof value === "number") { return `${TabLocalFiles.escapeHtml(String(value))}`; } if (typeof value === "boolean") { return `${TabLocalFiles.escapeHtml(String(value))}`; } return `${TabLocalFiles.escapeHtml(String(value))}`; } CodeText.renderJsonScalar = renderJsonScalar; const DELIMITED_COLUMN_WIDTH = 160; const DELIMITED_INITIAL_ROWS = 40; let disposeDelimitedVirtualTable; function renderDelimitedTablePreview(value, extension, fileName) { const table = parseDelimitedTable(value, delimiterForExtension(extension)); if (!table) { return renderHighlightedCode(value, extension); } const columns = table.totalColumns; const headerRow = normalizedDelimitedRow(table.rows[0] || [], columns); const bodyCount = table.rows.length - 1; const initial = TabLocalFiles.VirtualTable.renderWindowHtml({ start: 0, end: Math.min(bodyCount, DELIMITED_INITIAL_ROWS) }, bodyCount, columns, (index) => renderDelimitedBodyRow(table.rows[index + 1], columns, index)); return `
    ${``.repeat(columns)} ${headerRow.map((cell, index) => renderDelimitedHeaderCell(cell, index)).join("")} ${initial}
    `; } function bindDelimitedVirtualTable(extension, value) { disposeDelimitedVirtualTable?.(); disposeDelimitedVirtualTable = undefined; const wrap = document.querySelector(".tlf-csv-table-wrap[data-tlf-delimited-rows]"); const body = wrap?.querySelector("tbody"); const table = parseDelimitedTable(value, delimiterForExtension(extension)); if (!wrap || !body || !table) { return; } const columns = table.totalColumns; disposeDelimitedVirtualTable = TabLocalFiles.VirtualTable.bind({ wrap, body, rowCount: table.rows.length - 1, columnCount: columns, renderRow: (index) => renderDelimitedBodyRow(table.rows[index + 1], columns, index) }); } let lastDelimitedParse; function parseDelimitedTable(value, delimiter) { if (lastDelimitedParse && lastDelimitedParse.value === value && lastDelimitedParse.delimiter === delimiter) { return lastDelimitedParse.table; } const table = parseDelimitedTableUncached(value, delimiter); lastDelimitedParse = { value, delimiter, table }; return table; } function parseDelimitedTableUncached(value, delimiter) { const rows = parseDelimitedRows(value, delimiter); const totalRows = rows.length; const totalColumns = rows.reduce((max, row) => Math.max(max, row.length), 0); if (totalRows === 0 || totalColumns === 0) { return undefined; } return { rows, totalRows, totalColumns }; } function parseDelimitedRows(value, delimiter) { if (!value) { return []; } const rows = []; let row = []; let cell = ""; let inQuotes = false; for (let index = 0; index < value.length; index += 1) { const char = value[index]; const next = value[index + 1]; if (inQuotes) { if (char === '"') { if (next === '"') { cell += '"'; index += 1; } else { inQuotes = false; } } else { cell += char; } continue; } if (char === '"' && cell === "") { inQuotes = true; continue; } if (char === delimiter) { row.push(cell); cell = ""; continue; } if (char === "\r" || char === "\n") { row.push(cell); rows.push(row); row = []; cell = ""; if (char === "\r" && next === "\n") { index += 1; } continue; } cell += char; } row.push(cell); if (row.length > 1 || row[0] !== "" || !endsWithLineBreak(value)) { rows.push(row); } return rows; } function normalizedDelimitedRow(row, columns) { const normalized = row.slice(0, columns); while (normalized.length < columns) { normalized.push(""); } return normalized; } function renderDelimitedHeaderCell(cell, index) { const label = cell || TabLocalFiles.t("code.unnamedColumn", { index: index + 1 }); return `${TabLocalFiles.escapeHtml(label)}`; } function renderDelimitedBodyRow(row, columns, index) { const cells = normalizedDelimitedRow(row, columns) .map((cell) => { const text = TabLocalFiles.escapeHtml(cell); return `${text}`; }) .join(""); return `${cells}`; } function delimiterForExtension(extension) { return extension === ".tsv" ? "\t" : ","; } function isDelimitedTextExtension(extension) { return DELIMITED_TEXT_EXTENSIONS.has(extension); } function isDataTreeExtension(extension) { return DATA_TREE_EXTENSIONS.has(extension); } CodeText.isDataTreeExtension = isDataTreeExtension; const DATA_PARSERS_VENDOR_PATH = "vendor/data-file-parsers.js"; let dataParsersModule; function loadDataParsers() { if (!dataParsersModule) { dataParsersModule = import(dataVendorAssetUrl(DATA_PARSERS_VENDOR_PATH)); dataParsersModule.catch(() => { dataParsersModule = undefined; }); } return dataParsersModule; } function dataVendorAssetUrl(path) { const extensionUrl = globalThis.chrome?.runtime?.getURL?.(path); return extensionUrl || new URL(path, document.baseURI).toString(); } async function fillDataTreePanel(extension, value, viewState) { if (!isDataTreeExtension(extension) || viewState.dataView !== "tree") { return; } const panel = document.querySelector("[data-tlf-data-tree-pending]"); if (!panel) { return; } try { const module = await loadDataParsers(); if (!panel.isConnected || viewState.dataView !== "tree") { return; } const parsed = extension === ".toml" ? module.parseToml(value) : module.parseYaml(value); panel.outerHTML = renderParsedValueTree(parsed, { collapsedPaths: viewState.jsonCollapsed }); } catch (error) { if (panel.isConnected) { const message = error instanceof Error ? error.message : String(error); panel.outerHTML = `

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("code.dataTreeParseError"))}

    ${TabLocalFiles.escapeHtml(message)}
    `; } } } function endsWithLineBreak(value) { return value.endsWith("\n") || value.endsWith("\r"); } function bindFormatAction(extension) { const button = document.getElementById(FORMAT_BUTTON_ID); if (!button) { return; } button.addEventListener("click", () => { const currentText = TabLocalFiles.TextEditor.getText(CodeText.EDITOR_ROOT_ID) || ""; const formatAction = createFormatAction(currentText, extension); if (!formatAction) { TabLocalFiles.showToast(TabLocalFiles.t("code.noFormattingNeeded"), "neutral"); return; } TabLocalFiles.TextEditor.setText(CodeText.EDITOR_ROOT_ID, formatAction.text, true); TabLocalFiles.showToast(TabLocalFiles.t("code.formatApplied"), "success"); }); } async function refreshSourceFromFile(initialText, fileName, extension, sourceUrl, mountId) { const source = await TabLocalFiles.readCurrentFileAsText(sourceUrl); const mountChanged = mountId !== undefined && !TabLocalFiles.isCurrentDocumentMount(mountId); if (mountChanged || source === undefined || source === initialText || TabLocalFiles.TextEditor.isDirty(CodeText.EDITOR_ROOT_ID)) { return; } TabLocalFiles.TextEditor.setText(CodeText.EDITOR_ROOT_ID, source); } function applySyntaxHighlight(extension, viewState) { const code = document.getElementById(CODE_CONTENT_ID); const language = previewHighlightLanguage(extension, viewState); if (code && window.speedHighlight && language !== "plain") { void window.speedHighlight.highlightElement(code, language); } } function previewHighlightLanguage(extension, viewState) { return viewState?.highlightLanguage || highlightLanguageForExtension(extension); } function highlightLanguageForExtension(extension) { return HIGHLIGHT_LANGUAGE_BY_EXTENSION[extension] || "plain"; } function formatJson(rawText) { try { return JSON.stringify(JSON.parse(rawText), null, 2); } catch { return undefined; } } function createFormatAction(rawText, extension) { if (extension === ".json") { return createChangedFormatAction(rawText, formatJson(rawText), TabLocalFiles.t("code.formatJson")); } if (!isLikelyMinified(rawText)) { return undefined; } if (BRACE_EXPAND_EXTENSIONS.has(extension)) { return createChangedFormatAction(rawText, expandBraceCode(rawText), TabLocalFiles.t("code.expandCode")); } if (MARKUP_EXPAND_EXTENSIONS.has(extension)) { return createChangedFormatAction(rawText, expandMarkup(rawText), TabLocalFiles.t("code.expandCode")); } return undefined; } function createChangedFormatAction(rawText, formattedText, buttonText) { if (!formattedText || formattedText.trim() === rawText.trim()) { return undefined; } return { text: formattedText, buttonText }; } function isLikelyMinified(value) { const lines = value.split(/\r\n|\r|\n/); const longestLine = lines.reduce((max, line) => Math.max(max, line.length), 0); const averageLineLength = value.length / Math.max(lines.length, 1); return value.length > 200 && (longestLine > 240 || averageLineLength > 160); } function expandBraceCode(value) { let result = ""; let indentLevel = 0; let quote; let escaped = false; let lineComment = false; let blockComment = false; for (let index = 0; index < value.length; index += 1) { const char = value[index]; const next = value[index + 1]; if (lineComment) { result += char; if (char === "\n") { lineComment = false; result += indentation(indentLevel); } continue; } if (blockComment) { result += char; if (char === "*" && next === "/") { result += next; index += 1; blockComment = false; } continue; } if (quote) { const wasEscaped = escaped; result += char; if (char === quote && !wasEscaped) { quote = undefined; } escaped = char === "\\" && !wasEscaped; if (char !== "\\" && wasEscaped) { escaped = false; } continue; } if ((char === '"' || char === "'" || char === "`")) { quote = char; result += char; continue; } if (char === "/" && next === "/") { lineComment = true; result += char; continue; } if (char === "/" && next === "*") { blockComment = true; result += char; continue; } if (char === "{") { indentLevel += 1; result = `${trimTrailingSpaces(result)} {\n${indentation(indentLevel)}`; continue; } if (char === "}") { indentLevel = Math.max(indentLevel - 1, 0); result = `${trimTrailingSpaces(result)}\n${indentation(indentLevel)}}`; if (next && next !== ";" && next !== "," && next !== ")" && next !== "]") { result += `\n${indentation(indentLevel)}`; } continue; } if (char === ";") { result = `${trimTrailingSpaces(result)};\n${indentation(indentLevel)}`; continue; } result += char; } return cleanupExpandedText(result); } function expandMarkup(value) { const tokens = value.replace(/>\s*\n<").split("\n"); let indentLevel = 0; return cleanupExpandedText(tokens .map((token) => { const trimmed = token.trim(); if (!trimmed) { return ""; } if (/^<\//.test(trimmed)) { indentLevel = Math.max(indentLevel - 1, 0); } const line = `${indentation(indentLevel)}${trimmed}`; if (/^<[^!?/][^>]*[^/]>\s*$/.test(trimmed) && !/^<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)\b/i.test(trimmed)) { indentLevel += 1; } return line; }) .join("\n")); } function cleanupExpandedText(value) { return value .split(/\r\n|\r|\n/) .map((line) => line.trimEnd()) .filter((line, index, lines) => line || (lines[index - 1] && lines[index + 1])) .join("\n") .trim(); } function indentation(level) { return INDENT.repeat(level); } function trimTrailingSpaces(value) { return value.replace(/[ \t]+$/g, ""); } })(CodeText = TabLocalFiles.CodeText || (TabLocalFiles.CodeText = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var ZipFile; (function (ZipFile) { const STATUS_ID = "tlf-zip-status"; const SHELL_ID = "tlf-zip-shell"; const REFRESH_ACTION = "zip.refresh"; ZipFile.blockedEntryDefinition = { id: "zip-entry-blocked", displayName: "Archive entry", extensions: [], enabled: true, renderer: renderBlockedEntry }; function matches(decodedPath) { if (TabLocalFiles.ZipArchive.isArchivePath(decodedPath)) { return true; } const split = TabLocalFiles.ZipArchive.splitVirtualPath(decodedPath); return Boolean(split) && (!split.entryPath || split.entryPath.endsWith("/")); } ZipFile.matches = matches; async function resolveDefinition(definition, decodedPath) { const split = TabLocalFiles.ZipArchive.splitVirtualPath(decodedPath); if (!split || !split.entryPath || split.entryPath.endsWith("/")) { return definition; } const reason = await blockedEntryReason(split.archivePath, split.entryPath); return reason ? ZipFile.blockedEntryDefinition : definition; } ZipFile.resolveDefinition = resolveDefinition; async function blockedEntryReason(archivePath, entryPath) { const archive = await TabLocalFiles.loadArchiveForPath(archivePath); if (!archive.directory) { return undefined; } const entry = TabLocalFiles.ZipArchive.findEntry(archive.directory, entryPath); if (!entry) { return "not-found"; } if (entry.encrypted) { return "encrypted"; } if (!entry.supported) { return "unsupported-method"; } if (entry.uncompressedSize > TabLocalFiles.ZipArchive.MAX_ENTRY_BYTES) { return "too-large"; } return undefined; } function render(context) { return { title: context.fileName, bodyHtml: renderLoadingShell(), contentClassName: "tlf-zip", toolbar: { status: [TabLocalFiles.toolbarStatus(STATUS_ID, TabLocalFiles.t("zip.loading"))], contextPreview: [ TabLocalFiles.toolbarAction(REFRESH_ACTION, TabLocalFiles.t("zip.refresh"), "refresh-cw", { showLabel: true }) ], editable: false, saveBadge: false }, afterMount: () => { TabLocalFiles.onToolbarAction(REFRESH_ACTION, () => { TabLocalFiles.clearArchiveCache(); void mountOverview(context); }); void mountOverview(context); } }; } ZipFile.render = render; function renderBlockedEntry(context) { return { title: context.fileName, bodyHtml: renderLoadingShell(), contentClassName: "tlf-zip", toolbar: TabLocalFiles.filePreviewToolbar([TabLocalFiles.toolbarStatus(STATUS_ID, TabLocalFiles.t("zip.loading"))]), afterMount: () => { void mountBlockedEntry(context); } }; } function renderLoadingShell() { return `

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("zip.loading"))}

    `; } async function mountOverview(context) { const split = TabLocalFiles.ZipArchive.splitVirtualPath(context.decodedPath); const archivePath = split?.archivePath || context.decodedPath.replace(/\/+$/, ""); const folderPath = split?.entryPath || ""; const archive = await TabLocalFiles.loadArchiveForPath(archivePath); if (!TabLocalFiles.isCurrentDocumentMount(context.mountId)) { return; } if (!archive.directory) { renderShell(context, renderArchiveError(context, archive.error), archiveErrorMessage(archive.error)); return; } if (archive.byteLength) { TabLocalFiles.updateFileHeaderStatMetadata({ size: archive.byteLength }); } const children = TabLocalFiles.ZipArchive.childEntries(archive.directory, folderPath); renderShell(context, renderOverview(context, archive, children, folderPath), TabLocalFiles.t("zip.ready")); } async function mountBlockedEntry(context) { const split = TabLocalFiles.ZipArchive.splitVirtualPath(context.decodedPath); if (!split) { return; } const reason = await blockedEntryReason(split.archivePath, split.entryPath); if (!TabLocalFiles.isCurrentDocumentMount(context.mountId)) { return; } const message = entryErrorMessage(reason); renderShell(context, `
    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("zip.entryKicker"))}

    ${TabLocalFiles.escapeHtml(context.fileName)}

    ${TabLocalFiles.escapeHtml(message)}

    ${renderDetailRow(TabLocalFiles.t("zip.archive"), TabLocalFiles.fileNameFromPath(split.archivePath))} ${renderDetailRow(TabLocalFiles.t("zip.entryPath"), split.entryPath)}
    ${renderRevealAction(context)} `, message); } function renderOverview(context, archive, children, folderPath) { const directory = archive.directory; const fileEntries = directory.entries.filter((entry) => !entry.isDirectory); const encryptedCount = fileEntries.filter((entry) => entry.encrypted).length; const ratio = directory.totalUncompressedSize > 0 ? `${Math.round((directory.totalCompressedSize / directory.totalUncompressedSize) * 100)}%` : TabLocalFiles.t("common.unknown"); const warnings = [ directory.skippedUnsafeCount ? renderWarning(TabLocalFiles.t("zip.skippedUnsafe", { count: directory.skippedUnsafeCount })) : "", directory.truncated ? renderWarning(TabLocalFiles.t("zip.truncated", { count: TabLocalFiles.ZipArchive.MAX_ENTRY_COUNT })) : "", encryptedCount ? renderWarning(TabLocalFiles.t("zip.encryptedEntries", { count: encryptedCount })) : "" ].join(""); return `
    ${TabLocalFiles.escapeHtml(folderPath ? TabLocalFiles.t("zip.folderKicker") : TabLocalFiles.t("zip.kicker"))}

    ${TabLocalFiles.escapeHtml(context.fileName)}

    ${TabLocalFiles.escapeHtml(children.length ? TabLocalFiles.t("zip.description") : TabLocalFiles.t("zip.emptyArchive"))}

    ${renderDetailRow(TabLocalFiles.t("zip.entries"), TabLocalFiles.formatCount(fileEntries.length, "entry"))} ${renderDetailRow(TabLocalFiles.t("zip.childEntries"), TabLocalFiles.formatCount(children.length, "entry"))} ${renderDetailRow(TabLocalFiles.t("zip.uncompressedSize"), TabLocalFiles.formatBytes(directory.totalUncompressedSize))} ${renderDetailRow(TabLocalFiles.t("zip.compressedSize"), TabLocalFiles.formatBytes(directory.totalCompressedSize))} ${renderDetailRow(TabLocalFiles.t("zip.ratio"), ratio)} ${archive.byteLength ? renderDetailRow(TabLocalFiles.t("zip.archiveSize"), TabLocalFiles.formatBytes(archive.byteLength)) : ""}
    ${warnings} ${renderRevealAction(context)} `; } function renderArchiveError(context, error) { return `
    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("zip.kicker"))}

    ${TabLocalFiles.escapeHtml(context.fileName)}

    ${TabLocalFiles.escapeHtml(archiveErrorMessage(error))}

    ${renderRevealAction(context)} `; } function renderRevealAction(context) { const canReveal = TabLocalFiles.isLocalFileSourceUrl(context.sourceUrl); const label = canReveal ? TabLocalFiles.revealInFileManagerLabel() : TabLocalFiles.t("common.revealUnavailableRemote"); return `
    `; } function renderWarning(message) { return `

    ${TabLocalFiles.escapeHtml(message)}

    `; } function renderDetailRow(label, value) { return `
    ${TabLocalFiles.escapeHtml(label)}
    ${TabLocalFiles.escapeHtml(value)}
    `; } function renderShell(context, bodyHtml, status) { const shell = document.getElementById(SHELL_ID); if (shell) { shell.innerHTML = bodyHtml; bindActions(context); } const statusElement = document.getElementById(STATUS_ID); if (statusElement) { statusElement.textContent = status; } } function bindActions(context) { document.querySelectorAll("[data-tlf-zip-action]").forEach((button) => { if (button.dataset.tlfZipBound === "true") { return; } button.dataset.tlfZipBound = "true"; button.addEventListener("click", () => { if (button.dataset.tlfZipAction === "reveal") { void TabLocalFiles.openPathInFileManager(context.decodedPath).then((ok) => { TabLocalFiles.showToast(ok ? TabLocalFiles.t("unsupported.revealRequested") : TabLocalFiles.t("unsupported.revealFailed"), ok ? "success" : "error"); }); } }); }); } function archiveErrorMessage(error) { if (error === "too-large") { return TabLocalFiles.t("zip.tooLarge", { size: TabLocalFiles.formatBytes(TabLocalFiles.LargeFile.ARCHIVE_FULL_BYTES) }); } if (error === "corrupt") { return TabLocalFiles.t("zip.corrupt"); } return TabLocalFiles.t("zip.readFailed"); } function entryErrorMessage(error) { const messages = { encrypted: TabLocalFiles.t("zip.entryEncrypted"), "unsupported-method": TabLocalFiles.t("zip.entryUnsupportedMethod"), "too-large": TabLocalFiles.t("zip.entryTooLarge"), ratio: TabLocalFiles.t("zip.entryRatio"), corrupt: TabLocalFiles.t("zip.entryCorrupt"), "not-found": TabLocalFiles.t("zip.entryNotFound") }; return (error && messages[error]) || TabLocalFiles.t("zip.entryCorrupt"); } })(ZipFile = TabLocalFiles.ZipFile || (TabLocalFiles.ZipFile = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var UnsupportedFile; (function (UnsupportedFile) { const STATUS_ID = "tlf-unsupported-status"; UnsupportedFile.definition = { id: "unsupported", displayName: "Unsupported file", extensions: [], enabled: true, renderer: render }; function render(context) { return { title: context.fileName, bodyHtml: renderUnsupportedFile(context), contentClassName: "tlf-unsupported-file", toolbar: TabLocalFiles.filePreviewToolbar(), afterMount: () => { bindUnsupportedFile(context); void loadFileStat(context.decodedPath); } }; } UnsupportedFile.render = render; function renderUnsupportedFile(context) { const extension = TabLocalFiles.extensionFromFileName(context.fileName).replace(".", "").toUpperCase() || TabLocalFiles.t("common.unknown"); const canReveal = TabLocalFiles.isLocalFileSourceUrl(context.sourceUrl); const revealLabel = canReveal ? TabLocalFiles.revealInFileManagerLabel() : TabLocalFiles.t("common.revealUnavailableRemote"); const revealDisabled = canReveal ? "" : " disabled"; return `

    ${TabLocalFiles.escapeHtml(context.fileName)}

    ${TabLocalFiles.escapeHtml(TabLocalFiles.t("unsupported.description"))}

    ${renderDetailRow(TabLocalFiles.t("unsupported.type"), extension)} ${renderDetailRow(TabLocalFiles.t("unsupported.path"), context.decodedPath)} ${renderDetailRow(TabLocalFiles.t("unsupported.size"), TabLocalFiles.t("unsupported.reading"), "size")} ${renderDetailRow(TabLocalFiles.t("unsupported.created"), TabLocalFiles.t("unsupported.reading"), "created")} ${renderDetailRow(TabLocalFiles.t("unsupported.modified"), TabLocalFiles.t("unsupported.reading"), "modified")}

    `; } function renderDetailRow(label, value, field) { return `
    ${TabLocalFiles.escapeHtml(label)}
    ${TabLocalFiles.escapeHtml(value)}
    `; } function bindUnsupportedFile(context) { document.querySelectorAll("[data-tlf-unsupported-action]").forEach((button) => { button.addEventListener("click", () => { const action = button.dataset.tlfUnsupportedAction; if (action === "reveal") { if (!TabLocalFiles.isLocalFileSourceUrl(context.sourceUrl)) { updateStatus(TabLocalFiles.t("common.revealUnavailableRemote")); return; } void TabLocalFiles.openPathInFileManager(context.decodedPath).then((ok) => updateStatus(ok ? TabLocalFiles.t("unsupported.revealRequested") : TabLocalFiles.t("unsupported.revealFailed"))); return; } }); }); } async function loadFileStat(path) { const stat = await TabLocalFiles.statOriginalFile(path); if (!stat) { updateField("size", TabLocalFiles.t("unsupported.unreadable")); updateField("created", TabLocalFiles.t("unsupported.unreadable")); updateField("modified", TabLocalFiles.t("unsupported.unreadable")); return; } updateField("size", TabLocalFiles.formatBytes(stat.size)); updateField("created", TabLocalFiles.formatFileTimestamp(stat.birthtime)); updateField("modified", TabLocalFiles.formatFileTimestamp(stat.mtime)); } function updateField(field, value) { const target = document.querySelector(`[data-tlf-unsupported-field="${field}"]`); if (target) { const textTarget = target.querySelector(".tlf-unsupported-detail-text") || target; textTarget.textContent = value; } } function updateStatus(value) { const status = document.getElementById(STATUS_ID); if (status) { status.textContent = value; } } })(UnsupportedFile = TabLocalFiles.UnsupportedFile || (TabLocalFiles.UnsupportedFile = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { const FALLBACK_FILE_TYPE_CATALOG = [ { id: "markdown", labelKey: "settings.type.markdown", extensions: [".md", ".markdown"], defaultOpenMode: "previewer", chromeNativeRenderable: true, osAssociation: "associate" }, { id: "svg", labelKey: "settings.type.svg", extensions: [".svg"], defaultOpenMode: "previewer", chromeNativeRenderable: true, osAssociation: "associate" }, { id: "pdf", labelKey: "settings.type.pdf", extensions: [".pdf"], defaultOpenMode: "native", chromeNativeRenderable: true, osAssociation: "associate" }, { id: "office", labelKey: "settings.type.office", extensions: [".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx"], defaultOpenMode: "previewer", chromeNativeRenderable: false, osAssociation: "none" }, { id: "html", labelKey: "settings.type.html", extensions: [".html", ".htm", ".xhtml", ".xht", ".xhtm"], defaultOpenMode: "native", chromeNativeRenderable: true, osAssociation: "associate", osAssociationOverrides: { ".xhtml": "none", ".xht": "none", ".xhtm": "none" } }, { id: "subtitle", labelKey: "settings.type.subtitle", extensions: [".srt", ".vtt", ".ass", ".ssa", ".ttml", ".dfxp", ".smi", ".sbv", ".sub", ".mpl", ".lrc", ".idx"], defaultOpenMode: "previewer", chromeNativeRenderable: true, chromeNativeRenderableOverrides: { ".sub": false }, osAssociation: "none" }, { id: "code-text", labelKey: "settings.type.codeText", extensions: [ ".txt", ".text", ".csv", ".tsv", ".rtf", ".tex", ".bib", ".mdx", ".js", ".cjs", ".mjs", ".ts", ".jsx", ".tsx", ".vue", ".py", ".rb", ".java", ".swift", ".go", ".rs", ".cpp", ".cc", ".cxx", ".c", ".h", ".hh", ".hpp", ".hxx", ".css", ".less", ".scss", ".json", ".jsonc", ".jsonl", ".ndjson", ".xml", ".yaml", ".yml", ".toml", ".ini", ".sh", ".sql", ".log" ], defaultOpenMode: "previewer", chromeNativeRenderable: true, osAssociation: "associate", osAssociationOverrides: { ".text": "none", ".rtf": "none", ".mdx": "none", ".cjs": "none", ".mjs": "none", ".vue": "none", ".rb": "none", ".swift": "none", ".cc": "none", ".cxx": "none", ".hh": "none", ".hxx": "none", ".less": "none", ".scss": "none", ".jsonc": "none", ".jsonl": "none", ".ndjson": "none", ".toml": "none", ".ini": "none" } }, { id: "zip", labelKey: "settings.type.archive", extensions: [ ".zip", ".jar", ".apk", ".crx", ".epub", ".skill", ".xpi", ".vsix", ".whl", ".nupkg", ".odt", ".ods", ".odp" ], defaultOpenMode: "previewer", chromeNativeRenderable: false, osAssociation: "none" }, { id: "image", labelKey: "settings.type.image", extensions: [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".avif", ".ico", ".apng"], defaultOpenMode: "previewer", chromeNativeRenderable: true, osAssociation: "associate", osAssociationOverrides: { ".avif": "none", ".ico": "none", ".apng": "none" } }, { id: "media", labelKey: "settings.type.media", extensions: [ ".webm", ".mp4", ".m4v", ".mkv", ".ogv", ".ogm", ".mp3", ".wav", ".flac", ".ogg", ".oga", ".opus", ".weba", ".m4a", ".mka" ], defaultOpenMode: "native", chromeNativeRenderable: true, osAssociation: "none" } ]; function manifestFileTypeCatalog() { return globalThis.chrome?.runtime?.getManifest?.()?.tab_local_file_preview?.fileTypeCatalog; } TabLocalFiles.FILE_TYPE_CATALOG = manifestFileTypeCatalog() ?? FALLBACK_FILE_TYPE_CATALOG; function extensionsForId(id) { return TabLocalFiles.FILE_TYPE_CATALOG.find((entry) => entry.id === id)?.extensions ?? []; } TabLocalFiles.MARKDOWN_EXTENSIONS = extensionsForId("markdown"); TabLocalFiles.IMAGE_EXTENSIONS = [...extensionsForId("image"), ...extensionsForId("svg")]; TabLocalFiles.MEDIA_EXTENSIONS = extensionsForId("media"); TabLocalFiles.HTML_EXTENSIONS = [...extensionsForId("html"), ".text_html"]; TabLocalFiles.OFFICE_EXTENSIONS = extensionsForId("office"); TabLocalFiles.SUBTITLE_EXTENSIONS = extensionsForId("subtitle"); TabLocalFiles.CODE_TEXT_EXTENSIONS = extensionsForId("code-text"); TabLocalFiles.ARCHIVE_CATALOG_EXTENSIONS = extensionsForId("zip"); TabLocalFiles.SIDEBAR_ONLY_EXTENSIONS = TabLocalFiles.ARCHIVE_CATALOG_EXTENSIONS.filter((extension) => extension !== ".zip" && extension !== ".skill"); function catalogEntryForExtension(extension) { const normalized = extension.toLowerCase(); return TabLocalFiles.FILE_TYPE_CATALOG.find((entry) => entry.extensions.includes(normalized)); } TabLocalFiles.catalogEntryForExtension = catalogEntryForExtension; function catalogDefaultOpenMode(extension) { return catalogEntryForExtension(extension)?.defaultOpenMode; } TabLocalFiles.catalogDefaultOpenMode = catalogDefaultOpenMode; function catalogChromeNativeRenderable(extension) { const normalized = extension.toLowerCase(); const entry = catalogEntryForExtension(normalized); return entry?.chromeNativeRenderableOverrides?.[normalized] ?? entry?.chromeNativeRenderable; } TabLocalFiles.catalogChromeNativeRenderable = catalogChromeNativeRenderable; function catalogOsAssociation(extension) { const normalized = extension.toLowerCase(); const entry = catalogEntryForExtension(normalized); if (!entry) { return "none"; } return entry.osAssociationOverrides?.[normalized] ?? entry.osAssociation; } TabLocalFiles.catalogOsAssociation = catalogOsAssociation; function effectiveOpenMode(extension) { const normalized = extension.toLowerCase(); return TabLocalFiles.Preferences.readOpenModes()[normalized] || catalogDefaultOpenMode(normalized); } TabLocalFiles.effectiveOpenMode = effectiveOpenMode; })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { TabLocalFiles.EXTENSION_CONFIG = { appName: "Previewer", renderedAttribute: "data-tab-local-files-viewer", maxOutlineLevel: 6 }; TabLocalFiles.FILE_TYPE_DEFINITIONS = [ { id: "zip", displayName: "Archive", extensions: [], enabled: true, renderer: TabLocalFiles.ZipFile.render, pathMatcher: TabLocalFiles.ZipFile.matches }, { id: "directory", displayName: "Folder", extensions: [], enabled: true, renderer: TabLocalFiles.Directory.render, pathMatcher: TabLocalFiles.Directory.matches }, { id: "markdown", displayName: "Markdown", extensions: TabLocalFiles.MARKDOWN_EXTENSIONS, enabled: true, renderer: TabLocalFiles.Markdown.render }, { id: "svg", displayName: "SVG", extensions: [".svg"], enabled: true, renderer: TabLocalFiles.Svg.render }, { id: "image", displayName: "Image", extensions: TabLocalFiles.IMAGE_EXTENSIONS.filter((extension) => extension !== ".svg"), enabled: true, renderer: TabLocalFiles.ImageFile.render }, { id: "media", displayName: "Media", extensions: TabLocalFiles.MEDIA_EXTENSIONS, enabled: true, renderer: TabLocalFiles.MediaFile.render }, { id: "pdf", displayName: "PDF", extensions: [".pdf"], enabled: true, renderer: TabLocalFiles.Pdf.render }, { id: "office", displayName: "Office", extensions: TabLocalFiles.OFFICE_EXTENSIONS, enabled: true, renderer: TabLocalFiles.Office.render }, { id: "html", displayName: "HTML", extensions: TabLocalFiles.HTML_EXTENSIONS, enabled: true, renderer: TabLocalFiles.HtmlFile.render }, { id: "subtitle", displayName: "Subtitle", extensions: TabLocalFiles.SUBTITLE_EXTENSIONS, enabled: true, renderer: TabLocalFiles.Subtitle.render }, { id: "code-text", displayName: "Code / Text", extensions: TabLocalFiles.CODE_TEXT_EXTENSIONS, enabled: true, renderer: TabLocalFiles.CodeText.render } ]; function findPreviewableFileType(decodedPath) { return TabLocalFiles.findEnabledFileType(decodedPath, TabLocalFiles.FILE_TYPE_DEFINITIONS); } TabLocalFiles.findPreviewableFileType = findPreviewableFileType; function canPreviewPath(decodedPath) { return Boolean(findPreviewableFileType(decodedPath)); } TabLocalFiles.canPreviewPath = canPreviewPath; })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var InlineViewer; (function (InlineViewer) { let linksBound = false; let navigationRequestId = 0; function bindFileLinks() { if (linksBound) { return; } linksBound = true; document.addEventListener("click", (event) => { if (event.defaultPrevented || !isPlainPrimaryClick(event)) { return; } const anchor = event.target?.closest("a[href]"); if (!anchor || (anchor.target && anchor.target !== "_self")) { return; } if ((anchor.getAttribute?.("href") || "").startsWith("#")) { return; } if (navigateToLink(anchor.href)) { event.preventDefault(); } }); window.addEventListener("popstate", () => { const sourceUrl = sourceUrlFromLinkHref(window.location.href); if (!sourceUrl) { return; } void mountFile(sourceUrl, { chrome: TabLocalFiles.viewerChromeFromSearch(window.location.search), browserUrl: window.location.href, sidebarSnapshot: sidebarSnapshotFromLinkHref(window.location.href), updateHistory: false }); }); } InlineViewer.bindFileLinks = bindFileLinks; function sourceUrlFromLinkHref(href) { try { const url = new URL(href); if (url.protocol === "file:") { return url.toString(); } if (isExtensionViewerUrl(url)) { const sourceUrl = url.searchParams.get("file") || ""; return sourceUrl.startsWith("file://") ? sourceUrl : undefined; } } catch { return undefined; } return undefined; } InlineViewer.sourceUrlFromLinkHref = sourceUrlFromLinkHref; function navigateToLink(href) { const sourceUrl = sourceUrlFromLinkHref(href); if (!sourceUrl) { return false; } const url = new URL(sourceUrl); const decodedPath = decodeURIComponent(url.pathname); const definition = TabLocalFiles.findPreviewableFileType(decodedPath) || TabLocalFiles.UnsupportedFile.definition; if (!shouldInterceptInPage(definition) || !isCurrentExtensionViewer()) { return false; } void mountFile(sourceUrl, { browserUrl: browserUrlFromLinkHref(href), chrome: viewerChromeFromLinkHref(href), sidebarSnapshot: sidebarSnapshotFromLinkHref(href) }); return true; } InlineViewer.navigateToLink = navigateToLink; async function mountFile(sourceUrl, options = {}) { try { await mountFileInternal(sourceUrl, options); } catch (error) { console.error("[Previewer] Failed to mount file in workspace", error); const decodedPath = decodedPathFromSourceUrl(sourceUrl); const definition = TabLocalFiles.findPreviewableFileType(decodedPath) || TabLocalFiles.UnsupportedFile.definition; void TabLocalFiles.recordFileAnalyticsEvent({ type: "task", name: "file_preview_failed", value: "file-preview", triggerMethod: "manual_click", subModuleName: "viewer", sourceUrl, fileName: TabLocalFiles.fileNameFromPath(decodedPath), fileType: definition.id, fields: { stage: "inline_mount", error_name: error instanceof Error ? error.name : typeof error } }); } } InlineViewer.mountFile = mountFile; async function mountFileInternal(sourceUrl, options) { const requestId = nextNavigationRequestId(); if (!(await TabLocalFiles.EditSession.requestNavigation()) || !isCurrentNavigationRequest(requestId)) { return; } const decodedPath = decodedPathFromSourceUrl(sourceUrl); const definition = TabLocalFiles.findPreviewableFileType(decodedPath) || TabLocalFiles.UnsupportedFile.definition; const chrome = options.chrome || TabLocalFiles.viewerChromeFromSearch(window.location.search); if (await shouldStopUnavailableFileNavigation(sourceUrl, decodedPath, options)) { if (isCurrentNavigationRequest(requestId)) { TabLocalFiles.showToast(TabLocalFiles.t("common.fileUnavailable"), "error"); } return; } if (!isCurrentNavigationRequest(requestId)) { return; } const effectiveDefinition = await TabLocalFiles.ZipFile.resolveDefinition(definition, decodedPath); if (!isCurrentNavigationRequest(requestId)) { return; } const fileStat = effectiveDefinition.id === "subtitle" ? await TabLocalFiles.statOriginalFile(decodedPath) : undefined; if (!isCurrentNavigationRequest(requestId)) { return; } const largeFileDecision = TabLocalFiles.LargeFile.decisionFor(effectiveDefinition.id, fileStat); const mountId = TabLocalFiles.beginDocumentMount(); const snapshot = chrome.sidebarDisabled ? undefined : options.sidebarSnapshot || (await safeDirectorySnapshot(decodedPath)); if (!TabLocalFiles.isCurrentDocumentMount(mountId)) { return; } const fileName = TabLocalFiles.fileNameFromPath(decodedPath); const subtitleSource = effectiveDefinition.id === "subtitle" ? await TabLocalFiles.Subtitle.loadSource(fileName, sourceUrl, largeFileDecision) : undefined; const rawText = subtitleSource?.rawText ?? (await rawTextForDefinition(effectiveDefinition, sourceUrl)); if (!TabLocalFiles.isCurrentDocumentMount(mountId) || (effectiveDefinition.id === "subtitle" && !isCurrentNavigationRequest(requestId))) { return; } const context = { mountId, decodedPath, sourceUrl, fileName, fileTitle: TabLocalFiles.fileTitleFromName(fileName, effectiveDefinition), navigationRootPath: snapshot?.rootPath, navigationSidebarHtml: snapshot ? TabLocalFiles.Directory.renderSnapshotSidebar(snapshot, decodedPath) : undefined, headerDisabled: chrome.headerDisabled, sidebarDisabled: chrome.sidebarDisabled, fileStat, largeFileDecision, rawText, sourceBytes: subtitleSource?.sourceBytes, subtitleSubKind: subtitleSource?.subKind, rawHtml: effectiveDefinition.id === "html" ? rawText : undefined, definition: effectiveDefinition }; const rendered = effectiveDefinition.renderer(context); if (!TabLocalFiles.isCurrentDocumentMount(mountId)) { return; } document.documentElement.setAttribute(TabLocalFiles.EXTENSION_CONFIG.renderedAttribute, effectiveDefinition.id); document.title = rendered.title || context.fileTitle; TabLocalFiles.applyFileFavicon(context); if (!replaceWorkspaceMain(context, rendered)) { document.body.innerHTML = TabLocalFiles.renderDocumentLayout(context, rendered); } TabLocalFiles.bindLayoutControls(); try { rendered.afterMount?.(context); } catch (error) { console.error("[Previewer] afterMount failed", error); void TabLocalFiles.recordRenderContextAnalyticsEvent(context, { type: "task", name: "file_preview_failed", value: "file-preview", triggerMethod: "manual_click", subModuleName: "viewer", fields: { stage: "after_mount", error_name: error instanceof Error ? error.name : typeof error } }); } TabLocalFiles.applyStoredPanelMode(context); void TabLocalFiles.refreshFileHeaderStat(context); bindFileLinks(); updateBrowserUrl(browserUrlForSource(sourceUrl, snapshot, chrome, options.browserUrl), options.updateHistory !== false); void TabLocalFiles.recordRenderContextAnalyticsEvent(context, { type: "show", name: "file_preview_show", value: "file-preview", triggerMethod: "manual_click", subModuleName: "viewer", fields: { render_mode: context.largeFileDecision?.mode || "full", navigation_mode: "inline" } }); } function replaceWorkspaceMain(context, rendered) { if (context.sidebarDisabled || !context.navigationRootPath) { return false; } const page = document.querySelector(".tlf-page.tlf-workspace-page"); const main = page?.querySelector(":scope > .tlf-main"); const sidebar = page?.querySelector(":scope > .tlf-directory-sidebar[data-tlf-directory-snapshot]"); if (!page || !main || !sidebar || sidebarRootPath(sidebar) !== context.navigationRootPath) { return false; } page.setAttribute("data-file-type", context.definition.id); page.classList.toggle("tlf-page-no-header", Boolean(context.headerDisabled)); main.outerHTML = TabLocalFiles.renderDocumentMain(context, rendered); TabLocalFiles.Directory.syncSidebarCurrentPath(context.decodedPath); return true; } function nextNavigationRequestId() { navigationRequestId += 1; return navigationRequestId; } function isCurrentNavigationRequest(requestId) { return requestId === navigationRequestId; } async function shouldStopUnavailableFileNavigation(sourceUrl, decodedPath, options) { if (!shouldValidateFileAvailability(sourceUrl, decodedPath, options)) { return false; } try { const entries = await TabLocalFiles.listDirectoryEntries(parentDirectoryPath(decodedPath)); if (!entries) { return false; } return !entries.some((entry) => decodedPathFromSourceUrl(entry.href) === decodedPath); } catch { return false; } } function parentDirectoryPath(decodedPath) { const normalizedPath = decodedPath.replace(/\/+$/, ""); const lastSeparatorIndex = normalizedPath.lastIndexOf("/"); return lastSeparatorIndex <= 0 ? "/" : normalizedPath.slice(0, lastSeparatorIndex + 1); } function shouldValidateFileAvailability(sourceUrl, decodedPath, options) { if (decodedPath.endsWith("/") || !isFileSourceUrl(sourceUrl) || TabLocalFiles.ZipArchive.isVirtualPath(decodedPath)) { return false; } return Boolean(options.sidebarSnapshot || document.querySelector(".tlf-directory-sidebar[data-tlf-directory-snapshot]")); } function sidebarRootPath(sidebar) { const rawSnapshot = sidebar.dataset.tlfDirectorySnapshot; if (!rawSnapshot) { return undefined; } try { const snapshot = JSON.parse(rawSnapshot); return snapshot.rootPath; } catch { return undefined; } } async function safeDirectorySnapshot(decodedPath) { try { return await TabLocalFiles.Directory.snapshotForPath(decodedPath); } catch { return undefined; } } async function rawTextForDefinition(definition, sourceUrl) { if (definition.id === "unsupported") { return ""; } if (definition.id === "zip" || definition.id === "zip-entry-blocked") { return ""; } if (definition.id === "directory" || definition.id === "markdown" || definition.id === "code-text" || definition.id === "html" || definition.id === "svg") { return (await TabLocalFiles.readCurrentFileAsText(sourceUrl)) || ""; } return ""; } function decodedPathFromSourceUrl(sourceUrl) { try { return decodeURIComponent(new URL(sourceUrl).pathname); } catch { return sourceUrl; } } function shouldInterceptInPage(definition) { return definition.enabled; } function isFileSourceUrl(sourceUrl) { try { return new URL(sourceUrl).protocol === "file:"; } catch { return false; } } function isPlainPrimaryClick(event) { return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey; } InlineViewer.isPlainPrimaryClick = isPlainPrimaryClick; function isExtensionViewerUrl(url) { return url.protocol === "chrome-extension:" && url.pathname.endsWith("/viewer.html"); } function sidebarSnapshotFromLinkHref(href) { try { const url = new URL(href); if (!isExtensionViewerUrl(url)) { return undefined; } const rawSnapshot = url.searchParams.get("sidebar"); if (!rawSnapshot) { return undefined; } const snapshot = JSON.parse(rawSnapshot); return snapshot.rootPath && Array.isArray(snapshot.entries) ? snapshot : undefined; } catch { return undefined; } } function browserUrlFromLinkHref(href) { try { const url = new URL(href); return isExtensionViewerUrl(url) ? url.toString() : undefined; } catch { return undefined; } } function viewerChromeFromLinkHref(href) { try { const url = new URL(href); if (!isExtensionViewerUrl(url)) { return undefined; } const chrome = TabLocalFiles.viewerChromeFromSearch(url.search); return chrome.headerDisabled || chrome.sidebarDisabled ? chrome : undefined; } catch { return undefined; } } function browserUrlForSource(sourceUrl, sidebarSnapshot, chrome, requestedBrowserUrl) { try { if (requestedBrowserUrl) { return browserUrlWithChrome(requestedBrowserUrl, chrome); } return isCurrentExtensionViewer() ? TabLocalFiles.extensionViewerUrl(sourceUrl, sidebarSnapshot, chrome) : sourceUrl; } catch { return sourceUrl; } } function browserUrlWithChrome(browserUrl, chrome) { try { const url = new URL(browserUrl, window.location.href); if (!isExtensionViewerUrl(url)) { return browserUrl; } if (chrome.headerDisabled) { url.searchParams.set("tlf-header", "hidden"); } if (chrome.sidebarDisabled) { url.searchParams.set("tlf-sidebar", "hidden"); url.searchParams.delete("sidebar"); } return url.toString(); } catch { return browserUrl; } } function isCurrentExtensionViewer() { try { return isExtensionViewerUrl(new URL(window.location.href)); } catch { return false; } } function updateBrowserUrl(nextUrl, shouldUpdate) { if (!shouldUpdate || !window.history?.pushState) { return; } try { const current = new URL(window.location.href); const next = new URL(nextUrl, window.location.href); if (current.protocol !== next.protocol || current.origin !== next.origin || current.href === next.href) { return; } window.history.pushState({ tlfSourceUrl: sourceUrlFromLinkHref(next.href) }, "", next.href); } catch (error) { console.warn("[Previewer] Failed to update browser URL after in-page navigation", error); } } })(InlineViewer = TabLocalFiles.InlineViewer || (TabLocalFiles.InlineViewer = {})); })(TabLocalFiles || (TabLocalFiles = {})); var TabLocalFiles; (function (TabLocalFiles) { var Bootstrap; (function (Bootstrap) { const NAVIGATION_ROOT_STORAGE_KEY = "tab-local-files:navigation-root"; const EXTENSION_VIEWER_SOURCE_TEXT_DEFINITION_IDS = new Set(["directory", "markdown", "subtitle", "code-text", "html", "svg"]); const DIRECT_SOURCE_TEXT_DEFINITION_IDS = new Set(["markdown", "subtitle", "code-text", "html"]); const NATIVE_DIRECT_OPEN_DEFINITION_IDS = new Set(["pdf", "html", "media"]); const SELECT_TEXT_FORWARD_MESSAGE = "tlf-forward-selected-text"; const SELECT_TEXT_FORWARD_DELAY_MS = 80; let selectedTextForwardingBound = false; let selectedTextForwardTimer; let lastForwardedSelectedText = ""; function start() { TabLocalFiles.PerformanceMetrics.install(); TabLocalFiles.PdfTranslation.initializeFromRequest(); TabLocalFiles.localizeDocument(); TabLocalFiles.Preferences.onChange(() => { TabLocalFiles.Directory.applySettingsToSidebars(); TabLocalFiles.Directory.applySettingsToPreview(); TabLocalFiles.refreshDirectorySettingControls(); }); void startAsync(); } Bootstrap.start = start; async function startAsync() { let attemptedDefinition; let attemptedContext; let stage = "target"; try { await TabLocalFiles.Preferences.init(); const definition = getTargetDefinition(); if (!definition) { return; } attemptedDefinition = definition; ensureRendererStyles(); stage = "context"; const context = await createRenderContext(definition); if (!context) { return; } attemptedContext = context; stage = "render"; const rendered = context.definition.renderer(context); stage = "mount"; mountRenderedDocument(context, rendered); } catch (error) { console.error("[Previewer] Failed to render local file", error); if (attemptedDefinition) { recordInitialPreviewFailure(error, stage, attemptedDefinition, attemptedContext); } return; } } function getTargetDefinition() { if (hasRendered() || isRawFileRequest()) { return undefined; } const sourceUrl = targetSourceUrl(); if (!sourceUrl) { return undefined; } const decodedPath = decodedPathFromSourceUrl(sourceUrl); const definition = TabLocalFiles.findPreviewableFileType(decodedPath); if (!definition) { return isExtensionViewerRequest() ? TabLocalFiles.UnsupportedFile.definition : undefined; } return shouldUseNativeDirectFileOpen(definition, decodedPath, isExtensionViewerRequest()) ? undefined : definition; } async function createRenderContext(definition) { const sourceUrl = targetSourceUrl(); if (!sourceUrl) { return undefined; } const path = decodedPathFromSourceUrl(sourceUrl); const effectiveDefinition = await TabLocalFiles.ZipFile.resolveDefinition(definition, path); const fileStat = await safeStatSource(sourceUrl, path); const largeFileDecision = TabLocalFiles.LargeFile.decisionFor(effectiveDefinition.id, fileStat); const fileName = TabLocalFiles.fileNameFromPath(path); const subtitleSource = effectiveDefinition.id === "subtitle" ? await TabLocalFiles.Subtitle.loadSource(fileName, sourceUrl, largeFileDecision) : undefined; TabLocalFiles.recordDetectedTextEncoding(undefined); const rawText = subtitleSource ? subtitleSource.rawText : TabLocalFiles.ingestDecodedText(path, await rawTextForDefinition(effectiveDefinition, sourceUrl, largeFileDecision)); const chrome = TabLocalFiles.viewerChromeFromSearch(window.location.search); const snapshot = definition.id === "directory" ? undefined : await sidebarSnapshotForPath(path, chrome); const navigationRoot = navigationRootPath(path) || snapshot?.rootPath; return { decodedPath: path, sourceUrl, fileName, fileTitle: TabLocalFiles.fileTitleFromName(fileName, effectiveDefinition), navigationRootPath: navigationRoot, navigationSidebarHtml: snapshot ? TabLocalFiles.Directory.renderSnapshotSidebar(snapshot, path) : undefined, headerDisabled: chrome.headerDisabled, sidebarDisabled: chrome.sidebarDisabled, fileStat, largeFileDecision, rawText, sourceBytes: subtitleSource?.sourceBytes, subtitleSubKind: subtitleSource?.subKind, rawHtml: effectiveDefinition.id === "html" && largeFileDecision.mode === "full" ? TabLocalFiles.ingestDecodedText(path, await rawHtmlForSource(sourceUrl)) : undefined, definition: effectiveDefinition }; } function mountRenderedDocument(context, rendered) { context.mountId = TabLocalFiles.beginDocumentMount(); document.documentElement.setAttribute(TabLocalFiles.EXTENSION_CONFIG.renderedAttribute, context.definition.id); document.title = rendered.title || context.fileTitle; TabLocalFiles.applyFileFavicon(context); document.body.innerHTML = TabLocalFiles.renderDocumentLayout(context, rendered); TabLocalFiles.bindLayoutControls(); bindSelectedTextForwarding(); try { rendered.afterMount?.(context); } catch (error) { console.error("[Previewer] afterMount failed", error); void TabLocalFiles.recordRenderContextAnalyticsEvent(context, { type: "task", name: "file_preview_failed", value: "file-preview", triggerMethod: "auto", subModuleName: "viewer", fields: { stage: "after_mount", error_name: error instanceof Error ? error.name : typeof error } }); } TabLocalFiles.applyStoredPanelMode(context); TabLocalFiles.InlineViewer.bindFileLinks(); void TabLocalFiles.recordRenderContextAnalyticsEvent(context, { type: "show", name: "file_preview_show", value: "file-preview", triggerMethod: "auto", subModuleName: "viewer", fields: { render_mode: context.largeFileDecision?.mode || "full", navigation_mode: "initial" } }); void TabLocalFiles.refreshFileHeaderStat(context); } function recordInitialPreviewFailure(error, stage, definition, context) { const fields = { stage, error_name: error instanceof Error ? error.name : typeof error }; if (context) { void TabLocalFiles.recordRenderContextAnalyticsEvent(context, { type: "task", name: "file_preview_failed", value: "file-preview", triggerMethod: "auto", subModuleName: "viewer", fields }); return; } let sourceUrl = ""; try { sourceUrl = targetSourceUrl() || ""; } catch { } let decodedPath = ""; try { decodedPath = decodedPathFromSourceUrl(sourceUrl); } catch { } void TabLocalFiles.recordFileAnalyticsEvent({ type: "task", name: "file_preview_failed", value: "file-preview", triggerMethod: "auto", subModuleName: "viewer", sourceUrl, fileName: TabLocalFiles.fileNameFromPath(decodedPath), fileType: definition?.id || "unknown", fields }); } function bindSelectedTextForwarding() { if (selectedTextForwardingBound) { return; } selectedTextForwardingBound = true; const queueForward = () => { window.clearTimeout(selectedTextForwardTimer); selectedTextForwardTimer = window.setTimeout(() => { forwardSelectedText(); }, SELECT_TEXT_FORWARD_DELAY_MS); }; document.addEventListener("selectionchange", queueForward); document.addEventListener("mouseup", queueForward); document.addEventListener("keyup", queueForward); document.addEventListener("tlf:quote-document", (event) => { const context = TabLocalFiles.ChatEditViewer.captureDocument(event.detail); if (context) sendSelectedText(context.title, undefined, context); }); } function forwardSelectedText() { const editContext = TabLocalFiles.ChatEditViewer.captureSelection(); const text = editContext?.text || window.getSelection?.()?.toString().trim() || ""; const signature = text + (editContext ? JSON.stringify([editContext.contextId]) : ""); if (signature === lastForwardedSelectedText) { return; } lastForwardedSelectedText = signature; const tabs = chromeApi()?.tabs; if (tabs?.getCurrent) { try { const getCurrent = tabs.getCurrent.bind(tabs); getCurrent((tab) => { sendSelectedText(text, tab?.id, editContext); }); return; } catch { } } sendSelectedText(text, null, editContext); } function sendSelectedText(text, tabId, editContext) { const runtime = chromeApi()?.runtime; if (!runtime?.sendMessage) { return; } try { const sendMessage = runtime.sendMessage.bind(runtime); const reference = editContext ? { provider: editContext.provider, contextId: editContext.contextId, revision: editContext.revision, scope: editContext.scope, surface: editContext.surface, title: editContext.title } : undefined; if (tabId == null && chromeApi()?.tabs?.getCurrent) { chromeApi().tabs.getCurrent((tab) => sendMessage({ type: SELECT_TEXT_FORWARD_MESSAGE, text, tabId: tab?.id, editContext: reference })); } else { sendMessage({ type: SELECT_TEXT_FORWARD_MESSAGE, text, tabId, editContext: reference }); } } catch { } } function chromeApi() { return globalThis.chrome; } function hasRendered() { return document.documentElement.hasAttribute(TabLocalFiles.EXTENSION_CONFIG.renderedAttribute); } async function rawTextForDefinition(definition, sourceUrl, decision) { if (definition.id === "unsupported" || definition.id === "zip" || definition.id === "zip-entry-blocked") { return ""; } if (decision.mode === "blocked") { return ""; } if (decision.mode === "preview") { return (await TabLocalFiles.readCurrentFilePreviewAsText(sourceUrl, decision.maxBytes || TabLocalFiles.LargeFile.TEXT_PREVIEW_BYTES)) || ""; } if (isExtensionViewerRequest()) { return shouldReadSourceTextBeforeMount(definition, true) ? (await TabLocalFiles.readCurrentFileAsText(sourceUrl)) || "" : ""; } if (shouldReadSourceTextBeforeMount(definition, false)) { return (await TabLocalFiles.readCurrentFileAsText(sourceUrl)) || ""; } return TabLocalFiles.readRawTextFromDocument(); } function shouldReadSourceTextBeforeMount(definition, extensionViewer) { const sourceTextDefinitions = extensionViewer ? EXTENSION_VIEWER_SOURCE_TEXT_DEFINITION_IDS : DIRECT_SOURCE_TEXT_DEFINITION_IDS; return sourceTextDefinitions.has(definition.id); } Bootstrap.shouldReadSourceTextBeforeMount = shouldReadSourceTextBeforeMount; function shouldUseNativeDirectFileOpen(definition, decodedPath, extensionViewer) { if (extensionViewer) { return false; } const extension = TabLocalFiles.extensionFromFileName(TabLocalFiles.fileNameFromPath(decodedPath)); const effectiveMode = extension ? TabLocalFiles.effectiveOpenMode(extension) : undefined; if (effectiveMode) { return effectiveMode === "native"; } return NATIVE_DIRECT_OPEN_DEFINITION_IDS.has(definition.id); } Bootstrap.shouldUseNativeDirectFileOpen = shouldUseNativeDirectFileOpen; async function sidebarSnapshotForPath(path, chrome) { if (chrome.sidebarDisabled) { return undefined; } const urlSnapshot = viewerSidebarSnapshot(path); if (urlSnapshot) { return (await safeRefreshSidebarSnapshot(urlSnapshot)) || urlSnapshot; } const restoredSnapshot = await safeDirectorySnapshot(path); return restoredSnapshot ? (await safeRefreshSidebarSnapshot(restoredSnapshot)) || restoredSnapshot : undefined; } Bootstrap.sidebarSnapshotForPath = sidebarSnapshotForPath; async function rawHtmlForSource(sourceUrl) { if (isExtensionViewerRequest()) { return (await TabLocalFiles.readCurrentFileAsText(sourceUrl)) || ""; } return TabLocalFiles.readRawHtmlFromDocument(); } function targetSourceUrl() { if (TabLocalFiles.PdfMemory.isRequest()) { return TabLocalFiles.PdfMemory.sourceUrl(); } if (isExtensionViewerRequest()) { return extensionViewerSourceUrl(); } return window.location.protocol === "file:" ? window.location.href : undefined; } function extensionViewerSourceUrl() { if (!isExtensionViewerRequest()) { return undefined; } return new URLSearchParams(window.location.search).get("file") || undefined; } function decodedPathFromSourceUrl(sourceUrl) { return decodeURIComponent(new URL(sourceUrl).pathname); } async function safeStatSource(sourceUrl, path) { if (!isFileSourceUrl(sourceUrl) || path.endsWith("/")) { return undefined; } try { return await TabLocalFiles.statOriginalFile(path); } catch { return undefined; } } function isFileSourceUrl(sourceUrl) { try { return new URL(sourceUrl).protocol === "file:"; } catch { return false; } } function isRawFileRequest() { return window.location.protocol === "file:" && new URLSearchParams(window.location.search).has("tlf-raw"); } function isExtensionViewerRequest() { return window.location.protocol === "chrome-extension:" && window.location.pathname.endsWith("/viewer.html"); } function ensureRendererStyles() { if (isExtensionViewerRequest() || document.querySelector("link[data-tlf-renderer-style]")) { return; } const href = chromeRuntimeGetUrl("styles.css"); if (!href) { console.warn("[Previewer] Unable to resolve styles.css; rendered file page may be unstyled."); return; } const link = document.createElement("link"); link.rel = "stylesheet"; link.href = href; link.dataset.tlfRendererStyle = "true"; (document.head || document.documentElement).append(link); } function chromeRuntimeGetUrl(path) { return globalThis.chrome?.runtime?.getURL?.(path); } function viewerSidebarSnapshot(path) { if (!isExtensionViewerRequest()) { return undefined; } const rawSnapshot = new URLSearchParams(window.location.search).get("sidebar"); if (!rawSnapshot) { return undefined; } try { const snapshot = JSON.parse(rawSnapshot); if (!snapshot.rootPath || !Array.isArray(snapshot.entries) || !isInsideRoot(path, snapshot.rootPath)) { return undefined; } return snapshot; } catch { return undefined; } } function navigationRootPath(path) { const existingRoot = safeSessionGet(NAVIGATION_ROOT_STORAGE_KEY) || undefined; if (isDirectoryPath(path) && !TabLocalFiles.ZipArchive.isVirtualPath(path)) { if (existingRoot && isInsideRoot(path, existingRoot)) { return existingRoot; } safeSessionSet(NAVIGATION_ROOT_STORAGE_KEY, path); return path; } return existingRoot && isInsideRoot(path, existingRoot) ? existingRoot : undefined; } async function safeDirectorySnapshot(path) { try { return await TabLocalFiles.Directory.snapshotForPath(path); } catch (error) { console.warn("[Previewer] Failed to read directory snapshot", error); return undefined; } } async function safeRefreshSidebarSnapshot(snapshot) { try { const entries = await TabLocalFiles.listDirectoryEntries(snapshot.rootPath); return entries ? { ...snapshot, entries } : undefined; } catch (error) { console.warn("[Previewer] Failed to refresh directory snapshot", error); return undefined; } } function safeSessionGet(key) { try { return window.sessionStorage.getItem(key); } catch { return null; } } function safeSessionSet(key, value) { try { window.sessionStorage.setItem(key, value); } catch { } } function isDirectoryPath(path) { return path.endsWith("/"); } function isInsideRoot(path, root) { return path === root || path.startsWith(root); } })(Bootstrap = TabLocalFiles.Bootstrap || (TabLocalFiles.Bootstrap = {})); })(TabLocalFiles || (TabLocalFiles = {})); TabLocalFiles.Bootstrap.start();