1429 lines
47 KiB
JavaScript
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
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;
|
|
}
|