Files
chrome-dev-formatter/format.js

1429 lines
47 KiB
JavaScript

const DEFAULT_LIGHT_THEME = "vendor/highlightjs/styles/github.min.css";
const DEFAULT_DARK_THEME = "vendor/highlightjs/styles/github-dark.min.css";
const INDENT_SIZE = 2;
const MAX_STRING_LENGTH = 120;
const THEME_FILES = [
"vendor/highlightjs/styles/1c-light.min.css",
"vendor/highlightjs/styles/a11y-dark.min.css",
"vendor/highlightjs/styles/a11y-light.min.css",
"vendor/highlightjs/styles/agate.min.css",
"vendor/highlightjs/styles/androidstudio.min.css",
"vendor/highlightjs/styles/an-old-hope.min.css",
"vendor/highlightjs/styles/arduino-light.min.css",
"vendor/highlightjs/styles/arta.min.css",
"vendor/highlightjs/styles/ascetic.min.css",
"vendor/highlightjs/styles/atom-one-dark-reasonable.min.css",
"vendor/highlightjs/styles/atom-one-dark.min.css",
"vendor/highlightjs/styles/atom-one-light.min.css",
"vendor/highlightjs/styles/brown-paper.min.css",
"vendor/highlightjs/styles/codepen-embed.min.css",
"vendor/highlightjs/styles/color-brewer.min.css",
"vendor/highlightjs/styles/cybertopia-cherry.min.css",
"vendor/highlightjs/styles/cybertopia-dimmer.min.css",
"vendor/highlightjs/styles/cybertopia-icecap.min.css",
"vendor/highlightjs/styles/cybertopia-saturated.min.css",
"vendor/highlightjs/styles/dark.min.css",
"vendor/highlightjs/styles/default.min.css",
"vendor/highlightjs/styles/devibeans.min.css",
"vendor/highlightjs/styles/docco.min.css",
"vendor/highlightjs/styles/far.min.css",
"vendor/highlightjs/styles/felipec.min.css",
"vendor/highlightjs/styles/foundation.min.css",
"vendor/highlightjs/styles/github-dark-dimmed.min.css",
"vendor/highlightjs/styles/github-dark.min.css",
"vendor/highlightjs/styles/github.min.css",
"vendor/highlightjs/styles/gml.min.css",
"vendor/highlightjs/styles/googlecode.min.css",
"vendor/highlightjs/styles/gradient-dark.min.css",
"vendor/highlightjs/styles/gradient-light.min.css",
"vendor/highlightjs/styles/grayscale.min.css",
"vendor/highlightjs/styles/hybrid.min.css",
"vendor/highlightjs/styles/idea.min.css",
"vendor/highlightjs/styles/intellij-light.min.css",
"vendor/highlightjs/styles/ir-black.min.css",
"vendor/highlightjs/styles/isbl-editor-dark.min.css",
"vendor/highlightjs/styles/isbl-editor-light.min.css",
"vendor/highlightjs/styles/kimbie-dark.min.css",
"vendor/highlightjs/styles/kimbie-light.min.css",
"vendor/highlightjs/styles/lightfair.min.css",
"vendor/highlightjs/styles/lioshi.min.css",
"vendor/highlightjs/styles/magula.min.css",
"vendor/highlightjs/styles/mono-blue.min.css",
"vendor/highlightjs/styles/monokai-sublime.min.css",
"vendor/highlightjs/styles/monokai.min.css",
"vendor/highlightjs/styles/night-owl.min.css",
"vendor/highlightjs/styles/nnfx-dark.min.css",
"vendor/highlightjs/styles/nnfx-light.min.css",
"vendor/highlightjs/styles/nord.min.css",
"vendor/highlightjs/styles/obsidian.min.css",
"vendor/highlightjs/styles/panda-syntax-dark.min.css",
"vendor/highlightjs/styles/panda-syntax-light.min.css",
"vendor/highlightjs/styles/paraiso-dark.min.css",
"vendor/highlightjs/styles/paraiso-light.min.css",
"vendor/highlightjs/styles/pojoaque.min.css",
"vendor/highlightjs/styles/purebasic.min.css",
"vendor/highlightjs/styles/qtcreator-dark.min.css",
"vendor/highlightjs/styles/qtcreator-light.min.css",
"vendor/highlightjs/styles/rainbow.min.css",
"vendor/highlightjs/styles/rose-pine-dawn.min.css",
"vendor/highlightjs/styles/rose-pine-moon.min.css",
"vendor/highlightjs/styles/rose-pine.min.css",
"vendor/highlightjs/styles/routeros.min.css",
"vendor/highlightjs/styles/school-book.min.css",
"vendor/highlightjs/styles/shades-of-purple.min.css",
"vendor/highlightjs/styles/srcery.min.css",
"vendor/highlightjs/styles/stackoverflow-dark.min.css",
"vendor/highlightjs/styles/stackoverflow-light.min.css",
"vendor/highlightjs/styles/sunburst.min.css",
"vendor/highlightjs/styles/tokyo-night-dark.min.css",
"vendor/highlightjs/styles/tokyo-night-light.min.css",
"vendor/highlightjs/styles/tomorrow-night-blue.min.css",
"vendor/highlightjs/styles/tomorrow-night-bright.min.css",
"vendor/highlightjs/styles/vs.min.css",
"vendor/highlightjs/styles/vs2015.min.css",
"vendor/highlightjs/styles/xcode.min.css",
"vendor/highlightjs/styles/xt256.min.css",
"vendor/highlightjs/styles/base16/3024.min.css",
"vendor/highlightjs/styles/base16/apathy.min.css",
"vendor/highlightjs/styles/base16/apprentice.min.css",
"vendor/highlightjs/styles/base16/ashes.min.css",
"vendor/highlightjs/styles/base16/atelier-cave-light.min.css",
"vendor/highlightjs/styles/base16/atelier-cave.min.css",
"vendor/highlightjs/styles/base16/atelier-dune-light.min.css",
"vendor/highlightjs/styles/base16/atelier-dune.min.css",
"vendor/highlightjs/styles/base16/atelier-estuary-light.min.css",
"vendor/highlightjs/styles/base16/atelier-estuary.min.css",
"vendor/highlightjs/styles/base16/atelier-forest-light.min.css",
"vendor/highlightjs/styles/base16/atelier-forest.min.css",
"vendor/highlightjs/styles/base16/atelier-heath-light.min.css",
"vendor/highlightjs/styles/base16/atelier-heath.min.css",
"vendor/highlightjs/styles/base16/atelier-lakeside-light.min.css",
"vendor/highlightjs/styles/base16/atelier-lakeside.min.css",
"vendor/highlightjs/styles/base16/atelier-plateau-light.min.css",
"vendor/highlightjs/styles/base16/atelier-plateau.min.css",
"vendor/highlightjs/styles/base16/atelier-savanna-light.min.css",
"vendor/highlightjs/styles/base16/atelier-savanna.min.css",
"vendor/highlightjs/styles/base16/atelier-seaside-light.min.css",
"vendor/highlightjs/styles/base16/atelier-seaside.min.css",
"vendor/highlightjs/styles/base16/atelier-sulphurpool-light.min.css",
"vendor/highlightjs/styles/base16/atelier-sulphurpool.min.css",
"vendor/highlightjs/styles/base16/atlas.min.css",
"vendor/highlightjs/styles/base16/bespin.min.css",
"vendor/highlightjs/styles/base16/black-metal-bathory.min.css",
"vendor/highlightjs/styles/base16/black-metal-burzum.min.css",
"vendor/highlightjs/styles/base16/black-metal-dark-funeral.min.css",
"vendor/highlightjs/styles/base16/black-metal-gorgoroth.min.css",
"vendor/highlightjs/styles/base16/black-metal-immortal.min.css",
"vendor/highlightjs/styles/base16/black-metal-khold.min.css",
"vendor/highlightjs/styles/base16/black-metal-marduk.min.css",
"vendor/highlightjs/styles/base16/black-metal-mayhem.min.css",
"vendor/highlightjs/styles/base16/black-metal-nile.min.css",
"vendor/highlightjs/styles/base16/black-metal-venom.min.css",
"vendor/highlightjs/styles/base16/black-metal.min.css",
"vendor/highlightjs/styles/base16/bright.min.css",
"vendor/highlightjs/styles/base16/brewer.min.css",
"vendor/highlightjs/styles/base16/brogrammer.min.css",
"vendor/highlightjs/styles/base16/brush-trees-dark.min.css",
"vendor/highlightjs/styles/base16/brush-trees.min.css",
"vendor/highlightjs/styles/base16/chalk.min.css",
"vendor/highlightjs/styles/base16/circus.min.css",
"vendor/highlightjs/styles/base16/classic-dark.min.css",
"vendor/highlightjs/styles/base16/classic-light.min.css",
"vendor/highlightjs/styles/base16/codeschool.min.css",
"vendor/highlightjs/styles/base16/colors.min.css",
"vendor/highlightjs/styles/base16/cupcake.min.css",
"vendor/highlightjs/styles/base16/cupertino.min.css",
"vendor/highlightjs/styles/base16/danqing.min.css",
"vendor/highlightjs/styles/base16/darcula.min.css",
"vendor/highlightjs/styles/base16/dark-violet.min.css",
"vendor/highlightjs/styles/base16/darkmoss.min.css",
"vendor/highlightjs/styles/base16/darktooth.min.css",
"vendor/highlightjs/styles/base16/decaf.min.css",
"vendor/highlightjs/styles/base16/default-dark.min.css",
"vendor/highlightjs/styles/base16/default-light.min.css",
"vendor/highlightjs/styles/base16/dirtysea.min.css",
"vendor/highlightjs/styles/base16/dracula.min.css",
"vendor/highlightjs/styles/base16/edge-dark.min.css",
"vendor/highlightjs/styles/base16/edge-light.min.css",
"vendor/highlightjs/styles/base16/eighties.min.css",
"vendor/highlightjs/styles/base16/embers.min.css",
"vendor/highlightjs/styles/base16/equilibrium-dark.min.css",
"vendor/highlightjs/styles/base16/equilibrium-gray-dark.min.css",
"vendor/highlightjs/styles/base16/equilibrium-gray-light.min.css",
"vendor/highlightjs/styles/base16/equilibrium-light.min.css",
"vendor/highlightjs/styles/base16/espresso.min.css",
"vendor/highlightjs/styles/base16/eva-dim.min.css",
"vendor/highlightjs/styles/base16/eva.min.css",
"vendor/highlightjs/styles/base16/flat.min.css",
"vendor/highlightjs/styles/base16/framer.min.css",
"vendor/highlightjs/styles/base16/fruit-soda.min.css",
"vendor/highlightjs/styles/base16/gigavolt.min.css",
"vendor/highlightjs/styles/base16/google-dark.min.css",
"vendor/highlightjs/styles/base16/google-light.min.css",
"vendor/highlightjs/styles/base16/green-screen.min.css",
"vendor/highlightjs/styles/base16/grayscale-dark.min.css",
"vendor/highlightjs/styles/base16/grayscale-light.min.css",
"vendor/highlightjs/styles/base16/gruvbox-dark-hard.min.css",
"vendor/highlightjs/styles/base16/gruvbox-dark-medium.min.css",
"vendor/highlightjs/styles/base16/gruvbox-dark-pale.min.css",
"vendor/highlightjs/styles/base16/gruvbox-dark-soft.min.css",
"vendor/highlightjs/styles/base16/gruvbox-light-hard.min.css",
"vendor/highlightjs/styles/base16/gruvbox-light-medium.min.css",
"vendor/highlightjs/styles/base16/gruvbox-light-soft.min.css",
"vendor/highlightjs/styles/base16/hardcore.min.css",
"vendor/highlightjs/styles/base16/harmonic16-dark.min.css",
"vendor/highlightjs/styles/base16/harmonic16-light.min.css",
"vendor/highlightjs/styles/base16/heetch-dark.min.css",
"vendor/highlightjs/styles/base16/heetch-light.min.css",
"vendor/highlightjs/styles/base16/helios.min.css",
"vendor/highlightjs/styles/base16/hopscotch.min.css",
"vendor/highlightjs/styles/base16/horizon-dark.min.css",
"vendor/highlightjs/styles/base16/horizon-light.min.css",
"vendor/highlightjs/styles/base16/humanoid-dark.min.css",
"vendor/highlightjs/styles/base16/humanoid-light.min.css",
"vendor/highlightjs/styles/base16/ia-dark.min.css",
"vendor/highlightjs/styles/base16/ia-light.min.css",
"vendor/highlightjs/styles/base16/icy-dark.min.css",
"vendor/highlightjs/styles/base16/ir-black.min.css",
"vendor/highlightjs/styles/base16/isotope.min.css",
"vendor/highlightjs/styles/base16/kimber.min.css",
"vendor/highlightjs/styles/base16/london-tube.min.css",
"vendor/highlightjs/styles/base16/macintosh.min.css",
"vendor/highlightjs/styles/base16/marrakesh.min.css",
"vendor/highlightjs/styles/base16/material-darker.min.css",
"vendor/highlightjs/styles/base16/material-lighter.min.css",
"vendor/highlightjs/styles/base16/material-palenight.min.css",
"vendor/highlightjs/styles/base16/material-vivid.min.css",
"vendor/highlightjs/styles/base16/material.min.css",
"vendor/highlightjs/styles/base16/materia.min.css",
"vendor/highlightjs/styles/base16/mellow-purple.min.css",
"vendor/highlightjs/styles/base16/mexico-light.min.css",
"vendor/highlightjs/styles/base16/mocha.min.css",
"vendor/highlightjs/styles/base16/monokai.min.css",
"vendor/highlightjs/styles/base16/nebula.min.css",
"vendor/highlightjs/styles/base16/nord.min.css",
"vendor/highlightjs/styles/base16/nova.min.css",
"vendor/highlightjs/styles/base16/ocean.min.css",
"vendor/highlightjs/styles/base16/oceanicnext.min.css",
"vendor/highlightjs/styles/base16/onedark.min.css",
"vendor/highlightjs/styles/base16/one-light.min.css",
"vendor/highlightjs/styles/base16/outrun-dark.min.css",
"vendor/highlightjs/styles/base16/papercolor-dark.min.css",
"vendor/highlightjs/styles/base16/papercolor-light.min.css",
"vendor/highlightjs/styles/base16/paraiso.min.css",
"vendor/highlightjs/styles/base16/pasque.min.css",
"vendor/highlightjs/styles/base16/phd.min.css",
"vendor/highlightjs/styles/base16/pico.min.css",
"vendor/highlightjs/styles/base16/pop.min.css",
"vendor/highlightjs/styles/base16/porple.min.css",
"vendor/highlightjs/styles/base16/qualia.min.css",
"vendor/highlightjs/styles/base16/railscasts.min.css",
"vendor/highlightjs/styles/base16/rebecca.min.css",
"vendor/highlightjs/styles/base16/ros-pine-dawn.min.css",
"vendor/highlightjs/styles/base16/ros-pine-moon.min.css",
"vendor/highlightjs/styles/base16/ros-pine.min.css",
"vendor/highlightjs/styles/base16/sagelight.min.css",
"vendor/highlightjs/styles/base16/sandcastle.min.css",
"vendor/highlightjs/styles/base16/seti-ui.min.css",
"vendor/highlightjs/styles/base16/shapeshifter.min.css",
"vendor/highlightjs/styles/base16/silk-dark.min.css",
"vendor/highlightjs/styles/base16/silk-light.min.css",
"vendor/highlightjs/styles/base16/snazzy.min.css",
"vendor/highlightjs/styles/base16/solar-flare-light.min.css",
"vendor/highlightjs/styles/base16/solar-flare.min.css",
"vendor/highlightjs/styles/base16/solarized-dark.min.css",
"vendor/highlightjs/styles/base16/solarized-light.min.css",
"vendor/highlightjs/styles/base16/spacemacs.min.css",
"vendor/highlightjs/styles/base16/summercamp.min.css",
"vendor/highlightjs/styles/base16/summerfruit-dark.min.css",
"vendor/highlightjs/styles/base16/summerfruit-light.min.css",
"vendor/highlightjs/styles/base16/synth-midnight-terminal-dark.min.css",
"vendor/highlightjs/styles/base16/synth-midnight-terminal-light.min.css",
"vendor/highlightjs/styles/base16/tango.min.css",
"vendor/highlightjs/styles/base16/tender.min.css",
"vendor/highlightjs/styles/base16/tomorrow-night.min.css",
"vendor/highlightjs/styles/base16/tomorrow.min.css",
"vendor/highlightjs/styles/base16/twilight.min.css",
"vendor/highlightjs/styles/base16/unikitty-dark.min.css",
"vendor/highlightjs/styles/base16/unikitty-light.min.css",
"vendor/highlightjs/styles/base16/vulcan.min.css",
"vendor/highlightjs/styles/base16/windows-10-light.min.css",
"vendor/highlightjs/styles/base16/windows-10.min.css",
"vendor/highlightjs/styles/base16/windows-95-light.min.css",
"vendor/highlightjs/styles/base16/windows-95.min.css",
"vendor/highlightjs/styles/base16/windows-high-contrast-light.min.css",
"vendor/highlightjs/styles/base16/windows-high-contrast.min.css",
"vendor/highlightjs/styles/base16/windows-nt-light.min.css",
"vendor/highlightjs/styles/base16/windows-nt.min.css",
"vendor/highlightjs/styles/base16/woodland.min.css",
"vendor/highlightjs/styles/base16/xcode-dusk.min.css",
"vendor/highlightjs/styles/base16/zenburn.min.css"
];
function ensureBaseStyles() {
if (document.getElementById("json-formatter-style")) return;
const style = document.createElement("style");
style.id = "json-formatter-style";
style.textContent = `
:root {
--json-bg: #f6f8fa;
--json-header-bg: #f6f8fa;
--json-header-text: #1f2328;
--json-muted: #57606a;
--json-border: #d0d7de;
--json-control-bg: #ffffff;
--json-control-text: #1f2328;
--json-line-number: #8c959f;
--json-line-number-border: #d8dee4;
--json-toggle-bg: #ffffff;
--json-toggle-text: #57606a;
--json-toggle-border: #d0d7de;
--json-toggle-hover: #f3f4f6;
}
:root[data-json-theme="dark"] {
--json-bg: #0d1117;
--json-header-bg: #0d1117;
--json-header-text: #c9d1d9;
--json-muted: #8b949e;
--json-border: #30363d;
--json-control-bg: #161b22;
--json-control-text: #c9d1d9;
--json-line-number: #6e7681;
--json-line-number-border: #30363d;
--json-toggle-bg: #161b22;
--json-toggle-text: #8b949e;
--json-toggle-border: #30363d;
--json-toggle-hover: #21262d;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
background: var(--json-bg);
--json-ln-width: 0px;
--json-toggle-width: 16px;
}
body.show-line-numbers {
--json-ln-width: 48px;
}
.json-header {
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 12px;
background: var(--json-header-bg);
color: var(--json-header-text);
border-bottom: 1px solid var(--json-border);
}
.json-title {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.3px;
text-transform: uppercase;
color: var(--json-muted);
}
.json-controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.json-control {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--json-muted);
}
.json-control select {
font: inherit;
color: var(--json-control-text);
background: var(--json-control-bg);
border: 1px solid var(--json-border);
border-radius: 6px;
padding: 4px 8px;
}
.json-control input[type="checkbox"] {
accent-color: var(--json-control-text);
}
.json-content {
padding: 0 12px 16px;
overflow-x: auto;
}
pre {
margin: 0;
white-space: pre;
word-break: normal;
overflow: visible;
}
pre code.hljs {
display: block;
font-size: 13px;
line-height: 1.5;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
overflow-x: visible;
padding: 0;
}
.line {
position: relative;
display: block;
padding-left: calc(var(--json-ln-width) + var(--json-toggle-width) + 12px);
white-space: pre;
}
body:not(.show-line-numbers) .line {
padding-left: calc(var(--json-toggle-width) + 12px);
}
body.show-line-numbers .line::before {
content: attr(data-line);
position: absolute;
left: 0;
width: var(--json-ln-width);
text-align: right;
padding-right: 8px;
color: var(--json-line-number);
border-right: 1px solid var(--json-line-number-border);
user-select: none;
}
.json-toggle {
position: absolute;
left: calc(var(--json-ln-width) + 4px);
top: 2px;
width: var(--json-toggle-width);
height: var(--json-toggle-width);
border: 1px solid var(--json-toggle-border);
border-radius: 4px;
background: var(--json-toggle-bg);
color: var(--json-toggle-text);
font-size: 11px;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
}
body:not(.show-line-numbers) .json-toggle {
left: 4px;
}
.json-toggle:hover {
background: var(--json-toggle-hover);
}
.json-code {
white-space: pre;
}
.json-string {
white-space: pre-wrap;
word-break: break-word;
}
.json-block-summary {
color: var(--json-muted);
}
a.json-link {
color: inherit;
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
a.json-link:hover {
text-decoration-thickness: 2px;
filter: brightness(1.1);
}
`;
const container = document.head || document.documentElement || document.body;
if (container) container.appendChild(style);
}
function themeLabel(file) {
const trimmed = file
.replace(/^vendor\/highlightjs\/styles\//, "")
.replace(/\.min\.css$/, "")
.replace(/\.css$/, "");
const parts = trimmed.split("/");
const name = parts.pop() || trimmed;
const base = name
.split("-")
.map((chunk) =>
chunk.length ? chunk[0].toUpperCase() + chunk.slice(1) : chunk
)
.join(" ");
if (parts[0] === "base16") return `Base16: ${base}`;
return base;
}
function getThemeState() {
return new Promise((resolve) => {
chrome.storage.sync.get(
{
themeMode: null,
theme: null,
lightTheme: DEFAULT_LIGHT_THEME,
darkTheme: DEFAULT_DARK_THEME,
lineNumbers: false
},
(data) => {
resolve({
themeMode: data.themeMode || data.theme || "system",
lightTheme: data.lightTheme || DEFAULT_LIGHT_THEME,
darkTheme: data.darkTheme || DEFAULT_DARK_THEME,
lineNumbers: Boolean(data.lineNumbers)
});
}
);
});
}
function resolveMode(themeMode) {
if (themeMode === "dark" || themeMode === "light") return themeMode;
return window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function ensureThemeLink(themeFile) {
const url = chrome.runtime.getURL(themeFile);
let link = document.getElementById("json-highlight-theme");
if (!link) {
link = document.createElement("link");
link.id = "json-highlight-theme";
link.rel = "stylesheet";
const container = document.head || document.documentElement || document.body;
const baseStyle = document.getElementById("json-formatter-style");
if (container) {
if (baseStyle && baseStyle.parentNode === container) {
container.insertBefore(link, baseStyle);
} else {
container.appendChild(link);
}
}
}
if (link.href !== url) link.href = url;
}
function applyThemeState(state) {
const resolved = resolveMode(state.themeMode);
const cssFile = resolved === "dark" ? state.darkTheme : state.lightTheme;
document.documentElement.dataset.jsonTheme = resolved;
window.__jsonFormatterTheme = resolved;
ensureThemeLink(cssFile);
return resolved;
}
function populateThemeSelect(select) {
const entries = THEME_FILES.map((file) => ({
file,
label: themeLabel(file)
})).sort((a, b) => a.label.localeCompare(b.label));
select.innerHTML = "";
entries.forEach((entry) => {
const option = document.createElement("option");
option.value = entry.file;
option.textContent = entry.label;
select.appendChild(option);
});
}
function saveThemeState(state) {
chrome.storage.sync.set({
themeMode: state.themeMode,
lightTheme: state.lightTheme,
darkTheme: state.darkTheme,
lineNumbers: state.lineNumbers
});
}
let toggleCounter = 0;
function nextToggleId(prefix) {
toggleCounter += 1;
return `${prefix}-${toggleCounter}`;
}
function createTokenSpan(className, text) {
const span = document.createElement("span");
if (className) span.className = className;
span.textContent = text;
return span;
}
function createLine(indent, toggleId) {
const line = document.createElement("span");
line.className = "line";
line.dataset.line = "";
let toggle = null;
if (toggleId) {
toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "json-toggle";
toggle.dataset.toggleId = toggleId;
toggle.textContent = "-";
line.appendChild(toggle);
}
const code = document.createElement("span");
code.className = "json-code";
line.appendChild(code);
if (indent > 0) {
code.appendChild(document.createTextNode(" ".repeat(indent)));
}
return { line, code, toggle };
}
function appendKeyPrefix(code, key) {
if (key === null || key === undefined) return;
code.appendChild(createTokenSpan("hljs-attr", JSON.stringify(key)));
code.appendChild(document.createTextNode(": "));
}
function appendComma(code) {
code.appendChild(document.createTextNode(","));
}
function appendScalar(code, value, stringEntry) {
if (typeof value === "string") {
const span = createTokenSpan(
"hljs-string json-string",
JSON.stringify(value)
);
code.appendChild(span);
if (stringEntry) stringEntry.element = span;
return;
}
if (typeof value === "number") {
code.appendChild(createTokenSpan("hljs-number", String(value)));
return;
}
if (typeof value === "boolean" || value === null) {
code.appendChild(createTokenSpan("hljs-literal", String(value)));
return;
}
code.appendChild(document.createTextNode(String(value)));
}
function appendValue(container, value, indent, key, shouldComma, registry) {
if (Array.isArray(value)) {
if (value.length === 0) {
const lineData = createLine(indent, null);
appendKeyPrefix(lineData.code, key);
lineData.code.appendChild(document.createTextNode("[]"));
if (shouldComma) appendComma(lineData.code);
container.appendChild(lineData.line);
return;
}
const toggleId = nextToggleId("block");
const openLine = createLine(indent, toggleId);
appendKeyPrefix(openLine.code, key);
openLine.code.appendChild(document.createTextNode("["));
const body = document.createElement("div");
body.className = "json-block-body";
value.forEach((item, index) => {
appendValue(
body,
item,
indent + INDENT_SIZE,
null,
index < value.length - 1,
registry
);
});
const closeLine = createLine(indent, null);
closeLine.code.appendChild(document.createTextNode("]"));
const summaryLine = createLine(indent, toggleId);
summaryLine.line.classList.add("json-block-summary");
summaryLine.line.style.display = "none";
appendKeyPrefix(summaryLine.code, key);
summaryLine.code.appendChild(document.createTextNode("[...]"));
if (shouldComma) {
appendComma(closeLine.code);
appendComma(summaryLine.code);
}
const block = document.createElement("div");
block.className = "json-block";
block.appendChild(openLine.line);
block.appendChild(body);
block.appendChild(closeLine.line);
block.appendChild(summaryLine.line);
registry.set(toggleId, {
type: "block",
collapsed: false,
openLine: openLine.line,
closeLine: closeLine.line,
summaryLine: summaryLine.line,
body,
toggles: [openLine.toggle, summaryLine.toggle]
});
container.appendChild(block);
return;
}
if (value && typeof value === "object") {
const entries = Object.entries(value);
if (entries.length === 0) {
const lineData = createLine(indent, null);
appendKeyPrefix(lineData.code, key);
lineData.code.appendChild(document.createTextNode("{}"));
if (shouldComma) appendComma(lineData.code);
container.appendChild(lineData.line);
return;
}
const toggleId = nextToggleId("block");
const openLine = createLine(indent, toggleId);
appendKeyPrefix(openLine.code, key);
openLine.code.appendChild(document.createTextNode("{"));
const body = document.createElement("div");
body.className = "json-block-body";
entries.forEach(([entryKey, entryValue], index) => {
appendValue(
body,
entryValue,
indent + INDENT_SIZE,
entryKey,
index < entries.length - 1,
registry
);
});
const closeLine = createLine(indent, null);
closeLine.code.appendChild(document.createTextNode("}"));
const summaryLine = createLine(indent, toggleId);
summaryLine.line.classList.add("json-block-summary");
summaryLine.line.style.display = "none";
appendKeyPrefix(summaryLine.code, key);
summaryLine.code.appendChild(document.createTextNode("{...}"));
if (shouldComma) {
appendComma(closeLine.code);
appendComma(summaryLine.code);
}
const block = document.createElement("div");
block.className = "json-block";
block.appendChild(openLine.line);
block.appendChild(body);
block.appendChild(closeLine.line);
block.appendChild(summaryLine.line);
registry.set(toggleId, {
type: "block",
collapsed: false,
openLine: openLine.line,
closeLine: closeLine.line,
summaryLine: summaryLine.line,
body,
toggles: [openLine.toggle, summaryLine.toggle]
});
container.appendChild(block);
return;
}
const isString = typeof value === "string";
const canCollapse = isString && value.length > MAX_STRING_LENGTH;
const stringToggleId = canCollapse ? nextToggleId("string") : null;
const lineData = createLine(indent, stringToggleId);
appendKeyPrefix(lineData.code, key);
const stringEntry = canCollapse
? {
type: "string",
collapsed: false,
startCollapsed: true,
collapsedText: "\"...\"",
element: null,
toggle: lineData.toggle,
fullHtml: null
}
: null;
appendScalar(lineData.code, value, stringEntry);
if (shouldComma) appendComma(lineData.code);
container.appendChild(lineData.line);
if (stringEntry && stringToggleId) {
registry.set(stringToggleId, stringEntry);
}
}
function refreshLineNumbers(container, enabled) {
const lines = container.querySelectorAll(".line");
let lineNumber = 1;
lines.forEach((line) => {
if (line.style.display === "none") return;
line.dataset.line = String(lineNumber);
lineNumber += 1;
});
if (!enabled) return;
const maxDigits = String(Math.max(1, lineNumber - 1)).length;
const width = Math.max(32, maxDigits * 8 + 12);
document.body.style.setProperty("--json-ln-width", `${width}px`);
}
function updateToggleButtons(buttons, collapsed) {
buttons.forEach((button) => {
if (button) button.textContent = collapsed ? "+" : "-";
});
}
function applyBlockCollapsed(entry, collapsed) {
entry.collapsed = collapsed;
entry.openLine.style.display = collapsed ? "none" : "";
entry.body.style.display = collapsed ? "none" : "";
entry.closeLine.style.display = collapsed ? "none" : "";
entry.summaryLine.style.display = collapsed ? "" : "none";
updateToggleButtons(entry.toggles, collapsed);
}
function applyStringCollapsed(entry, collapsed) {
entry.collapsed = collapsed;
if (collapsed) {
entry.element.textContent = entry.collapsedText;
} else if (entry.fullHtml) {
entry.element.innerHTML = entry.fullHtml;
}
updateToggleButtons([entry.toggle], collapsed);
}
function escapeHtml(text) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function splitHighlightedHtmlLines(highlightedHtml) {
const container = document.createElement("div");
container.innerHTML = highlightedHtml;
const lines = [];
let currentLine = document.createDocumentFragment();
let currentTarget = currentLine;
let openDescriptors = [];
let currentClones = [];
function cloneDescriptor(desc) {
const clone = document.createElement(desc.tagName);
desc.attrs.forEach(([name, value]) => clone.setAttribute(name, value));
return clone;
}
function rebuildOpenElements() {
currentClones = [];
let target = currentLine;
openDescriptors.forEach((desc) => {
const clone = cloneDescriptor(desc);
target.appendChild(clone);
currentClones.push(clone);
target = clone;
});
currentTarget = target;
}
function startNewLine() {
lines.push(currentLine);
currentLine = document.createDocumentFragment();
rebuildOpenElements();
}
function describeElement(node) {
return {
tagName: node.tagName.toLowerCase(),
attrs: Array.from(node.attributes).map((attr) => [attr.name, attr.value])
};
}
function enterElement(node) {
const desc = describeElement(node);
openDescriptors.push(desc);
const clone = cloneDescriptor(desc);
currentTarget.appendChild(clone);
currentClones.push(clone);
currentTarget = clone;
}
function exitElement() {
openDescriptors.pop();
currentClones.pop();
currentTarget = currentClones.length
? currentClones[currentClones.length - 1]
: currentLine;
}
function processNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const text = (node.nodeValue || "")
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
const parts = text.split("\n");
parts.forEach((part, index) => {
if (part) {
currentTarget.appendChild(document.createTextNode(part));
}
if (index < parts.length - 1) {
startNewLine();
}
});
return;
}
if (node.nodeType === Node.ELEMENT_NODE) {
enterElement(node);
node.childNodes.forEach(processNode);
exitElement();
}
}
container.childNodes.forEach(processNode);
lines.push(currentLine);
return lines;
}
function highlightText(rawText, language) {
if (window.hljs && typeof hljs.highlight === "function") {
return hljs.highlight(rawText, { language }).value;
}
return escapeHtml(rawText);
}
function splitRawLines(rawText) {
return rawText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
}
function getIndentCount(line) {
let count = 0;
for (let i = 0; i < line.length; i += 1) {
const char = line[i];
if (char === " ") {
count += 1;
} else if (char === "\t") {
count += 2;
} else {
break;
}
}
return count;
}
function buildYamlGroups(lines) {
const groups = [];
const stack = [];
function isIgnorable(line) {
const trimmed = line.trim();
return trimmed === "" || trimmed.startsWith("#");
}
function nextNonEmptyIndent(fromIndex) {
for (let i = fromIndex; i < lines.length; i += 1) {
if (isIgnorable(lines[i])) continue;
return getIndentCount(lines[i]);
}
return null;
}
for (let i = 0; i < lines.length; i += 1) {
if (isIgnorable(lines[i])) continue;
const indent = getIndentCount(lines[i]);
while (stack.length && indent <= stack[stack.length - 1].indent) {
const group = stack.pop();
const end = i - 1;
if (end > group.start) {
groups.push({ start: group.start, end });
}
}
const nextIndent = nextNonEmptyIndent(i + 1);
if (nextIndent !== null && nextIndent > indent) {
stack.push({ start: i, indent });
}
}
const lastIndex = lines.length - 1;
while (stack.length) {
const group = stack.pop();
if (lastIndex > group.start) {
groups.push({ start: group.start, end: lastIndex });
}
}
return groups;
}
function buildMarkdownGroups(lines) {
const headings = [];
lines.forEach((line, index) => {
const match = line.match(/^\s*(#{1,6})\s+/);
if (match) {
headings.push({ index, level: match[1].length });
}
});
const groups = [];
headings.forEach((heading, i) => {
let end = lines.length - 1;
for (let j = i + 1; j < headings.length; j += 1) {
if (headings[j].level <= heading.level) {
end = headings[j].index - 1;
break;
}
}
if (end > heading.index) {
groups.push({ start: heading.index, end });
}
});
return groups;
}
function buildXmlGroups(lines) {
const groups = [];
const stack = [];
const tagRegex = /<[^>]+>/g;
function tagName(tag) {
const match = tag.match(/^<\/?\s*([^\s>\/]+)/);
return match ? match[1] : null;
}
lines.forEach((line, index) => {
const tags = line.match(tagRegex) || [];
tags.forEach((tag) => {
if (tag.startsWith("<!--") || tag.startsWith("<!") || tag.startsWith("<?")) {
return;
}
const name = tagName(tag);
if (!name) return;
const isClosing = /^<\//.test(tag);
const isSelfClosing = /\/\s*>$/.test(tag);
if (isClosing) {
const open = stack.pop();
if (open && index > open.start) {
groups.push({ start: open.start, end: index });
}
return;
}
if (!isSelfClosing) {
stack.push({ name, start: index });
}
});
});
return groups;
}
function buildCollapsibleGroups(lines, language) {
if (language === "yaml") return buildYamlGroups(lines);
if (language === "xml") return buildXmlGroups(lines);
if (language === "markdown") return buildMarkdownGroups(lines);
return [];
}
function updateFoldVisibility(lineElements, groups) {
const collapsed = groups.filter((group) => group.collapsed);
lineElements.forEach((line, index) => {
const hidden = collapsed.some(
(group) => index > group.start && index <= group.end
);
line.style.display = hidden ? "none" : "";
});
groups.forEach((group) => {
const hiddenByParent = collapsed.some(
(other) =>
other !== group &&
group.summaryPosition > other.start &&
group.summaryPosition <= other.end
);
group.summaryLine.style.display =
group.collapsed && !hiddenByParent ? "" : "none";
updateToggleButtons(group.toggles, group.collapsed);
});
}
function renderHighlightedContent(codeContainer, rawText, language) {
const rawLines = splitRawLines(rawText);
const highlightedHtml = highlightText(rawText, language);
const lineFragments = splitHighlightedHtmlLines(highlightedHtml);
while (lineFragments.length < rawLines.length) {
lineFragments.push(document.createDocumentFragment());
}
if (lineFragments.length > rawLines.length) {
lineFragments.length = rawLines.length;
}
const groups = buildCollapsibleGroups(rawLines, language).sort(
(a, b) => a.start - b.start || b.end - a.end
);
const groupsByStart = new Map();
groups.forEach((group) => {
const existing = groupsByStart.get(group.start);
if (!existing || existing.end < group.end) {
groupsByStart.set(group.start, group);
}
});
const uniqueGroups = Array.from(groupsByStart.values());
const registry = new Map();
const lineElements = [];
rawLines.forEach((line, index) => {
const group = groupsByStart.get(index);
const toggleId = group ? nextToggleId("fold") : null;
const lineData = createLine(0, toggleId);
lineData.code.appendChild(lineFragments[index]);
codeContainer.appendChild(lineData.line);
lineElements[index] = lineData.line;
if (group) {
group.toggleId = toggleId;
group.collapsed = false;
group.summaryPosition = group.start + 0.5;
const summaryData = createLine(0, toggleId);
summaryData.line.classList.add("json-block-summary");
summaryData.line.style.display = "none";
const indentText = line.match(/^\s*/)?.[0] || "";
summaryData.code.appendChild(document.createTextNode(indentText));
summaryData.code.appendChild(document.createTextNode("..."));
codeContainer.appendChild(summaryData.line);
group.summaryLine = summaryData.line;
group.toggles = [lineData.toggle, summaryData.toggle];
registry.set(toggleId, { type: "fold", group });
}
});
updateFoldVisibility(lineElements, uniqueGroups);
return { registry, lineElements, groups: uniqueGroups };
}
function detectLanguage(rawText, options) {
const enabled = options.enabledLanguages || {};
const contentType = (options.contentType || "").toLowerCase();
const allowAutoDetect = Boolean(options.allowAutoDetect);
let language = options.languageHint || null;
let parsedJson = null;
function isEnabled(name) {
return Boolean(enabled[name]);
}
if (language && !isEnabled(language)) {
language = null;
}
if (language === "json") {
try {
parsedJson = JSON.parse(rawText);
} catch (e) {
return { language: null, parsedJson: null };
}
return { language, parsedJson };
}
if (!language && isEnabled("json")) {
try {
parsedJson = JSON.parse(rawText);
language = "json";
return { language, parsedJson };
} catch (e) {
parsedJson = null;
}
}
if (!language) {
if (isEnabled("xml") && contentType.includes("xml")) {
language = "xml";
} else if (
isEnabled("yaml") &&
(contentType.includes("yaml") || contentType.includes("yml"))
) {
language = "yaml";
} else if (isEnabled("markdown") && contentType.includes("markdown")) {
language = "markdown";
} else if (
isEnabled("plaintext") &&
contentType.includes("text/plain")
) {
language = "plaintext";
}
}
if (
!language &&
allowAutoDetect &&
window.hljs &&
typeof hljs.highlightAuto === "function"
) {
const allowed = [];
if (isEnabled("markdown")) allowed.push("markdown");
if (isEnabled("plaintext")) allowed.push("plaintext");
if (isEnabled("xml")) allowed.push("xml");
if (isEnabled("yaml")) allowed.push("yaml");
if (allowed.length) {
const auto = hljs.highlightAuto(rawText, allowed);
if (auto && auto.language && allowed.includes(auto.language)) {
language = auto.language;
} else if (isEnabled("plaintext")) {
language = "plaintext";
}
}
}
return { language, parsedJson };
}
function linkifyInElement(root) {
const urlRegex = /(https?:\/\/[^\s"<>()\\]+)/g;
const targets = root.querySelectorAll(".hljs-string");
const elements = targets.length ? targets : [root];
elements.forEach((element) => {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null
);
const textNodes = [];
let node = walker.nextNode();
while (node) {
if (node.parentNode && node.parentNode.nodeName !== "A") {
textNodes.push(node);
}
node = walker.nextNode();
}
textNodes.forEach((textNode) => {
const text = textNode.nodeValue || "";
urlRegex.lastIndex = 0;
let match = urlRegex.exec(text);
if (!match) return;
const fragment = document.createDocumentFragment();
let lastIndex = 0;
while (match) {
if (match.index > lastIndex) {
fragment.appendChild(
document.createTextNode(text.slice(lastIndex, match.index))
);
}
const url = match[0];
const linkWrap = document.createElement("span");
linkWrap.className = "hljs-link";
const anchor = document.createElement("a");
anchor.className = "json-link";
anchor.href = url;
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.textContent = url;
linkWrap.appendChild(anchor);
fragment.appendChild(linkWrap);
lastIndex = match.index + url.length;
match = urlRegex.exec(text);
}
if (lastIndex < text.length) {
fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
}
textNode.replaceWith(fragment);
});
});
}
async function applyFormatting(rawText, options = {}) {
const enabledLanguages = options.enabledLanguages || { json: true };
const detection = detectLanguage(rawText, {
enabledLanguages,
languageHint: options.languageHint || null,
contentType: options.contentType || "",
allowAutoDetect: options.allowAutoDetect
});
if (!detection.language) {
console.warn("[format] no supported language detected");
return false;
}
document.body.innerHTML = "";
ensureBaseStyles();
const header = document.createElement("div");
header.className = "json-header";
const title = document.createElement("div");
title.className = "json-title";
title.textContent = "JSON Formatter";
const controls = document.createElement("div");
controls.className = "json-controls";
const modeWrap = document.createElement("label");
modeWrap.className = "json-control";
modeWrap.textContent = "Modus";
const modeSelect = document.createElement("select");
modeSelect.setAttribute("aria-label", "Modus");
[
{ value: "system", label: "Wie Chrome" },
{ value: "dark", label: "Dark" },
{ value: "light", label: "Light" }
].forEach((optionData) => {
const option = document.createElement("option");
option.value = optionData.value;
option.textContent = optionData.label;
modeSelect.appendChild(option);
});
modeWrap.appendChild(modeSelect);
const themeWrap = document.createElement("label");
themeWrap.className = "json-control";
themeWrap.textContent = "Theme";
const themeSelect = document.createElement("select");
themeSelect.setAttribute("aria-label", "Theme");
populateThemeSelect(themeSelect);
themeWrap.appendChild(themeSelect);
const lineWrap = document.createElement("label");
lineWrap.className = "json-control";
const lineToggle = document.createElement("input");
lineToggle.type = "checkbox";
lineToggle.setAttribute("aria-label", "Zeilennummern");
lineWrap.appendChild(lineToggle);
lineWrap.appendChild(document.createTextNode("Zeilennummern"));
controls.appendChild(modeWrap);
controls.appendChild(themeWrap);
controls.appendChild(lineWrap);
header.appendChild(title);
header.appendChild(controls);
const content = document.createElement("div");
content.className = "json-content";
const pre = document.createElement("pre");
const code = document.createElement("code");
code.className = `hljs language-${detection.language}`;
pre.appendChild(code);
content.appendChild(pre);
document.body.appendChild(header);
document.body.appendChild(content);
let registry = null;
let foldContext = null;
if (detection.language === "json") {
registry = new Map();
appendValue(code, detection.parsedJson, 0, null, false, registry);
} else {
foldContext = renderHighlightedContent(code, rawText, detection.language);
registry = foldContext.registry;
}
const state = await getThemeState();
modeSelect.value = state.themeMode;
const resolved = applyThemeState(state);
themeSelect.value = resolved === "dark" ? state.darkTheme : state.lightTheme;
lineToggle.checked = state.lineNumbers;
document.body.classList.toggle("show-line-numbers", state.lineNumbers);
modeSelect.addEventListener("change", () => {
state.themeMode = modeSelect.value;
const nextResolved = applyThemeState(state);
themeSelect.value =
nextResolved === "dark" ? state.darkTheme : state.lightTheme;
saveThemeState(state);
});
themeSelect.addEventListener("change", () => {
const resolvedMode = resolveMode(state.themeMode);
if (resolvedMode === "dark") {
state.darkTheme = themeSelect.value;
} else {
state.lightTheme = themeSelect.value;
}
applyThemeState(state);
saveThemeState(state);
});
lineToggle.addEventListener("change", () => {
state.lineNumbers = lineToggle.checked;
document.body.classList.toggle("show-line-numbers", state.lineNumbers);
refreshLineNumbers(content, state.lineNumbers);
saveThemeState(state);
});
linkifyInElement(code);
if (registry) {
registry.forEach((entry) => {
if (entry.type === "string") {
entry.fullHtml = entry.element.innerHTML;
if (entry.startCollapsed) {
applyStringCollapsed(entry, true);
} else {
updateToggleButtons([entry.toggle], entry.collapsed);
}
} else if (entry.type === "block") {
updateToggleButtons(entry.toggles, entry.collapsed);
} else if (entry.type === "fold") {
updateToggleButtons(entry.group.toggles, entry.group.collapsed);
}
});
}
refreshLineNumbers(content, state.lineNumbers);
if (registry) {
content.addEventListener("click", (event) => {
const toggle = event.target.closest(".json-toggle");
if (!toggle) return;
const entry = registry.get(toggle.dataset.toggleId);
if (!entry) return;
if (entry.type === "block") {
applyBlockCollapsed(entry, !entry.collapsed);
} else if (entry.type === "string") {
applyStringCollapsed(entry, !entry.collapsed);
} else if (entry.type === "fold") {
entry.group.collapsed = !entry.group.collapsed;
updateFoldVisibility(foldContext.lineElements, foldContext.groups);
}
refreshLineNumbers(content, state.lineNumbers);
});
}
return true;
}