All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 6s
2382 lines
117 KiB
HTML
2382 lines
117 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ZDDC Table</title>
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZTNhNWYiLz4KICA8ZyBmaWxsPSIjZmZmIj4KICAgIDxyZWN0IHg9IjE0IiB5PSIxOCIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iNDMsMjUgNTAsMjUgMjEsNDMgMTQsNDMiLz4KICAgIDxyZWN0IHg9IjE0IiB5PSI0MyIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICA8L2c+Cjwvc3ZnPgo=">
|
||
<style>
|
||
/* ==========================================================================
|
||
ZDDC Shared Base — single source of truth for tokens and primitives
|
||
Included first by every tool's build.sh via ../shared/base.css
|
||
========================================================================== */
|
||
|
||
/* ── CSS custom properties ────────────────────────────────────────────────── */
|
||
:root {
|
||
/* Brand / accent (matches zddc.varasys.io website --accent) */
|
||
--primary: #2a5a8a;
|
||
--primary-hover: #1d4060;
|
||
--primary-active: #163352;
|
||
--primary-light: #e8f0f7;
|
||
|
||
/* Semantic colours */
|
||
--success: #28a745;
|
||
--warning: #d97706;
|
||
--danger: #dc3545;
|
||
--info: #17a2b8;
|
||
|
||
/* Backgrounds */
|
||
--bg: #ffffff;
|
||
--bg-secondary: #f8f9fa;
|
||
--bg-hover: #f0f4f8;
|
||
--bg-selected: var(--primary-light);
|
||
|
||
/* Text */
|
||
--text: #212529;
|
||
--text-muted: #6c757d;
|
||
--text-light: #ffffff;
|
||
|
||
/* Borders */
|
||
--border: #dee2e6;
|
||
--border-dark: #adb5bd;
|
||
|
||
/* Shape */
|
||
--radius: 4px;
|
||
|
||
/* Typography */
|
||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', 'Courier New', monospace;
|
||
}
|
||
|
||
/* ── Dark mode tokens ─────────────────────────────────────────────────────── */
|
||
/* Applied via: OS preference (auto) or [data-theme="dark"] on <html> */
|
||
/* The [data-theme="light"] selector locks light mode regardless of OS pref. */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
--primary: #4a90c4;
|
||
--primary-hover: #5ba3d9;
|
||
--primary-active: #6ab5e8;
|
||
--primary-light: #1a3550;
|
||
|
||
--bg: #1e1e1e;
|
||
--bg-secondary: #252526;
|
||
--bg-hover: #2d2d30;
|
||
--bg-selected: #1a3550;
|
||
|
||
--text: #d4d4d4;
|
||
--text-muted: #9d9d9d;
|
||
--text-light: #ffffff;
|
||
|
||
--border: #3e3e42;
|
||
--border-dark: #6e6e72;
|
||
}
|
||
}
|
||
|
||
/* Manual dark override — wins over media query */
|
||
[data-theme="dark"] {
|
||
--primary: #4a90c4;
|
||
--primary-hover: #5ba3d9;
|
||
--primary-active: #6ab5e8;
|
||
--primary-light: #1a3550;
|
||
|
||
--bg: #1e1e1e;
|
||
--bg-secondary: #252526;
|
||
--bg-hover: #2d2d30;
|
||
--bg-selected: #1a3550;
|
||
|
||
--text: #d4d4d4;
|
||
--text-muted: #9d9d9d;
|
||
--text-light: #ffffff;
|
||
|
||
--border: #3e3e42;
|
||
--border-dark: #6e6e72;
|
||
}
|
||
|
||
/* ── Reset ────────────────────────────────────────────────────────────────── */
|
||
*, *::before, *::after {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
/* ── Base document ────────────────────────────────────────────────────────── */
|
||
html, body {
|
||
height: 100%;
|
||
font-family: var(--font);
|
||
font-size: 16px;
|
||
line-height: 1.5;
|
||
color: var(--text);
|
||
background-color: var(--bg-secondary);
|
||
}
|
||
|
||
/* ── Typography ───────────────────────────────────────────────────────────── */
|
||
h1, h2, h3, h4, h5, h6 {
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
a {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* ── Utility ──────────────────────────────────────────────────────────────── */
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
.truncate {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* ── Scrollbars (webkit) ──────────────────────────────────────────────────── */
|
||
::-webkit-scrollbar {
|
||
width: 7px;
|
||
height: 7px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #a0a0a0;
|
||
}
|
||
|
||
/* ── Button primitive ─────────────────────────────────────────────────────── */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
padding: 0.4rem 0.85rem;
|
||
font-family: var(--font);
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
text-align: center;
|
||
text-decoration: none;
|
||
white-space: nowrap;
|
||
vertical-align: middle;
|
||
cursor: pointer;
|
||
border: 1px solid transparent;
|
||
border-radius: var(--radius);
|
||
transition: background 0.15s, box-shadow 0.15s, border-color 0.15s, color 0.15s;
|
||
background: var(--bg-secondary);
|
||
color: var(--text);
|
||
}
|
||
|
||
.btn:disabled,
|
||
.btn[disabled] {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn:not(:disabled):hover {
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.btn:not(:disabled):active {
|
||
box-shadow: none;
|
||
}
|
||
|
||
/* Variants */
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
color: var(--text-light);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.btn-primary:not(:disabled):hover {
|
||
background: var(--primary-hover);
|
||
border-color: var(--primary-hover);
|
||
color: var(--text-light);
|
||
}
|
||
|
||
.btn-primary:not(:disabled):active {
|
||
background: var(--primary-active);
|
||
border-color: var(--primary-active);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
border-color: var(--border);
|
||
}
|
||
|
||
.btn-secondary:not(:disabled):hover {
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
/* Subdued / de-emphasized variant.
|
||
Used on the "Add Local Directory" button when a tool is operating
|
||
in server (online) mode — the local-dir affordance is still
|
||
available but visually quieter, since the typical user already
|
||
has the directory loaded from the server. */
|
||
.btn.btn--subtle {
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
border-color: var(--border);
|
||
box-shadow: none;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.btn.btn--subtle:not(:disabled):hover {
|
||
color: var(--text);
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.btn-success {
|
||
background: var(--success);
|
||
color: var(--text-light);
|
||
border-color: var(--success);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: var(--danger);
|
||
color: var(--text-light);
|
||
border-color: var(--danger);
|
||
}
|
||
|
||
/* Sizes */
|
||
.btn-sm {
|
||
padding: 0.25rem 0.5rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.btn-lg {
|
||
padding: 0.6rem 1.4rem;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.btn-link {
|
||
background: transparent;
|
||
border-color: transparent;
|
||
color: var(--primary);
|
||
padding-left: 0;
|
||
padding-right: 0;
|
||
}
|
||
|
||
.btn-link:not(:disabled):hover {
|
||
text-decoration: underline;
|
||
box-shadow: none;
|
||
}
|
||
|
||
/* ── App header chrome ────────────────────────────────────────────────────── */
|
||
.app-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.35rem 1rem;
|
||
background: var(--bg-secondary);
|
||
border-bottom: 1px solid var(--border);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Tool name inside the header */
|
||
.app-header__title {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
letter-spacing: 0.01em;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Brand logo — sits left of the title in every tool's app-header.
|
||
Self-contained: the SVG provides its own dark blue rounded background,
|
||
so no extra wrapper styling is needed. */
|
||
.app-header__logo {
|
||
width: 26px;
|
||
height: 26px;
|
||
flex-shrink: 0;
|
||
display: block;
|
||
}
|
||
|
||
/* ── Build timestamp ──────────────────────────────────────────────────────── */
|
||
.build-timestamp {
|
||
font-size: 0.55rem;
|
||
color: var(--text-muted);
|
||
opacity: 0.7;
|
||
font-weight: 300;
|
||
white-space: nowrap;
|
||
padding-top: 0.15rem;
|
||
}
|
||
|
||
/* Title + timestamp stacked vertically on the left side of the header */
|
||
.header-title-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* ── Icon buttons (help, theme, refresh) ─────────────────────────────────── */
|
||
/* Square, centered — overrides the asymmetric text-button padding/line-height */
|
||
#help-btn,
|
||
#theme-btn,
|
||
#refreshHeaderBtn {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
padding: 0;
|
||
line-height: 1;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* Toast CSS lives in classifier/css/base.css — only that tool uses toasts. */
|
||
|
||
/* ── Theme and help icon buttons ─────────────────────────────────────────── */
|
||
#theme-btn,
|
||
#help-btn {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* ── Help panel (shared slide-out drawer) ─────────────────────────────────── */
|
||
/* Used by all four tools. Toggle open/close via shared/help.js. */
|
||
|
||
.help-panel {
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
width: min(420px, 85vw);
|
||
height: 100vh;
|
||
z-index: 1000;
|
||
background: var(--bg);
|
||
border-left: 1px solid var(--border);
|
||
box-shadow: -2px 0 12px rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
transform: translateX(100%);
|
||
transition: transform 0.25s ease;
|
||
}
|
||
|
||
.help-panel:not([hidden]) {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.help-panel[hidden] {
|
||
display: flex;
|
||
transform: translateX(100%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.help-panel__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.75rem 1rem;
|
||
border-bottom: 1px solid var(--border);
|
||
flex-shrink: 0;
|
||
background: var(--bg);
|
||
}
|
||
|
||
.help-panel__title {
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin: 0;
|
||
}
|
||
|
||
.help-panel__close {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
font-size: 1.35rem;
|
||
cursor: pointer;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: var(--radius);
|
||
line-height: 1;
|
||
transition: background 0.15s, color 0.15s;
|
||
}
|
||
|
||
.help-panel__close:hover {
|
||
color: var(--text);
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.help-panel__body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1rem 1rem 2rem;
|
||
font-size: 0.85rem;
|
||
line-height: 1.6;
|
||
color: var(--text);
|
||
}
|
||
|
||
.help-panel__body h3 {
|
||
font-size: 0.95rem;
|
||
font-weight: 700;
|
||
margin: 1.25rem 0 0.35rem;
|
||
color: var(--text);
|
||
border-bottom: 1px solid var(--border);
|
||
padding-bottom: 0.15rem;
|
||
}
|
||
|
||
.help-panel__body h3:first-child {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.help-panel__body h4 {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
margin: 1.25rem 0 0.3rem;
|
||
padding-left: 0.5rem;
|
||
border-left: 3px solid var(--border-dark);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.help-panel__body p {
|
||
margin: 0 0 0.5rem;
|
||
}
|
||
|
||
.help-panel__body ol,
|
||
.help-panel__body ul {
|
||
padding-left: 1.5rem;
|
||
margin: 0.3rem 0 0.5rem;
|
||
}
|
||
|
||
.help-panel__body li {
|
||
margin-bottom: 0.3rem;
|
||
}
|
||
|
||
.help-panel__body dl {
|
||
margin: 0.3rem 0;
|
||
}
|
||
|
||
.help-panel__body dt {
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.help-panel__body dd {
|
||
margin: 0 0 0.5rem 1rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.help-panel__body code {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.8em;
|
||
background: var(--bg-secondary);
|
||
padding: 0.1em 0.3em;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.help-badge {
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
padding: 0.1rem 0.35rem;
|
||
border-radius: var(--radius);
|
||
vertical-align: middle;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.help-badge--draft {
|
||
color: #2563eb;
|
||
background: #eff6ff;
|
||
}
|
||
|
||
.help-badge--published {
|
||
color: #7c3aed;
|
||
background: #f5f3ff;
|
||
}
|
||
|
||
/* Shrink main content when help panel is open */
|
||
body.help-open .app-header {
|
||
margin-right: min(420px, 85vw);
|
||
}
|
||
|
||
/* ── Column filter inputs (shared across archive, classifier, transmittal) ─── */
|
||
.column-filter {
|
||
display: block;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
margin-top: 0.25rem;
|
||
padding: 0.2rem 0.4rem;
|
||
font-size: 0.8rem;
|
||
font-family: var(--font);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
transition: border-color 0.15s;
|
||
}
|
||
|
||
.column-filter:focus {
|
||
border-color: var(--primary);
|
||
outline: none;
|
||
box-shadow: 0 0 0 1px rgba(42, 90, 138, 0.35);
|
||
}
|
||
|
||
.column-filter::placeholder {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* tables/ — directory-of-YAML table view. Reuses tokens from shared/base.css. */
|
||
|
||
.table-main {
|
||
padding: var(--spacing-md);
|
||
max-width: 100%;
|
||
}
|
||
|
||
.table-description {
|
||
margin: 0 0 var(--spacing-md);
|
||
color: var(--color-text-muted);
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.table-status {
|
||
margin: 0 0 var(--spacing-md);
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
background: var(--color-bg-warning, #fff8e6);
|
||
border: 1px solid var(--color-border, #d6cfa3);
|
||
border-radius: var(--radius-sm, 4px);
|
||
}
|
||
|
||
.table-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: var(--spacing-md);
|
||
margin: 0 0 var(--spacing-sm);
|
||
}
|
||
|
||
.table-toolbar__left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
|
||
.table-rowcount {
|
||
color: var(--color-text-muted);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.table-scroll {
|
||
overflow: auto;
|
||
max-height: calc(100vh - 200px);
|
||
border: 1px solid var(--color-border, #d8d8d8);
|
||
border-radius: var(--radius-sm, 4px);
|
||
}
|
||
|
||
.zddc-table {
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.zddc-table thead {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 2;
|
||
background: var(--color-bg-elevated, #f5f5f5);
|
||
}
|
||
|
||
.zddc-table__title-row .zddc-table__th {
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
text-align: left;
|
||
font-weight: 600;
|
||
border-bottom: 1px solid var(--color-border, #d8d8d8);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.zddc-table__title-row .zddc-table__th:hover {
|
||
background: var(--color-bg-hover, rgba(0, 0, 0, 0.04));
|
||
}
|
||
|
||
.zddc-table__filter-row .zddc-table__filter-cell {
|
||
padding: 4px var(--spacing-sm);
|
||
border-bottom: 1px solid var(--color-border, #d8d8d8);
|
||
background: var(--color-bg-elevated, #f5f5f5);
|
||
}
|
||
|
||
.zddc-table__filter-text,
|
||
.zddc-table__filter-enum {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 2px 4px;
|
||
font-size: 0.85rem;
|
||
border: 1px solid var(--color-border, #d0d0d0);
|
||
border-radius: 3px;
|
||
background: var(--color-bg, #fff);
|
||
color: var(--color-text, #111);
|
||
}
|
||
|
||
.zddc-table__filter-enum {
|
||
min-height: 1.8em;
|
||
}
|
||
|
||
.zddc-table__row:nth-child(even) {
|
||
background: var(--color-bg-zebra, rgba(0, 0, 0, 0.02));
|
||
}
|
||
|
||
.zddc-table__row--editable {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.zddc-table__row--editable:hover {
|
||
background: var(--color-bg-hover, rgba(50, 100, 200, 0.08));
|
||
}
|
||
|
||
.zddc-table__row--readonly {
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
.zddc-table__cell {
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
border-bottom: 1px solid var(--color-border-soft, rgba(0, 0, 0, 0.06));
|
||
vertical-align: top;
|
||
}
|
||
|
||
.table-empty {
|
||
padding: var(--spacing-lg) var(--spacing-md);
|
||
text-align: center;
|
||
color: var(--color-text-muted);
|
||
font-style: italic;
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="app-header">
|
||
<div class="header-left">
|
||
<svg class="app-header__logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true">
|
||
<rect width="64" height="64" rx="12" fill="#1e3a5f"/>
|
||
<g fill="#fff">
|
||
<rect x="14" y="18" width="36" height="7"/>
|
||
<polygon points="43,25 50,25 21,43 14,43"/>
|
||
<rect x="14" y="43" width="36" height="7"/>
|
||
</g>
|
||
</svg>
|
||
<div class="header-title-group">
|
||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-07</span></span>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
||
<button id="help-btn" class="btn btn-secondary" title="Help" aria-label="Help">?</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="table-main">
|
||
<div id="table-description" class="table-description" hidden></div>
|
||
<div id="table-status" class="table-status" hidden></div>
|
||
<div class="table-toolbar" id="table-toolbar">
|
||
<div class="table-toolbar__left">
|
||
<span id="table-rowcount" class="table-rowcount" aria-live="polite"></span>
|
||
<button type="button" id="table-clear-filters" class="btn btn-secondary btn-sm" hidden>Clear filters</button>
|
||
</div>
|
||
</div>
|
||
<div class="table-scroll">
|
||
<table id="table-root" class="zddc-table" aria-describedby="table-description">
|
||
<thead></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
<div id="table-empty" class="table-empty" hidden>No rows match the current filters.</div>
|
||
</main>
|
||
|
||
<!-- Help Panel -->
|
||
<aside id="help-panel" class="help-panel" hidden aria-labelledby="help-panel-title">
|
||
<div class="help-panel__header">
|
||
<h2 id="help-panel-title" class="help-panel__title">Help — ZDDC Table</h2>
|
||
<button type="button" class="help-panel__close" id="help-panel-close" aria-label="Close">×</button>
|
||
</div>
|
||
<div class="help-panel__body">
|
||
<h3>What is this table?</h3>
|
||
<p>Each row in this table is one YAML file in the source directory.
|
||
Tables are declared in <code>.zddc</code> via a
|
||
<code>tables:</code> map. The columns and row schema come from
|
||
a <code>*.table.yaml</code> spec file.</p>
|
||
|
||
<h3>Sorting</h3>
|
||
<p>Click a column header to sort by that column. Click again to
|
||
toggle direction. Shift-click another header to add a secondary
|
||
sort key.</p>
|
||
|
||
<h3>Filtering</h3>
|
||
<p>Type in the box under a column header to filter rows whose
|
||
value contains your text (case-insensitive). For columns with a
|
||
fixed enum, the box becomes a multi-select — leave it empty to
|
||
show every value.</p>
|
||
|
||
<h3>Editing a row</h3>
|
||
<p>Click a row to open its YAML in the form editor. Whether the
|
||
row is editable depends on the cascading <code>.zddc</code>
|
||
permissions for the row's path. Rows in <code>Issued</code> or
|
||
<code>Received</code> archives are read-only by design (WORM).</p>
|
||
|
||
<h3>Header buttons</h3>
|
||
<dl>
|
||
<dt>◐ Theme</dt>
|
||
<dd>Cycle auto / light / dark.</dd>
|
||
<dt>? Help</dt>
|
||
<dd>This panel. Press <kbd>Esc</kbd> to close.</dd>
|
||
</dl>
|
||
</div>
|
||
</aside>
|
||
|
||
<!--
|
||
Server injects the table context here on render. Shape:
|
||
{
|
||
"title": "Optional page title override",
|
||
"description": "Optional description shown above the table",
|
||
"columns": [{field, title, width?, format?, filter?, sort?, enum?}],
|
||
"rows": [{url, data, editable}],
|
||
"defaults": {sort?: [{field, dir}], filter?: {field: value}}
|
||
}
|
||
-->
|
||
<script id="table-context" type="application/json">{}</script>
|
||
|
||
<script>
|
||
/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */
|
||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).jsyaml={})}(this,(function(e){"use strict";function t(e){return null==e}var n={isNothing:t,isObject:function(e){return"object"==typeof e&&null!==e},toArray:function(e){return Array.isArray(e)?e:t(e)?[]:[e]},repeat:function(e,t){var n,i="";for(n=0;n<t;n+=1)i+=e;return i},isNegativeZero:function(e){return 0===e&&Number.NEGATIVE_INFINITY===1/e},extend:function(e,t){var n,i,r,o;if(t)for(n=0,i=(o=Object.keys(t)).length;n<i;n+=1)e[r=o[n]]=t[r];return e}};function i(e,t){var n="",i=e.reason||"(unknown reason)";return e.mark?(e.mark.name&&(n+='in "'+e.mark.name+'" '),n+="("+(e.mark.line+1)+":"+(e.mark.column+1)+")",!t&&e.mark.snippet&&(n+="\n\n"+e.mark.snippet),i+" "+n):i}function r(e,t){Error.call(this),this.name="YAMLException",this.reason=e,this.mark=t,this.message=i(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||""}r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r.prototype.toString=function(e){return this.name+": "+i(this,e)};var o=r;function a(e,t,n,i,r){var o="",a="",l=Math.floor(r/2)-1;return i-t>l&&(t=i-l+(o=" ... ").length),n-i>l&&(n=i+l-(a=" ...").length),{str:o+e.slice(t,n).replace(/\t/g,"→")+a,pos:i-t+o.length}}function l(e,t){return n.repeat(" ",t-e.length)+e}var c=function(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),"number"!=typeof t.indent&&(t.indent=1),"number"!=typeof t.linesBefore&&(t.linesBefore=3),"number"!=typeof t.linesAfter&&(t.linesAfter=2);for(var i,r=/\r?\n|\r|\0/g,o=[0],c=[],s=-1;i=r.exec(e.buffer);)c.push(i.index),o.push(i.index+i[0].length),e.position<=i.index&&s<0&&(s=o.length-2);s<0&&(s=o.length-1);var u,p,f="",d=Math.min(e.line+t.linesAfter,c.length).toString().length,h=t.maxLength-(t.indent+d+3);for(u=1;u<=t.linesBefore&&!(s-u<0);u++)p=a(e.buffer,o[s-u],c[s-u],e.position-(o[s]-o[s-u]),h),f=n.repeat(" ",t.indent)+l((e.line-u+1).toString(),d)+" | "+p.str+"\n"+f;for(p=a(e.buffer,o[s],c[s],e.position,h),f+=n.repeat(" ",t.indent)+l((e.line+1).toString(),d)+" | "+p.str+"\n",f+=n.repeat("-",t.indent+d+3+p.pos)+"^\n",u=1;u<=t.linesAfter&&!(s+u>=c.length);u++)p=a(e.buffer,o[s+u],c[s+u],e.position-(o[s]-o[s+u]),h),f+=n.repeat(" ",t.indent)+l((e.line+u+1).toString(),d)+" | "+p.str+"\n";return f.replace(/\n$/,"")},s=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],u=["scalar","sequence","mapping"];var p=function(e,t){if(t=t||{},Object.keys(t).forEach((function(t){if(-1===s.indexOf(t))throw new o('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')})),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=function(e){var t={};return null!==e&&Object.keys(e).forEach((function(n){e[n].forEach((function(e){t[String(e)]=n}))})),t}(t.styleAliases||null),-1===u.indexOf(this.kind))throw new o('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')};function f(e,t){var n=[];return e[t].forEach((function(e){var t=n.length;n.forEach((function(n,i){n.tag===e.tag&&n.kind===e.kind&&n.multi===e.multi&&(t=i)})),n[t]=e})),n}function d(e){return this.extend(e)}d.prototype.extend=function(e){var t=[],n=[];if(e instanceof p)n.push(e);else if(Array.isArray(e))n=n.concat(e);else{if(!e||!Array.isArray(e.implicit)&&!Array.isArray(e.explicit))throw new o("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(n=n.concat(e.explicit))}t.forEach((function(e){if(!(e instanceof p))throw new o("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(e.loadKind&&"scalar"!==e.loadKind)throw new o("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(e.multi)throw new o("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")})),n.forEach((function(e){if(!(e instanceof p))throw new o("Specified list of YAML types (or a single Type object) contains a non-Type object.")}));var i=Object.create(d.prototype);return i.implicit=(this.implicit||[]).concat(t),i.explicit=(this.explicit||[]).concat(n),i.compiledImplicit=f(i,"implicit"),i.compiledExplicit=f(i,"explicit"),i.compiledTypeMap=function(){var e,t,n={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function i(e){e.multi?(n.multi[e.kind].push(e),n.multi.fallback.push(e)):n[e.kind][e.tag]=n.fallback[e.tag]=e}for(e=0,t=arguments.length;e<t;e+=1)arguments[e].forEach(i);return n}(i.compiledImplicit,i.compiledExplicit),i};var h=d,g=new p("tag:yaml.org,2002:str",{kind:"scalar",construct:function(e){return null!==e?e:""}}),m=new p("tag:yaml.org,2002:seq",{kind:"sequence",construct:function(e){return null!==e?e:[]}}),y=new p("tag:yaml.org,2002:map",{kind:"mapping",construct:function(e){return null!==e?e:{}}}),b=new h({explicit:[g,m,y]});var A=new p("tag:yaml.org,2002:null",{kind:"scalar",resolve:function(e){if(null===e)return!0;var t=e.length;return 1===t&&"~"===e||4===t&&("null"===e||"Null"===e||"NULL"===e)},construct:function(){return null},predicate:function(e){return null===e},represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"},empty:function(){return""}},defaultStyle:"lowercase"});var v=new p("tag:yaml.org,2002:bool",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t=e.length;return 4===t&&("true"===e||"True"===e||"TRUE"===e)||5===t&&("false"===e||"False"===e||"FALSE"===e)},construct:function(e){return"true"===e||"True"===e||"TRUE"===e},predicate:function(e){return"[object Boolean]"===Object.prototype.toString.call(e)},represent:{lowercase:function(e){return e?"true":"false"},uppercase:function(e){return e?"TRUE":"FALSE"},camelcase:function(e){return e?"True":"False"}},defaultStyle:"lowercase"});function w(e){return 48<=e&&e<=55}function k(e){return 48<=e&&e<=57}var C=new p("tag:yaml.org,2002:int",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,n,i=e.length,r=0,o=!1;if(!i)return!1;if("-"!==(t=e[r])&&"+"!==t||(t=e[++r]),"0"===t){if(r+1===i)return!0;if("b"===(t=e[++r])){for(r++;r<i;r++)if("_"!==(t=e[r])){if("0"!==t&&"1"!==t)return!1;o=!0}return o&&"_"!==t}if("x"===t){for(r++;r<i;r++)if("_"!==(t=e[r])){if(!(48<=(n=e.charCodeAt(r))&&n<=57||65<=n&&n<=70||97<=n&&n<=102))return!1;o=!0}return o&&"_"!==t}if("o"===t){for(r++;r<i;r++)if("_"!==(t=e[r])){if(!w(e.charCodeAt(r)))return!1;o=!0}return o&&"_"!==t}}if("_"===t)return!1;for(;r<i;r++)if("_"!==(t=e[r])){if(!k(e.charCodeAt(r)))return!1;o=!0}return!(!o||"_"===t)},construct:function(e){var t,n=e,i=1;if(-1!==n.indexOf("_")&&(n=n.replace(/_/g,"")),"-"!==(t=n[0])&&"+"!==t||("-"===t&&(i=-1),t=(n=n.slice(1))[0]),"0"===n)return 0;if("0"===t){if("b"===n[1])return i*parseInt(n.slice(2),2);if("x"===n[1])return i*parseInt(n.slice(2),16);if("o"===n[1])return i*parseInt(n.slice(2),8)}return i*parseInt(n,10)},predicate:function(e){return"[object Number]"===Object.prototype.toString.call(e)&&e%1==0&&!n.isNegativeZero(e)},represent:{binary:function(e){return e>=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0o"+e.toString(8):"-0o"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),x=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");var I=/^[-+]?[0-9]+e/;var S=new p("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(e){return null!==e&&!(!x.test(e)||"_"===e[e.length-1])},construct:function(e){var t,n;return n="-"===(t=e.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(t[0])>=0&&(t=t.slice(1)),".inf"===t?1===n?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===t?NaN:n*parseFloat(t,10)},predicate:function(e){return"[object Number]"===Object.prototype.toString.call(e)&&(e%1!=0||n.isNegativeZero(e))},represent:function(e,t){var i;if(isNaN(e))switch(t){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===e)switch(t){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(n.isNegativeZero(e))return"-0.0";return i=e.toString(10),I.test(i)?i.replace("e",".e"):i},defaultStyle:"lowercase"}),O=b.extend({implicit:[A,v,C,S]}),j=O,T=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),N=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");var F=new p("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function(e){return null!==e&&(null!==T.exec(e)||null!==N.exec(e))},construct:function(e){var t,n,i,r,o,a,l,c,s=0,u=null;if(null===(t=T.exec(e))&&(t=N.exec(e)),null===t)throw new Error("Date resolve error");if(n=+t[1],i=+t[2]-1,r=+t[3],!t[4])return new Date(Date.UTC(n,i,r));if(o=+t[4],a=+t[5],l=+t[6],t[7]){for(s=t[7].slice(0,3);s.length<3;)s+="0";s=+s}return t[9]&&(u=6e4*(60*+t[10]+ +(t[11]||0)),"-"===t[9]&&(u=-u)),c=new Date(Date.UTC(n,i,r,o,a,l,s)),u&&c.setTime(c.getTime()-u),c},instanceOf:Date,represent:function(e){return e.toISOString()}});var E=new p("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(e){return"<<"===e||null===e}}),M="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r";var L=new p("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,n,i=0,r=e.length,o=M;for(n=0;n<r;n++)if(!((t=o.indexOf(e.charAt(n)))>64)){if(t<0)return!1;i+=6}return i%8==0},construct:function(e){var t,n,i=e.replace(/[\r\n=]/g,""),r=i.length,o=M,a=0,l=[];for(t=0;t<r;t++)t%4==0&&t&&(l.push(a>>16&255),l.push(a>>8&255),l.push(255&a)),a=a<<6|o.indexOf(i.charAt(t));return 0===(n=r%4*6)?(l.push(a>>16&255),l.push(a>>8&255),l.push(255&a)):18===n?(l.push(a>>10&255),l.push(a>>2&255)):12===n&&l.push(a>>4&255),new Uint8Array(l)},predicate:function(e){return"[object Uint8Array]"===Object.prototype.toString.call(e)},represent:function(e){var t,n,i="",r=0,o=e.length,a=M;for(t=0;t<o;t++)t%3==0&&t&&(i+=a[r>>18&63],i+=a[r>>12&63],i+=a[r>>6&63],i+=a[63&r]),r=(r<<8)+e[t];return 0===(n=o%3)?(i+=a[r>>18&63],i+=a[r>>12&63],i+=a[r>>6&63],i+=a[63&r]):2===n?(i+=a[r>>10&63],i+=a[r>>4&63],i+=a[r<<2&63],i+=a[64]):1===n&&(i+=a[r>>2&63],i+=a[r<<4&63],i+=a[64],i+=a[64]),i}}),_=Object.prototype.hasOwnProperty,D=Object.prototype.toString;var U=new p("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(e){if(null===e)return!0;var t,n,i,r,o,a=[],l=e;for(t=0,n=l.length;t<n;t+=1){if(i=l[t],o=!1,"[object Object]"!==D.call(i))return!1;for(r in i)if(_.call(i,r)){if(o)return!1;o=!0}if(!o)return!1;if(-1!==a.indexOf(r))return!1;a.push(r)}return!0},construct:function(e){return null!==e?e:[]}}),q=Object.prototype.toString;var Y=new p("tag:yaml.org,2002:pairs",{kind:"sequence",resolve:function(e){if(null===e)return!0;var t,n,i,r,o,a=e;for(o=new Array(a.length),t=0,n=a.length;t<n;t+=1){if(i=a[t],"[object Object]"!==q.call(i))return!1;if(1!==(r=Object.keys(i)).length)return!1;o[t]=[r[0],i[r[0]]]}return!0},construct:function(e){if(null===e)return[];var t,n,i,r,o,a=e;for(o=new Array(a.length),t=0,n=a.length;t<n;t+=1)i=a[t],r=Object.keys(i),o[t]=[r[0],i[r[0]]];return o}}),R=Object.prototype.hasOwnProperty;var B=new p("tag:yaml.org,2002:set",{kind:"mapping",resolve:function(e){if(null===e)return!0;var t,n=e;for(t in n)if(R.call(n,t)&&null!==n[t])return!1;return!0},construct:function(e){return null!==e?e:{}}}),K=j.extend({implicit:[F,E],explicit:[L,U,Y,B]}),P=Object.prototype.hasOwnProperty,W=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,H=/[\x85\u2028\u2029]/,$=/[,\[\]\{\}]/,G=/^(?:!|!!|![a-z\-]+!)$/i,V=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;function Z(e){return Object.prototype.toString.call(e)}function J(e){return 10===e||13===e}function Q(e){return 9===e||32===e}function z(e){return 9===e||32===e||10===e||13===e}function X(e){return 44===e||91===e||93===e||123===e||125===e}function ee(e){var t;return 48<=e&&e<=57?e-48:97<=(t=32|e)&&t<=102?t-97+10:-1}function te(e){return 48===e?"\0":97===e?"":98===e?"\b":116===e||9===e?"\t":110===e?"\n":118===e?"\v":102===e?"\f":114===e?"\r":101===e?"":32===e?" ":34===e?'"':47===e?"/":92===e?"\\":78===e?"
":95===e?" ":76===e?"\u2028":80===e?"\u2029":""}function ne(e){return e<=65535?String.fromCharCode(e):String.fromCharCode(55296+(e-65536>>10),56320+(e-65536&1023))}for(var ie=new Array(256),re=new Array(256),oe=0;oe<256;oe++)ie[oe]=te(oe)?1:0,re[oe]=te(oe);function ae(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||K,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function le(e,t){var n={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return n.snippet=c(n),new o(t,n)}function ce(e,t){throw le(e,t)}function se(e,t){e.onWarning&&e.onWarning.call(null,le(e,t))}var ue={YAML:function(e,t,n){var i,r,o;null!==e.version&&ce(e,"duplication of %YAML directive"),1!==n.length&&ce(e,"YAML directive accepts exactly one argument"),null===(i=/^([0-9]+)\.([0-9]+)$/.exec(n[0]))&&ce(e,"ill-formed argument of the YAML directive"),r=parseInt(i[1],10),o=parseInt(i[2],10),1!==r&&ce(e,"unacceptable YAML version of the document"),e.version=n[0],e.checkLineBreaks=o<2,1!==o&&2!==o&&se(e,"unsupported YAML version of the document")},TAG:function(e,t,n){var i,r;2!==n.length&&ce(e,"TAG directive accepts exactly two arguments"),i=n[0],r=n[1],G.test(i)||ce(e,"ill-formed tag handle (first argument) of the TAG directive"),P.call(e.tagMap,i)&&ce(e,'there is a previously declared suffix for "'+i+'" tag handle'),V.test(r)||ce(e,"ill-formed tag prefix (second argument) of the TAG directive");try{r=decodeURIComponent(r)}catch(t){ce(e,"tag prefix is malformed: "+r)}e.tagMap[i]=r}};function pe(e,t,n,i){var r,o,a,l;if(t<n){if(l=e.input.slice(t,n),i)for(r=0,o=l.length;r<o;r+=1)9===(a=l.charCodeAt(r))||32<=a&&a<=1114111||ce(e,"expected valid JSON character");else W.test(l)&&ce(e,"the stream contains non-printable characters");e.result+=l}}function fe(e,t,i,r){var o,a,l,c;for(n.isObject(i)||ce(e,"cannot merge mappings; the provided source object is unacceptable"),l=0,c=(o=Object.keys(i)).length;l<c;l+=1)a=o[l],P.call(t,a)||(t[a]=i[a],r[a]=!0)}function de(e,t,n,i,r,o,a,l,c){var s,u;if(Array.isArray(r))for(s=0,u=(r=Array.prototype.slice.call(r)).length;s<u;s+=1)Array.isArray(r[s])&&ce(e,"nested arrays are not supported inside keys"),"object"==typeof r&&"[object Object]"===Z(r[s])&&(r[s]="[object Object]");if("object"==typeof r&&"[object Object]"===Z(r)&&(r="[object Object]"),r=String(r),null===t&&(t={}),"tag:yaml.org,2002:merge"===i)if(Array.isArray(o))for(s=0,u=o.length;s<u;s+=1)fe(e,t,o[s],n);else fe(e,t,o,n);else e.json||P.call(n,r)||!P.call(t,r)||(e.line=a||e.line,e.lineStart=l||e.lineStart,e.position=c||e.position,ce(e,"duplicated mapping key")),"__proto__"===r?Object.defineProperty(t,r,{configurable:!0,enumerable:!0,writable:!0,value:o}):t[r]=o,delete n[r];return t}function he(e){var t;10===(t=e.input.charCodeAt(e.position))?e.position++:13===t?(e.position++,10===e.input.charCodeAt(e.position)&&e.position++):ce(e,"a line break is expected"),e.line+=1,e.lineStart=e.position,e.firstTabInLine=-1}function ge(e,t,n){for(var i=0,r=e.input.charCodeAt(e.position);0!==r;){for(;Q(r);)9===r&&-1===e.firstTabInLine&&(e.firstTabInLine=e.position),r=e.input.charCodeAt(++e.position);if(t&&35===r)do{r=e.input.charCodeAt(++e.position)}while(10!==r&&13!==r&&0!==r);if(!J(r))break;for(he(e),r=e.input.charCodeAt(e.position),i++,e.lineIndent=0;32===r;)e.lineIndent++,r=e.input.charCodeAt(++e.position)}return-1!==n&&0!==i&&e.lineIndent<n&&se(e,"deficient indentation"),i}function me(e){var t,n=e.position;return!(45!==(t=e.input.charCodeAt(n))&&46!==t||t!==e.input.charCodeAt(n+1)||t!==e.input.charCodeAt(n+2)||(n+=3,0!==(t=e.input.charCodeAt(n))&&!z(t)))}function ye(e,t){1===t?e.result+=" ":t>1&&(e.result+=n.repeat("\n",t-1))}function be(e,t){var n,i,r=e.tag,o=e.anchor,a=[],l=!1;if(-1!==e.firstTabInLine)return!1;for(null!==e.anchor&&(e.anchorMap[e.anchor]=a),i=e.input.charCodeAt(e.position);0!==i&&(-1!==e.firstTabInLine&&(e.position=e.firstTabInLine,ce(e,"tab characters must not be used in indentation")),45===i)&&z(e.input.charCodeAt(e.position+1));)if(l=!0,e.position++,ge(e,!0,-1)&&e.lineIndent<=t)a.push(null),i=e.input.charCodeAt(e.position);else if(n=e.line,we(e,t,3,!1,!0),a.push(e.result),ge(e,!0,-1),i=e.input.charCodeAt(e.position),(e.line===n||e.lineIndent>t)&&0!==i)ce(e,"bad indentation of a sequence entry");else if(e.lineIndent<t)break;return!!l&&(e.tag=r,e.anchor=o,e.kind="sequence",e.result=a,!0)}function Ae(e){var t,n,i,r,o=!1,a=!1;if(33!==(r=e.input.charCodeAt(e.position)))return!1;if(null!==e.tag&&ce(e,"duplication of a tag property"),60===(r=e.input.charCodeAt(++e.position))?(o=!0,r=e.input.charCodeAt(++e.position)):33===r?(a=!0,n="!!",r=e.input.charCodeAt(++e.position)):n="!",t=e.position,o){do{r=e.input.charCodeAt(++e.position)}while(0!==r&&62!==r);e.position<e.length?(i=e.input.slice(t,e.position),r=e.input.charCodeAt(++e.position)):ce(e,"unexpected end of the stream within a verbatim tag")}else{for(;0!==r&&!z(r);)33===r&&(a?ce(e,"tag suffix cannot contain exclamation marks"):(n=e.input.slice(t-1,e.position+1),G.test(n)||ce(e,"named tag handle cannot contain such characters"),a=!0,t=e.position+1)),r=e.input.charCodeAt(++e.position);i=e.input.slice(t,e.position),$.test(i)&&ce(e,"tag suffix cannot contain flow indicator characters")}i&&!V.test(i)&&ce(e,"tag name cannot contain such characters: "+i);try{i=decodeURIComponent(i)}catch(t){ce(e,"tag name is malformed: "+i)}return o?e.tag=i:P.call(e.tagMap,n)?e.tag=e.tagMap[n]+i:"!"===n?e.tag="!"+i:"!!"===n?e.tag="tag:yaml.org,2002:"+i:ce(e,'undeclared tag handle "'+n+'"'),!0}function ve(e){var t,n;if(38!==(n=e.input.charCodeAt(e.position)))return!1;for(null!==e.anchor&&ce(e,"duplication of an anchor property"),n=e.input.charCodeAt(++e.position),t=e.position;0!==n&&!z(n)&&!X(n);)n=e.input.charCodeAt(++e.position);return e.position===t&&ce(e,"name of an anchor node must contain at least one character"),e.anchor=e.input.slice(t,e.position),!0}function we(e,t,i,r,o){var a,l,c,s,u,p,f,d,h,g=1,m=!1,y=!1;if(null!==e.listener&&e.listener("open",e),e.tag=null,e.anchor=null,e.kind=null,e.result=null,a=l=c=4===i||3===i,r&&ge(e,!0,-1)&&(m=!0,e.lineIndent>t?g=1:e.lineIndent===t?g=0:e.lineIndent<t&&(g=-1)),1===g)for(;Ae(e)||ve(e);)ge(e,!0,-1)?(m=!0,c=a,e.lineIndent>t?g=1:e.lineIndent===t?g=0:e.lineIndent<t&&(g=-1)):c=!1;if(c&&(c=m||o),1!==g&&4!==i||(d=1===i||2===i?t:t+1,h=e.position-e.lineStart,1===g?c&&(be(e,h)||function(e,t,n){var i,r,o,a,l,c,s,u=e.tag,p=e.anchor,f={},d=Object.create(null),h=null,g=null,m=null,y=!1,b=!1;if(-1!==e.firstTabInLine)return!1;for(null!==e.anchor&&(e.anchorMap[e.anchor]=f),s=e.input.charCodeAt(e.position);0!==s;){if(y||-1===e.firstTabInLine||(e.position=e.firstTabInLine,ce(e,"tab characters must not be used in indentation")),i=e.input.charCodeAt(e.position+1),o=e.line,63!==s&&58!==s||!z(i)){if(a=e.line,l=e.lineStart,c=e.position,!we(e,n,2,!1,!0))break;if(e.line===o){for(s=e.input.charCodeAt(e.position);Q(s);)s=e.input.charCodeAt(++e.position);if(58===s)z(s=e.input.charCodeAt(++e.position))||ce(e,"a whitespace character is expected after the key-value separator within a block mapping"),y&&(de(e,f,d,h,g,null,a,l,c),h=g=m=null),b=!0,y=!1,r=!1,h=e.tag,g=e.result;else{if(!b)return e.tag=u,e.anchor=p,!0;ce(e,"can not read an implicit mapping pair; a colon is missed")}}else{if(!b)return e.tag=u,e.anchor=p,!0;ce(e,"can not read a block mapping entry; a multiline key may not be an implicit key")}}else 63===s?(y&&(de(e,f,d,h,g,null,a,l,c),h=g=m=null),b=!0,y=!0,r=!0):y?(y=!1,r=!0):ce(e,"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line"),e.position+=1,s=i;if((e.line===o||e.lineIndent>t)&&(y&&(a=e.line,l=e.lineStart,c=e.position),we(e,t,4,!0,r)&&(y?g=e.result:m=e.result),y||(de(e,f,d,h,g,m,a,l,c),h=g=m=null),ge(e,!0,-1),s=e.input.charCodeAt(e.position)),(e.line===o||e.lineIndent>t)&&0!==s)ce(e,"bad indentation of a mapping entry");else if(e.lineIndent<t)break}return y&&de(e,f,d,h,g,null,a,l,c),b&&(e.tag=u,e.anchor=p,e.kind="mapping",e.result=f),b}(e,h,d))||function(e,t){var n,i,r,o,a,l,c,s,u,p,f,d,h=!0,g=e.tag,m=e.anchor,y=Object.create(null);if(91===(d=e.input.charCodeAt(e.position)))a=93,s=!1,o=[];else{if(123!==d)return!1;a=125,s=!0,o={}}for(null!==e.anchor&&(e.anchorMap[e.anchor]=o),d=e.input.charCodeAt(++e.position);0!==d;){if(ge(e,!0,t),(d=e.input.charCodeAt(e.position))===a)return e.position++,e.tag=g,e.anchor=m,e.kind=s?"mapping":"sequence",e.result=o,!0;h?44===d&&ce(e,"expected the node content, but found ','"):ce(e,"missed comma between flow collection entries"),f=null,l=c=!1,63===d&&z(e.input.charCodeAt(e.position+1))&&(l=c=!0,e.position++,ge(e,!0,t)),n=e.line,i=e.lineStart,r=e.position,we(e,t,1,!1,!0),p=e.tag,u=e.result,ge(e,!0,t),d=e.input.charCodeAt(e.position),!c&&e.line!==n||58!==d||(l=!0,d=e.input.charCodeAt(++e.position),ge(e,!0,t),we(e,t,1,!1,!0),f=e.result),s?de(e,o,y,p,u,f,n,i,r):l?o.push(de(e,null,y,p,u,f,n,i,r)):o.push(u),ge(e,!0,t),44===(d=e.input.charCodeAt(e.position))?(h=!0,d=e.input.charCodeAt(++e.position)):h=!1}ce(e,"unexpected end of the stream within a flow collection")}(e,d)?y=!0:(l&&function(e,t){var i,r,o,a,l,c=1,s=!1,u=!1,p=t,f=0,d=!1;if(124===(a=e.input.charCodeAt(e.position)))r=!1;else{if(62!==a)return!1;r=!0}for(e.kind="scalar",e.result="";0!==a;)if(43===(a=e.input.charCodeAt(++e.position))||45===a)1===c?c=43===a?3:2:ce(e,"repeat of a chomping mode identifier");else{if(!((o=48<=(l=a)&&l<=57?l-48:-1)>=0))break;0===o?ce(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):u?ce(e,"repeat of an indentation width identifier"):(p=t+o-1,u=!0)}if(Q(a)){do{a=e.input.charCodeAt(++e.position)}while(Q(a));if(35===a)do{a=e.input.charCodeAt(++e.position)}while(!J(a)&&0!==a)}for(;0!==a;){for(he(e),e.lineIndent=0,a=e.input.charCodeAt(e.position);(!u||e.lineIndent<p)&&32===a;)e.lineIndent++,a=e.input.charCodeAt(++e.position);if(!u&&e.lineIndent>p&&(p=e.lineIndent),J(a))f++;else{if(e.lineIndent<p){3===c?e.result+=n.repeat("\n",s?1+f:f):1===c&&s&&(e.result+="\n");break}for(r?Q(a)?(d=!0,e.result+=n.repeat("\n",s?1+f:f)):d?(d=!1,e.result+=n.repeat("\n",f+1)):0===f?s&&(e.result+=" "):e.result+=n.repeat("\n",f):e.result+=n.repeat("\n",s?1+f:f),s=!0,u=!0,f=0,i=e.position;!J(a)&&0!==a;)a=e.input.charCodeAt(++e.position);pe(e,i,e.position,!1)}}return!0}(e,d)||function(e,t){var n,i,r;if(39!==(n=e.input.charCodeAt(e.position)))return!1;for(e.kind="scalar",e.result="",e.position++,i=r=e.position;0!==(n=e.input.charCodeAt(e.position));)if(39===n){if(pe(e,i,e.position,!0),39!==(n=e.input.charCodeAt(++e.position)))return!0;i=e.position,e.position++,r=e.position}else J(n)?(pe(e,i,r,!0),ye(e,ge(e,!1,t)),i=r=e.position):e.position===e.lineStart&&me(e)?ce(e,"unexpected end of the document within a single quoted scalar"):(e.position++,r=e.position);ce(e,"unexpected end of the stream within a single quoted scalar")}(e,d)||function(e,t){var n,i,r,o,a,l,c;if(34!==(l=e.input.charCodeAt(e.position)))return!1;for(e.kind="scalar",e.result="",e.position++,n=i=e.position;0!==(l=e.input.charCodeAt(e.position));){if(34===l)return pe(e,n,e.position,!0),e.position++,!0;if(92===l){if(pe(e,n,e.position,!0),J(l=e.input.charCodeAt(++e.position)))ge(e,!1,t);else if(l<256&&ie[l])e.result+=re[l],e.position++;else if((a=120===(c=l)?2:117===c?4:85===c?8:0)>0){for(r=a,o=0;r>0;r--)(a=ee(l=e.input.charCodeAt(++e.position)))>=0?o=(o<<4)+a:ce(e,"expected hexadecimal character");e.result+=ne(o),e.position++}else ce(e,"unknown escape sequence");n=i=e.position}else J(l)?(pe(e,n,i,!0),ye(e,ge(e,!1,t)),n=i=e.position):e.position===e.lineStart&&me(e)?ce(e,"unexpected end of the document within a double quoted scalar"):(e.position++,i=e.position)}ce(e,"unexpected end of the stream within a double quoted scalar")}(e,d)?y=!0:!function(e){var t,n,i;if(42!==(i=e.input.charCodeAt(e.position)))return!1;for(i=e.input.charCodeAt(++e.position),t=e.position;0!==i&&!z(i)&&!X(i);)i=e.input.charCodeAt(++e.position);return e.position===t&&ce(e,"name of an alias node must contain at least one character"),n=e.input.slice(t,e.position),P.call(e.anchorMap,n)||ce(e,'unidentified alias "'+n+'"'),e.result=e.anchorMap[n],ge(e,!0,-1),!0}(e)?function(e,t,n){var i,r,o,a,l,c,s,u,p=e.kind,f=e.result;if(z(u=e.input.charCodeAt(e.position))||X(u)||35===u||38===u||42===u||33===u||124===u||62===u||39===u||34===u||37===u||64===u||96===u)return!1;if((63===u||45===u)&&(z(i=e.input.charCodeAt(e.position+1))||n&&X(i)))return!1;for(e.kind="scalar",e.result="",r=o=e.position,a=!1;0!==u;){if(58===u){if(z(i=e.input.charCodeAt(e.position+1))||n&&X(i))break}else if(35===u){if(z(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&me(e)||n&&X(u))break;if(J(u)){if(l=e.line,c=e.lineStart,s=e.lineIndent,ge(e,!1,-1),e.lineIndent>=t){a=!0,u=e.input.charCodeAt(e.position);continue}e.position=o,e.line=l,e.lineStart=c,e.lineIndent=s;break}}a&&(pe(e,r,o,!1),ye(e,e.line-l),r=o=e.position,a=!1),Q(u)||(o=e.position+1),u=e.input.charCodeAt(++e.position)}return pe(e,r,o,!1),!!e.result||(e.kind=p,e.result=f,!1)}(e,d,1===i)&&(y=!0,null===e.tag&&(e.tag="?")):(y=!0,null===e.tag&&null===e.anchor||ce(e,"alias node should not have any properties")),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):0===g&&(y=c&&be(e,h))),null===e.tag)null!==e.anchor&&(e.anchorMap[e.anchor]=e.result);else if("?"===e.tag){for(null!==e.result&&"scalar"!==e.kind&&ce(e,'unacceptable node kind for !<?> tag; it should be "scalar", not "'+e.kind+'"'),s=0,u=e.implicitTypes.length;s<u;s+=1)if((f=e.implicitTypes[s]).resolve(e.result)){e.result=f.construct(e.result),e.tag=f.tag,null!==e.anchor&&(e.anchorMap[e.anchor]=e.result);break}}else if("!"!==e.tag){if(P.call(e.typeMap[e.kind||"fallback"],e.tag))f=e.typeMap[e.kind||"fallback"][e.tag];else for(f=null,s=0,u=(p=e.typeMap.multi[e.kind||"fallback"]).length;s<u;s+=1)if(e.tag.slice(0,p[s].tag.length)===p[s].tag){f=p[s];break}f||ce(e,"unknown tag !<"+e.tag+">"),null!==e.result&&f.kind!==e.kind&&ce(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+f.kind+'", not "'+e.kind+'"'),f.resolve(e.result,e.tag)?(e.result=f.construct(e.result,e.tag),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):ce(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return null!==e.listener&&e.listener("close",e),null!==e.tag||null!==e.anchor||y}function ke(e){var t,n,i,r,o=e.position,a=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);0!==(r=e.input.charCodeAt(e.position))&&(ge(e,!0,-1),r=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==r));){for(a=!0,r=e.input.charCodeAt(++e.position),t=e.position;0!==r&&!z(r);)r=e.input.charCodeAt(++e.position);for(i=[],(n=e.input.slice(t,e.position)).length<1&&ce(e,"directive name must not be less than one character in length");0!==r;){for(;Q(r);)r=e.input.charCodeAt(++e.position);if(35===r){do{r=e.input.charCodeAt(++e.position)}while(0!==r&&!J(r));break}if(J(r))break;for(t=e.position;0!==r&&!z(r);)r=e.input.charCodeAt(++e.position);i.push(e.input.slice(t,e.position))}0!==r&&he(e),P.call(ue,n)?ue[n](e,n,i):se(e,'unknown document directive "'+n+'"')}ge(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,ge(e,!0,-1)):a&&ce(e,"directives end mark is expected"),we(e,e.lineIndent-1,4,!1,!0),ge(e,!0,-1),e.checkLineBreaks&&H.test(e.input.slice(o,e.position))&&se(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&me(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,ge(e,!0,-1)):e.position<e.length-1&&ce(e,"end of the stream or a document separator is expected")}function Ce(e,t){t=t||{},0!==(e=String(e)).length&&(10!==e.charCodeAt(e.length-1)&&13!==e.charCodeAt(e.length-1)&&(e+="\n"),65279===e.charCodeAt(0)&&(e=e.slice(1)));var n=new ae(e,t),i=e.indexOf("\0");for(-1!==i&&(n.position=i,ce(n,"null byte is not allowed in input")),n.input+="\0";32===n.input.charCodeAt(n.position);)n.lineIndent+=1,n.position+=1;for(;n.position<n.length-1;)ke(n);return n.documents}var xe={loadAll:function(e,t,n){null!==t&&"object"==typeof t&&void 0===n&&(n=t,t=null);var i=Ce(e,n);if("function"!=typeof t)return i;for(var r=0,o=i.length;r<o;r+=1)t(i[r])},load:function(e,t){var n=Ce(e,t);if(0!==n.length){if(1===n.length)return n[0];throw new o("expected a single document in the stream, but found more")}}},Ie=Object.prototype.toString,Se=Object.prototype.hasOwnProperty,Oe=65279,je={0:"\\0",7:"\\a",8:"\\b",9:"\\t",10:"\\n",11:"\\v",12:"\\f",13:"\\r",27:"\\e",34:'\\"',92:"\\\\",133:"\\N",160:"\\_",8232:"\\L",8233:"\\P"},Te=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"],Ne=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/;function Fe(e){var t,i,r;if(t=e.toString(16).toUpperCase(),e<=255)i="x",r=2;else if(e<=65535)i="u",r=4;else{if(!(e<=4294967295))throw new o("code point within a string may not be greater than 0xFFFFFFFF");i="U",r=8}return"\\"+i+n.repeat("0",r-t.length)+t}function Ee(e){this.schema=e.schema||K,this.indent=Math.max(1,e.indent||2),this.noArrayIndent=e.noArrayIndent||!1,this.skipInvalid=e.skipInvalid||!1,this.flowLevel=n.isNothing(e.flowLevel)?-1:e.flowLevel,this.styleMap=function(e,t){var n,i,r,o,a,l,c;if(null===t)return{};for(n={},r=0,o=(i=Object.keys(t)).length;r<o;r+=1)a=i[r],l=String(t[a]),"!!"===a.slice(0,2)&&(a="tag:yaml.org,2002:"+a.slice(2)),(c=e.compiledTypeMap.fallback[a])&&Se.call(c.styleAliases,l)&&(l=c.styleAliases[l]),n[a]=l;return n}(this.schema,e.styles||null),this.sortKeys=e.sortKeys||!1,this.lineWidth=e.lineWidth||80,this.noRefs=e.noRefs||!1,this.noCompatMode=e.noCompatMode||!1,this.condenseFlow=e.condenseFlow||!1,this.quotingType='"'===e.quotingType?2:1,this.forceQuotes=e.forceQuotes||!1,this.replacer="function"==typeof e.replacer?e.replacer:null,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result="",this.duplicates=[],this.usedDuplicates=null}function Me(e,t){for(var i,r=n.repeat(" ",t),o=0,a=-1,l="",c=e.length;o<c;)-1===(a=e.indexOf("\n",o))?(i=e.slice(o),o=c):(i=e.slice(o,a+1),o=a+1),i.length&&"\n"!==i&&(l+=r),l+=i;return l}function Le(e,t){return"\n"+n.repeat(" ",e.indent*t)}function _e(e){return 32===e||9===e}function De(e){return 32<=e&&e<=126||161<=e&&e<=55295&&8232!==e&&8233!==e||57344<=e&&e<=65533&&e!==Oe||65536<=e&&e<=1114111}function Ue(e){return De(e)&&e!==Oe&&13!==e&&10!==e}function qe(e,t,n){var i=Ue(e),r=i&&!_e(e);return(n?i:i&&44!==e&&91!==e&&93!==e&&123!==e&&125!==e)&&35!==e&&!(58===t&&!r)||Ue(t)&&!_e(t)&&35===e||58===t&&r}function Ye(e,t){var n,i=e.charCodeAt(t);return i>=55296&&i<=56319&&t+1<e.length&&(n=e.charCodeAt(t+1))>=56320&&n<=57343?1024*(i-55296)+n-56320+65536:i}function Re(e){return/^\n* /.test(e)}function Be(e,t,n,i,r,o,a,l){var c,s,u=0,p=null,f=!1,d=!1,h=-1!==i,g=-1,m=De(s=Ye(e,0))&&s!==Oe&&!_e(s)&&45!==s&&63!==s&&58!==s&&44!==s&&91!==s&&93!==s&&123!==s&&125!==s&&35!==s&&38!==s&&42!==s&&33!==s&&124!==s&&61!==s&&62!==s&&39!==s&&34!==s&&37!==s&&64!==s&&96!==s&&function(e){return!_e(e)&&58!==e}(Ye(e,e.length-1));if(t||a)for(c=0;c<e.length;u>=65536?c+=2:c++){if(!De(u=Ye(e,c)))return 5;m=m&&qe(u,p,l),p=u}else{for(c=0;c<e.length;u>=65536?c+=2:c++){if(10===(u=Ye(e,c)))f=!0,h&&(d=d||c-g-1>i&&" "!==e[g+1],g=c);else if(!De(u))return 5;m=m&&qe(u,p,l),p=u}d=d||h&&c-g-1>i&&" "!==e[g+1]}return f||d?n>9&&Re(e)?5:a?2===o?5:2:d?4:3:!m||a||r(e)?2===o?5:2:1}function Ke(e,t,n,i,r){e.dump=function(){if(0===t.length)return 2===e.quotingType?'""':"''";if(!e.noCompatMode&&(-1!==Te.indexOf(t)||Ne.test(t)))return 2===e.quotingType?'"'+t+'"':"'"+t+"'";var a=e.indent*Math.max(1,n),l=-1===e.lineWidth?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-a),c=i||e.flowLevel>-1&&n>=e.flowLevel;switch(Be(t,c,e.indent,l,(function(t){return function(e,t){var n,i;for(n=0,i=e.implicitTypes.length;n<i;n+=1)if(e.implicitTypes[n].resolve(t))return!0;return!1}(e,t)}),e.quotingType,e.forceQuotes&&!i,r)){case 1:return t;case 2:return"'"+t.replace(/'/g,"''")+"'";case 3:return"|"+Pe(t,e.indent)+We(Me(t,a));case 4:return">"+Pe(t,e.indent)+We(Me(function(e,t){var n,i,r=/(\n+)([^\n]*)/g,o=(l=e.indexOf("\n"),l=-1!==l?l:e.length,r.lastIndex=l,He(e.slice(0,l),t)),a="\n"===e[0]||" "===e[0];var l;for(;i=r.exec(e);){var c=i[1],s=i[2];n=" "===s[0],o+=c+(a||n||""===s?"":"\n")+He(s,t),a=n}return o}(t,l),a));case 5:return'"'+function(e){for(var t,n="",i=0,r=0;r<e.length;i>=65536?r+=2:r++)i=Ye(e,r),!(t=je[i])&&De(i)?(n+=e[r],i>=65536&&(n+=e[r+1])):n+=t||Fe(i);return n}(t)+'"';default:throw new o("impossible error: invalid scalar style")}}()}function Pe(e,t){var n=Re(e)?String(t):"",i="\n"===e[e.length-1];return n+(i&&("\n"===e[e.length-2]||"\n"===e)?"+":i?"":"-")+"\n"}function We(e){return"\n"===e[e.length-1]?e.slice(0,-1):e}function He(e,t){if(""===e||" "===e[0])return e;for(var n,i,r=/ [^ ]/g,o=0,a=0,l=0,c="";n=r.exec(e);)(l=n.index)-o>t&&(i=a>o?a:l,c+="\n"+e.slice(o,i),o=i+1),a=l;return c+="\n",e.length-o>t&&a>o?c+=e.slice(o,a)+"\n"+e.slice(a+1):c+=e.slice(o),c.slice(1)}function $e(e,t,n,i){var r,o,a,l="",c=e.tag;for(r=0,o=n.length;r<o;r+=1)a=n[r],e.replacer&&(a=e.replacer.call(n,String(r),a)),(Ve(e,t+1,a,!0,!0,!1,!0)||void 0===a&&Ve(e,t+1,null,!0,!0,!1,!0))&&(i&&""===l||(l+=Le(e,t)),e.dump&&10===e.dump.charCodeAt(0)?l+="-":l+="- ",l+=e.dump);e.tag=c,e.dump=l||"[]"}function Ge(e,t,n){var i,r,a,l,c,s;for(a=0,l=(r=n?e.explicitTypes:e.implicitTypes).length;a<l;a+=1)if(((c=r[a]).instanceOf||c.predicate)&&(!c.instanceOf||"object"==typeof t&&t instanceof c.instanceOf)&&(!c.predicate||c.predicate(t))){if(n?c.multi&&c.representName?e.tag=c.representName(t):e.tag=c.tag:e.tag="?",c.represent){if(s=e.styleMap[c.tag]||c.defaultStyle,"[object Function]"===Ie.call(c.represent))i=c.represent(t,s);else{if(!Se.call(c.represent,s))throw new o("!<"+c.tag+'> tag resolver accepts not "'+s+'" style');i=c.represent[s](t,s)}e.dump=i}return!0}return!1}function Ve(e,t,n,i,r,a,l){e.tag=null,e.dump=n,Ge(e,n,!1)||Ge(e,n,!0);var c,s=Ie.call(e.dump),u=i;i&&(i=e.flowLevel<0||e.flowLevel>t);var p,f,d="[object Object]"===s||"[object Array]"===s;if(d&&(f=-1!==(p=e.duplicates.indexOf(n))),(null!==e.tag&&"?"!==e.tag||f||2!==e.indent&&t>0)&&(r=!1),f&&e.usedDuplicates[p])e.dump="*ref_"+p;else{if(d&&f&&!e.usedDuplicates[p]&&(e.usedDuplicates[p]=!0),"[object Object]"===s)i&&0!==Object.keys(e.dump).length?(!function(e,t,n,i){var r,a,l,c,s,u,p="",f=e.tag,d=Object.keys(n);if(!0===e.sortKeys)d.sort();else if("function"==typeof e.sortKeys)d.sort(e.sortKeys);else if(e.sortKeys)throw new o("sortKeys must be a boolean or a function");for(r=0,a=d.length;r<a;r+=1)u="",i&&""===p||(u+=Le(e,t)),c=n[l=d[r]],e.replacer&&(c=e.replacer.call(n,l,c)),Ve(e,t+1,l,!0,!0,!0)&&((s=null!==e.tag&&"?"!==e.tag||e.dump&&e.dump.length>1024)&&(e.dump&&10===e.dump.charCodeAt(0)?u+="?":u+="? "),u+=e.dump,s&&(u+=Le(e,t)),Ve(e,t+1,c,!0,s)&&(e.dump&&10===e.dump.charCodeAt(0)?u+=":":u+=": ",p+=u+=e.dump));e.tag=f,e.dump=p||"{}"}(e,t,e.dump,r),f&&(e.dump="&ref_"+p+e.dump)):(!function(e,t,n){var i,r,o,a,l,c="",s=e.tag,u=Object.keys(n);for(i=0,r=u.length;i<r;i+=1)l="",""!==c&&(l+=", "),e.condenseFlow&&(l+='"'),a=n[o=u[i]],e.replacer&&(a=e.replacer.call(n,o,a)),Ve(e,t,o,!1,!1)&&(e.dump.length>1024&&(l+="? "),l+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),Ve(e,t,a,!1,!1)&&(c+=l+=e.dump));e.tag=s,e.dump="{"+c+"}"}(e,t,e.dump),f&&(e.dump="&ref_"+p+" "+e.dump));else if("[object Array]"===s)i&&0!==e.dump.length?(e.noArrayIndent&&!l&&t>0?$e(e,t-1,e.dump,r):$e(e,t,e.dump,r),f&&(e.dump="&ref_"+p+e.dump)):(!function(e,t,n){var i,r,o,a="",l=e.tag;for(i=0,r=n.length;i<r;i+=1)o=n[i],e.replacer&&(o=e.replacer.call(n,String(i),o)),(Ve(e,t,o,!1,!1)||void 0===o&&Ve(e,t,null,!1,!1))&&(""!==a&&(a+=","+(e.condenseFlow?"":" ")),a+=e.dump);e.tag=l,e.dump="["+a+"]"}(e,t,e.dump),f&&(e.dump="&ref_"+p+" "+e.dump));else{if("[object String]"!==s){if("[object Undefined]"===s)return!1;if(e.skipInvalid)return!1;throw new o("unacceptable kind of an object to dump "+s)}"?"!==e.tag&&Ke(e,e.dump,t,a,u)}null!==e.tag&&"?"!==e.tag&&(c=encodeURI("!"===e.tag[0]?e.tag.slice(1):e.tag).replace(/!/g,"%21"),c="!"===e.tag[0]?"!"+c:"tag:yaml.org,2002:"===c.slice(0,18)?"!!"+c.slice(18):"!<"+c+">",e.dump=c+" "+e.dump)}return!0}function Ze(e,t){var n,i,r=[],o=[];for(Je(e,r,o),n=0,i=o.length;n<i;n+=1)t.duplicates.push(r[o[n]]);t.usedDuplicates=new Array(i)}function Je(e,t,n){var i,r,o;if(null!==e&&"object"==typeof e)if(-1!==(r=t.indexOf(e)))-1===n.indexOf(r)&&n.push(r);else if(t.push(e),Array.isArray(e))for(r=0,o=e.length;r<o;r+=1)Je(e[r],t,n);else for(r=0,o=(i=Object.keys(e)).length;r<o;r+=1)Je(e[i[r]],t,n)}function Qe(e,t){return function(){throw new Error("Function yaml."+e+" is removed in js-yaml 4. Use yaml."+t+" instead, which is now safe by default.")}}var ze=p,Xe=h,et=b,tt=O,nt=j,it=K,rt=xe.load,ot=xe.loadAll,at={dump:function(e,t){var n=new Ee(t=t||{});n.noRefs||Ze(e,n);var i=e;return n.replacer&&(i=n.replacer.call({"":i},"",i)),Ve(n,0,i,!0,!0)?n.dump+"\n":""}}.dump,lt=o,ct={binary:L,float:S,map:y,null:A,pairs:Y,set:B,timestamp:F,bool:v,int:C,merge:E,omap:U,seq:m,str:g},st=Qe("safeLoad","load"),ut=Qe("safeLoadAll","loadAll"),pt=Qe("safeDump","dump"),ft={Type:ze,Schema:Xe,FAILSAFE_SCHEMA:et,JSON_SCHEMA:tt,CORE_SCHEMA:nt,DEFAULT_SCHEMA:it,load:rt,loadAll:ot,dump:at,YAMLException:lt,types:ct,safeLoad:st,safeLoadAll:ut,safeDump:pt};e.CORE_SCHEMA=nt,e.DEFAULT_SCHEMA=it,e.FAILSAFE_SCHEMA=et,e.JSON_SCHEMA=tt,e.Schema=Xe,e.Type=ze,e.YAMLException=lt,e.default=ft,e.dump=at,e.load=rt,e.loadAll=ot,e.safeDump=pt,e.safeLoad=st,e.safeLoadAll=ut,e.types=ct,Object.defineProperty(e,"__esModule",{value:!0})}));
|
||
|
||
/**
|
||
* ZDDC — shared naming convention library
|
||
*
|
||
* Canonical implementation of all ZDDC filename, folder name, tracking number,
|
||
* revision, and status logic. Included in every tool's build via shared/zddc.js.
|
||
*
|
||
* Exposed as window.zddc (plain global) so it works with every tool's module
|
||
* pattern (archive globals, classifier IIFE, transmittal IIFE, mdedit globals).
|
||
*
|
||
* Public API
|
||
* ----------
|
||
* zddc.parseFilename(str) → ParsedFile | null
|
||
* zddc.parseFolder(str) → ParsedFolder | null
|
||
* zddc.parseRevision(str) → ParsedRevision
|
||
* zddc.formatFilename(parts) → string
|
||
* zddc.formatFolder(parts) → string
|
||
* zddc.compareRevisions(a, b) → number (-1 | 0 | 1)
|
||
* zddc.isValidStatus(str) → boolean
|
||
* zddc.STATUSES → string[]
|
||
*
|
||
* ParsedFile { trackingNumber, revision, status, title, extension }
|
||
* ParsedFolder { date, trackingNumber, status, title }
|
||
* ParsedRevision { base, modifier, modifierType, modifierNumber, isDraft, full }
|
||
*/
|
||
|
||
(function (root) {
|
||
'use strict';
|
||
|
||
// ── Valid status codes ───────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Complete list of valid ZDDC document status codes.
|
||
* '---' denotes an unknown or not-yet-assigned status.
|
||
*/
|
||
var STATUSES = [
|
||
'---',
|
||
'IFA', 'IFB', 'IFC', 'IFD', 'IFI', 'IFP', 'IFR', 'IFU',
|
||
'REC',
|
||
'RSA', 'RSB', 'RSC', 'RSD', 'RSI',
|
||
];
|
||
|
||
var STATUS_SET = {};
|
||
for (var _i = 0; _i < STATUSES.length; _i++) {
|
||
STATUS_SET[STATUSES[_i]] = true;
|
||
}
|
||
|
||
function isValidStatus(str) {
|
||
return !!STATUS_SET[str];
|
||
}
|
||
|
||
// ── Filename parsing ─────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Canonical file regex.
|
||
* Matches: TRACKING_REVISION (STATUS) - TITLE.EXT
|
||
*
|
||
* Tracking number: no underscores, no whitespace.
|
||
* Revision: no whitespace, no parentheses.
|
||
* Status: anything inside parentheses (validated separately).
|
||
* Title: everything up to the last dot.
|
||
* Extension: after the last dot (lowercased by parseFilename).
|
||
*/
|
||
var FILE_RE = /^([^_\s]+)_([^\s()_]+)\s*\(([^)]+)\)\s*-\s*(\S.*\S|\S)\.\s*([^\s.]+)$/;
|
||
|
||
/**
|
||
* Parse a ZDDC filename.
|
||
*
|
||
* @param {string} filename
|
||
* @returns {{ trackingNumber: string, revision: string, status: string,
|
||
* title: string, extension: string, valid: boolean } | null}
|
||
* null only if filename is falsy.
|
||
* `valid` is true when all fields matched the ZDDC pattern.
|
||
*/
|
||
function parseFilename(filename) {
|
||
if (!filename) { return null; }
|
||
|
||
var match = filename.match(FILE_RE);
|
||
|
||
if (!match) {
|
||
var lastDot = filename.lastIndexOf('.');
|
||
return {
|
||
trackingNumber: '',
|
||
revision: '',
|
||
status: '',
|
||
title: lastDot > 0 ? filename.substring(0, lastDot) : filename,
|
||
extension: lastDot > 0 ? filename.substring(lastDot + 1).toLowerCase() : '',
|
||
valid: false,
|
||
};
|
||
}
|
||
|
||
return {
|
||
trackingNumber: match[1].trim(),
|
||
revision: match[2].trim(),
|
||
status: match[3].trim(),
|
||
title: match[4].trim(),
|
||
extension: match[5].toLowerCase(),
|
||
valid: true,
|
||
};
|
||
}
|
||
|
||
// ── Folder name parsing ──────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Transmittal folder regex.
|
||
* Matches: YYYY-MM-DD_TRACKING (STATUS) - TITLE
|
||
*/
|
||
var FOLDER_RE = /^(\d{4}-\d{2}-\d{2})_([^_\s(]+)\s*\(([^)]+)\)\s*-\s*(.+)$/;
|
||
|
||
/**
|
||
* Parse a ZDDC transmittal folder name.
|
||
*
|
||
* @param {string} foldername
|
||
* @returns {{ date: string, trackingNumber: string, status: string,
|
||
* title: string, valid: boolean } | null}
|
||
* null only if foldername is falsy.
|
||
*/
|
||
function parseFolder(foldername) {
|
||
if (!foldername) { return null; }
|
||
|
||
var match = foldername.match(FOLDER_RE);
|
||
|
||
if (!match) {
|
||
return {
|
||
date: '',
|
||
trackingNumber: '',
|
||
status: '',
|
||
title: foldername,
|
||
valid: false,
|
||
};
|
||
}
|
||
|
||
return {
|
||
date: match[1],
|
||
trackingNumber: match[2].trim(),
|
||
status: match[3].trim(),
|
||
title: match[4].trim(),
|
||
valid: true,
|
||
};
|
||
}
|
||
|
||
// ── Revision parsing ─────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Modifier sub-regex: +LETTER DIGITS e.g. +C1, +B2, +N1, +Q1
|
||
* The draft prefix (~) may appear inside the modifier: A+~C1
|
||
*/
|
||
var MODIFIER_RE = /^\+(~?)([A-Za-z])(\d+)$/;
|
||
|
||
/**
|
||
* Parse a ZDDC revision string.
|
||
*
|
||
* Revision grammar:
|
||
* revision = ['~'] base ['+' ['~'] modifier_letter modifier_number]
|
||
* base = letter(s) | digit(s) | date(YYYY-MM-DD)
|
||
* modifier = letter + digits e.g. C1, B2, N1, Q1
|
||
*
|
||
* @param {string} revision
|
||
* @returns {{
|
||
* base: string,
|
||
* modifier: string, full modifier string e.g. '+C1', '' if none
|
||
* modifierType: string, modifier letter e.g. 'C', '' if none
|
||
* modifierNumber: number, modifier number e.g. 1, 0 if none
|
||
* modifierIsDraft: boolean,
|
||
* isDraft: boolean, true if base revision starts with ~
|
||
* full: string, original input
|
||
* }}
|
||
*/
|
||
function parseRevision(revision) {
|
||
var raw = (revision || '').toString();
|
||
|
||
// Split on '+' to separate base from optional modifier
|
||
var plusIdx = raw.indexOf('+');
|
||
var basePart = plusIdx === -1 ? raw : raw.substring(0, plusIdx);
|
||
var modifierPart = plusIdx === -1 ? '' : raw.substring(plusIdx);
|
||
|
||
// Draft flag on the base part
|
||
var isDraft = basePart.startsWith('~');
|
||
var base = isDraft ? basePart.substring(1) : basePart;
|
||
|
||
// Parse modifier
|
||
var modifier = '';
|
||
var modifierType = '';
|
||
var modifierNumber = 0;
|
||
var modifierIsDraft = false;
|
||
|
||
if (modifierPart) {
|
||
var mMatch = modifierPart.match(MODIFIER_RE);
|
||
if (mMatch) {
|
||
modifierIsDraft = mMatch[1] === '~';
|
||
modifierType = mMatch[2].toUpperCase();
|
||
modifierNumber = parseInt(mMatch[3], 10);
|
||
modifier = modifierPart;
|
||
} else {
|
||
// Unrecognised modifier — preserve as-is
|
||
modifier = modifierPart;
|
||
}
|
||
}
|
||
|
||
return {
|
||
base: base,
|
||
modifier: modifier,
|
||
modifierType: modifierType,
|
||
modifierNumber: modifierNumber,
|
||
modifierIsDraft: modifierIsDraft,
|
||
isDraft: isDraft,
|
||
full: raw,
|
||
};
|
||
}
|
||
|
||
// ── Revision comparison ──────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Classify a base revision string into a sort tier:
|
||
* 0 = date (YYYY-MM-DD)
|
||
* 1 = letter(s) A, B, AA …
|
||
* 2 = number(s) 0, 1, 2, 1.5 …
|
||
* 3 = other
|
||
*/
|
||
function _baseTier(base) {
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(base)) { return 0; }
|
||
if (/^[A-Za-z]+$/.test(base)) { return 1; }
|
||
if (/^\d+(\.\d+)?$/.test(base)) { return 2; }
|
||
return 3;
|
||
}
|
||
|
||
/**
|
||
* Compare two base revision strings.
|
||
* Sort order: dates < letters < numbers < other.
|
||
*/
|
||
function _compareBase(a, b) {
|
||
var ta = _baseTier(a);
|
||
var tb = _baseTier(b);
|
||
if (ta !== tb) { return ta - tb; }
|
||
|
||
if (ta === 0) { return a < b ? -1 : a > b ? 1 : 0; } // date lexicographic = chronological
|
||
if (ta === 1) { return a.toUpperCase() < b.toUpperCase() ? -1 : a.toUpperCase() > b.toUpperCase() ? 1 : 0; }
|
||
if (ta === 2) { return parseFloat(a) - parseFloat(b); }
|
||
return a.localeCompare(b);
|
||
}
|
||
|
||
/**
|
||
* Compare two ZDDC revision strings for sort ordering.
|
||
*
|
||
* Canonical order (ascending = older → newer):
|
||
* ~A < A < A+B1 < A+C1 < A+~C2 < A+C2 < A+N1 < A+Q1
|
||
* < ~B < B < … < 0 < 1 < 2
|
||
*
|
||
* Rules:
|
||
* 1. Compare base revisions first (dates < letters < numbers).
|
||
* 2. For equal bases, draft (isDraft=true) comes before final.
|
||
* 3. For equal base+draft, no-modifier < has-modifier.
|
||
* 4. For equal base+draft+modifier presence:
|
||
* a. modifier draft comes before modifier final (modifierIsDraft).
|
||
* b. Sort modifier by type letter then by number.
|
||
*
|
||
* @param {string} a
|
||
* @param {string} b
|
||
* @returns {number} negative if a < b, 0 if equal, positive if a > b
|
||
*/
|
||
function compareRevisions(a, b) {
|
||
var pa = parseRevision(a);
|
||
var pb = parseRevision(b);
|
||
|
||
// 1. Base revision
|
||
var baseCmp = _compareBase(pa.base, pb.base);
|
||
if (baseCmp !== 0) { return baseCmp; }
|
||
|
||
// 2. Draft before final (for same base)
|
||
if (pa.isDraft !== pb.isDraft) { return pa.isDraft ? -1 : 1; }
|
||
|
||
// 3. No modifier before any modifier
|
||
var aHasMod = pa.modifier !== '';
|
||
var bHasMod = pb.modifier !== '';
|
||
if (aHasMod !== bHasMod) { return aHasMod ? 1 : -1; }
|
||
|
||
if (!aHasMod) { return 0; } // both have no modifier
|
||
|
||
// 4. Compare modifiers: type → number → draft (draft is a tie-breaker only)
|
||
// 4a. Modifier type letter (B < C < N < Q …)
|
||
if (pa.modifierType !== pb.modifierType) {
|
||
return pa.modifierType < pb.modifierType ? -1 : 1;
|
||
}
|
||
|
||
// 4b. Modifier number (1 < 2 …)
|
||
if (pa.modifierNumber !== pb.modifierNumber) {
|
||
return pa.modifierNumber - pb.modifierNumber;
|
||
}
|
||
|
||
// 4c. Draft of a modifier comes before the final modifier (same type+number)
|
||
if (pa.modifierIsDraft !== pb.modifierIsDraft) {
|
||
return pa.modifierIsDraft ? -1 : 1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// ── Filename / folder formatting ─────────────────────────────────────────
|
||
|
||
/**
|
||
* Build a ZDDC filename from its components.
|
||
*
|
||
* @param {{ trackingNumber: string, revision: string, status: string,
|
||
* title: string, extension: string }} parts
|
||
* @returns {string} e.g. "123456-EL-SPC-2623_A (IFR) - Specification.pdf"
|
||
*/
|
||
function formatFilename(parts) {
|
||
var tn = (parts.trackingNumber || '').trim();
|
||
var rev = (parts.revision || '').trim();
|
||
var st = (parts.status || '').trim();
|
||
var ttl = (parts.title || '').trim();
|
||
var ext = (parts.extension || '').replace(/^\./, '');
|
||
|
||
if (!tn || !rev || !st || !ttl) { return ''; }
|
||
|
||
var name = tn + '_' + rev + ' (' + st + ') - ' + ttl;
|
||
return ext ? name + '.' + ext : name;
|
||
}
|
||
|
||
/**
|
||
* Build a ZDDC transmittal folder name from its components.
|
||
*
|
||
* @param {{ date: string, trackingNumber: string, status: string,
|
||
* title: string }} parts
|
||
* @returns {string} e.g. "2025-10-31_123456-EM-SUB-0001 (IFR) - Title"
|
||
*/
|
||
function formatFolder(parts) {
|
||
var dt = (parts.date || '').trim();
|
||
var tn = (parts.trackingNumber || '').trim();
|
||
var st = (parts.status || '').trim();
|
||
var ttl = (parts.title || '').trim();
|
||
|
||
if (!dt || !tn || !st || !ttl) { return ''; }
|
||
|
||
return dt + '_' + tn + ' (' + st + ') - ' + ttl;
|
||
}
|
||
|
||
// ── Filename / extension splitting ───────────────────────────────────────
|
||
|
||
/**
|
||
* Split a filename into its base name and extension (no leading dot).
|
||
* Treats leading dot ('.gitignore') as no extension.
|
||
*
|
||
* @param {string} filename
|
||
* @returns {{ name: string, extension: string }}
|
||
*/
|
||
function splitExtension(filename) {
|
||
if (!filename) { return { name: '', extension: '' }; }
|
||
var lastDot = filename.lastIndexOf('.');
|
||
if (lastDot <= 0) { return { name: filename, extension: '' }; }
|
||
return {
|
||
name: filename.substring(0, lastDot),
|
||
extension: filename.substring(lastDot + 1).toLowerCase(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Join a base name and extension. Tolerant of either form ('pdf' or '.pdf').
|
||
* Returns just the name when extension is empty.
|
||
*/
|
||
function joinExtension(name, extension) {
|
||
var ext = (extension || '').replace(/^\./, '');
|
||
return ext ? name + '.' + ext : name;
|
||
}
|
||
|
||
// ── Public API ───────────────────────────────────────────────────────────
|
||
|
||
root.zddc = {
|
||
STATUSES: STATUSES,
|
||
isValidStatus: isValidStatus,
|
||
parseFilename: parseFilename,
|
||
parseFolder: parseFolder,
|
||
parseRevision: parseRevision,
|
||
formatFilename: formatFilename,
|
||
formatFolder: formatFolder,
|
||
compareRevisions: compareRevisions,
|
||
splitExtension: splitExtension,
|
||
joinExtension: joinExtension,
|
||
};
|
||
|
||
}(typeof window !== 'undefined' ? window : this));
|
||
|
||
// shared/zddc-source.js — source abstraction for tools that handle
|
||
// directory trees (classifier, mdedit, transmittal, browse, archive).
|
||
//
|
||
// Two backends:
|
||
//
|
||
// 1. Local — wraps a real FileSystemDirectoryHandle from the
|
||
// File System Access API. Reads + writes go through the
|
||
// FS Access API directly.
|
||
//
|
||
// 2. HTTP — talks to zddc-server's directory listing JSON
|
||
// (Accept: application/json) for reads and the file API
|
||
// (PUT/DELETE/POST X-ZDDC-Op) for writes. Implements a
|
||
// polyfill of the FS Access API surface area the tools
|
||
// use (kind, name, values(), getFileHandle, getDirectoryHandle,
|
||
// removeEntry, getFile, createWritable, queryPermission /
|
||
// requestPermission) so existing code works unchanged.
|
||
//
|
||
// The polyfill makes auto-load possible: when zddc-server serves
|
||
// a tool at /<dir>/<tool>.html, the tool detects HTTP mode at
|
||
// startup, builds an HttpDirectoryHandle for the tool's containing
|
||
// directory, and hands it to the existing openDirectory(handle)
|
||
// flow without ever showing the file picker.
|
||
//
|
||
// Renames inside a tool today are typically done as
|
||
// "write new + remove old". With HTTP-backed handles this becomes
|
||
// PUT + DELETE — non-atomic. Tools that prefer the atomic server
|
||
// MOVE should call window.zddc.source.moveFile(srcUrl, dstUrl)
|
||
// directly instead of going through the polyfill.
|
||
(function () {
|
||
'use strict';
|
||
|
||
if (!window.zddc) window.zddc = {};
|
||
var FA = window.FileSystemDirectoryHandle || null;
|
||
|
||
// -----------------------------------------------------------------
|
||
// HTTP file API helpers
|
||
// -----------------------------------------------------------------
|
||
|
||
function joinUrl(base, name, isDir) {
|
||
if (!base.endsWith('/')) base = base + '/';
|
||
return base + encodeURIComponent(name) + (isDir ? '/' : '');
|
||
}
|
||
|
||
// Server returns directory entries with a trailing "/" on names.
|
||
// Strip it for the FS Access API name surface.
|
||
function stripSlash(name) {
|
||
return name.endsWith('/') ? name.slice(0, -1) : name;
|
||
}
|
||
|
||
async function httpListing(url) {
|
||
var resp = await fetch(url, {
|
||
headers: { 'Accept': 'application/json' },
|
||
credentials: 'same-origin'
|
||
});
|
||
if (!resp.ok) {
|
||
var err = new Error('listing ' + url + ': HTTP ' + resp.status);
|
||
err.status = resp.status;
|
||
throw err;
|
||
}
|
||
var data = await resp.json();
|
||
if (!Array.isArray(data)) {
|
||
throw new Error('listing ' + url + ': non-array body');
|
||
}
|
||
return data;
|
||
}
|
||
|
||
async function httpExists(url) {
|
||
try {
|
||
var r = await fetch(url, { method: 'HEAD', credentials: 'same-origin' });
|
||
return r.ok;
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// -----------------------------------------------------------------
|
||
// HttpFileHandle — FileSystemFileHandle polyfill
|
||
// -----------------------------------------------------------------
|
||
|
||
function makeFile(blob, name, modTime) {
|
||
return new File([blob], name, {
|
||
type: blob.type,
|
||
lastModified: modTime ? modTime.getTime() : Date.now()
|
||
});
|
||
}
|
||
|
||
function HttpFileHandle(url, name, size, modTime) {
|
||
this.kind = 'file';
|
||
this.name = name;
|
||
this._url = url;
|
||
this._size = size || 0;
|
||
this._modTime = modTime || null;
|
||
this._etag = null;
|
||
}
|
||
HttpFileHandle.prototype.getFile = async function () {
|
||
var resp = await fetch(this._url, { credentials: 'same-origin' });
|
||
if (!resp.ok) {
|
||
throw new Error('GET ' + this._url + ': ' + resp.status);
|
||
}
|
||
var etag = resp.headers.get('ETag');
|
||
if (etag) this._etag = etag.replace(/"/g, '');
|
||
var lm = resp.headers.get('Last-Modified');
|
||
var modTime = lm ? new Date(lm) : this._modTime;
|
||
var blob = await resp.blob();
|
||
return makeFile(blob, this.name, modTime);
|
||
};
|
||
HttpFileHandle.prototype.createWritable = async function () {
|
||
var chunks = [];
|
||
var handle = this;
|
||
return {
|
||
async write(data) {
|
||
if (data == null) return;
|
||
if (typeof data === 'object' && data && 'type' in data && data.type === 'write') {
|
||
chunks.push(data.data);
|
||
return;
|
||
}
|
||
if (typeof data === 'object' && data && 'type' in data) {
|
||
// seek/truncate not supported by HTTP backend
|
||
throw new Error('HttpFileHandle write op not supported: ' + data.type);
|
||
}
|
||
chunks.push(data);
|
||
},
|
||
async close() {
|
||
var blob = new Blob(chunks);
|
||
var resp = await fetch(handle._url, {
|
||
method: 'PUT',
|
||
body: blob,
|
||
credentials: 'same-origin'
|
||
});
|
||
if (!resp.ok) {
|
||
var body = '';
|
||
try { body = await resp.text(); } catch (_) { /* ignore */ }
|
||
throw new Error('PUT ' + handle._url + ': ' + resp.status + ' ' + body);
|
||
}
|
||
var et = resp.headers.get('ETag');
|
||
if (et) handle._etag = et.replace(/"/g, '');
|
||
handle._size = blob.size;
|
||
},
|
||
async abort() { chunks = []; }
|
||
};
|
||
};
|
||
HttpFileHandle.prototype.queryPermission = async function () { return 'granted'; };
|
||
HttpFileHandle.prototype.requestPermission = async function () { return 'granted'; };
|
||
HttpFileHandle.prototype.isHttp = true;
|
||
HttpFileHandle.prototype.url = function () { return this._url; };
|
||
|
||
// -----------------------------------------------------------------
|
||
// HttpDirectoryHandle — FileSystemDirectoryHandle polyfill
|
||
// -----------------------------------------------------------------
|
||
|
||
function HttpDirectoryHandle(url, name) {
|
||
this.kind = 'directory';
|
||
if (!url.endsWith('/')) url = url + '/';
|
||
this._url = url;
|
||
this.name = name || guessNameFromUrl(url);
|
||
}
|
||
function guessNameFromUrl(url) {
|
||
var u = url.replace(/\/+$/, '');
|
||
var slash = u.lastIndexOf('/');
|
||
return slash >= 0 ? decodeURIComponent(u.substring(slash + 1)) : u;
|
||
}
|
||
HttpDirectoryHandle.prototype.values = function () {
|
||
var url = this._url;
|
||
return (async function* () {
|
||
var entries;
|
||
try {
|
||
entries = await httpListing(url);
|
||
} catch (e) {
|
||
return;
|
||
}
|
||
for (var i = 0; i < entries.length; i++) {
|
||
var e = entries[i];
|
||
var rawName = stripSlash(e.name);
|
||
var childUrl = joinUrl(url, rawName, e.is_dir);
|
||
if (e.is_dir) {
|
||
yield new HttpDirectoryHandle(childUrl, rawName);
|
||
} else {
|
||
var modTime = e.mod_time ? new Date(e.mod_time) : null;
|
||
yield new HttpFileHandle(childUrl, rawName, e.size || 0, modTime);
|
||
}
|
||
}
|
||
})();
|
||
};
|
||
HttpDirectoryHandle.prototype.entries = function () {
|
||
var iter = this.values();
|
||
return (async function* () {
|
||
for (;;) {
|
||
var step = await iter.next();
|
||
if (step.done) return;
|
||
yield [step.value.name, step.value];
|
||
}
|
||
})();
|
||
};
|
||
HttpDirectoryHandle.prototype.keys = function () {
|
||
var iter = this.values();
|
||
return (async function* () {
|
||
for (;;) {
|
||
var step = await iter.next();
|
||
if (step.done) return;
|
||
yield step.value.name;
|
||
}
|
||
})();
|
||
};
|
||
HttpDirectoryHandle.prototype.getFileHandle = async function (name, opts) {
|
||
opts = opts || {};
|
||
var url = joinUrl(this._url, name, false);
|
||
var exists = await httpExists(url);
|
||
if (!exists && !opts.create) {
|
||
var err = new Error('NotFoundError: ' + name);
|
||
err.name = 'NotFoundError';
|
||
throw err;
|
||
}
|
||
return new HttpFileHandle(url, name, 0, null);
|
||
};
|
||
HttpDirectoryHandle.prototype.getDirectoryHandle = async function (name, opts) {
|
||
opts = opts || {};
|
||
var url = joinUrl(this._url, name, true);
|
||
if (opts.create) {
|
||
var resp = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'X-ZDDC-Op': 'mkdir' },
|
||
credentials: 'same-origin'
|
||
});
|
||
if (!resp.ok && resp.status !== 200 && resp.status !== 201) {
|
||
throw new Error('mkdir ' + url + ': ' + resp.status);
|
||
}
|
||
}
|
||
return new HttpDirectoryHandle(url, name);
|
||
};
|
||
HttpDirectoryHandle.prototype.removeEntry = async function (name, opts) {
|
||
opts = opts || {};
|
||
// Probe listing to discover whether name is a file or directory.
|
||
var entries;
|
||
try {
|
||
entries = await httpListing(this._url);
|
||
} catch (e) {
|
||
throw new Error('removeEntry probe failed: ' + e.message);
|
||
}
|
||
var match = null;
|
||
for (var i = 0; i < entries.length; i++) {
|
||
if (stripSlash(entries[i].name) === name) {
|
||
match = entries[i];
|
||
break;
|
||
}
|
||
}
|
||
if (!match) {
|
||
var err = new Error('NotFoundError: ' + name);
|
||
err.name = 'NotFoundError';
|
||
throw err;
|
||
}
|
||
if (match.is_dir && !opts.recursive) {
|
||
// Server doesn't expose a recursive-delete endpoint yet,
|
||
// and FS Access API requires recursive=true to remove a
|
||
// non-empty directory anyway. Reject explicitly so the
|
||
// caller doesn't silently leave a stale tree behind.
|
||
var derr = new Error('Removing directories over HTTP is not supported');
|
||
derr.name = 'InvalidStateError';
|
||
throw derr;
|
||
}
|
||
var url = joinUrl(this._url, name, match.is_dir);
|
||
var resp = await fetch(url, { method: 'DELETE', credentials: 'same-origin' });
|
||
if (!resp.ok && resp.status !== 204) {
|
||
throw new Error('DELETE ' + url + ': ' + resp.status);
|
||
}
|
||
};
|
||
HttpDirectoryHandle.prototype.queryPermission = async function () { return 'granted'; };
|
||
HttpDirectoryHandle.prototype.requestPermission = async function () { return 'granted'; };
|
||
HttpDirectoryHandle.prototype.isHttp = true;
|
||
HttpDirectoryHandle.prototype.url = function () { return this._url; };
|
||
|
||
// -----------------------------------------------------------------
|
||
// Top-level helpers
|
||
// -----------------------------------------------------------------
|
||
|
||
// Strip a trailing tool .html (e.g. classifier.html) from a path
|
||
// to land on the "directory the tool was opened in".
|
||
function pathToDir(pathname) {
|
||
if (!pathname) return '/';
|
||
if (pathname.endsWith('/')) return pathname;
|
||
var slash = pathname.lastIndexOf('/');
|
||
return slash >= 0 ? pathname.substring(0, slash + 1) : '/';
|
||
}
|
||
|
||
// Probe the server-mode root for the current page. Returns:
|
||
//
|
||
// { handle: HttpDirectoryHandle, status: 200 } — server reachable, listing returned
|
||
// { handle: null, status: 403 } — server reachable but listing forbidden
|
||
// { handle: null, status: 0 } — not http(s), or server unreachable / non-JSON
|
||
//
|
||
// Tools that auto-load on startup distinguish 403 (show "no
|
||
// permission to list this directory" message) from 0 (fall back
|
||
// to local-mode welcome screen).
|
||
//
|
||
// Tool init pattern:
|
||
// if (location.protocol !== 'file:') {
|
||
// const r = await zddc.source.detectServerRoot();
|
||
// if (r.handle) await openDirectory(r.handle);
|
||
// else if (r.status === 403) showNoPermissionMessage();
|
||
// else showWelcome();
|
||
// } else { showWelcome(); }
|
||
async function detectServerRoot() {
|
||
if (typeof location === 'undefined') {
|
||
return { handle: null, status: 0 };
|
||
}
|
||
if (location.protocol !== 'http:' && location.protocol !== 'https:') {
|
||
return { handle: null, status: 0 };
|
||
}
|
||
var dirPath = pathToDir(location.pathname);
|
||
var url = location.origin + dirPath;
|
||
try {
|
||
await httpListing(url);
|
||
} catch (e) {
|
||
if (e && e.status === 403) {
|
||
return { handle: null, status: 403 };
|
||
}
|
||
return { handle: null, status: 0 };
|
||
}
|
||
return {
|
||
handle: new HttpDirectoryHandle(url, guessNameFromUrl(url)),
|
||
status: 200,
|
||
};
|
||
}
|
||
|
||
// Atomic file move. Path arguments are absolute URL paths
|
||
// (starting with /). Honors the file API's POST /op=move
|
||
// contract. Returns the new ETag.
|
||
async function moveFile(srcUrlPath, dstUrlPath, opts) {
|
||
opts = opts || {};
|
||
var headers = {
|
||
'X-ZDDC-Op': 'move',
|
||
'X-ZDDC-Destination': dstUrlPath
|
||
};
|
||
if (opts.ifMatch) headers['If-Match'] = opts.ifMatch;
|
||
var resp = await fetch(srcUrlPath, {
|
||
method: 'POST',
|
||
headers: headers,
|
||
credentials: 'same-origin'
|
||
});
|
||
if (!resp.ok) {
|
||
var body = '';
|
||
try { body = await resp.text(); } catch (_) { /* ignore */ }
|
||
throw new Error('move ' + srcUrlPath + ' → ' + dstUrlPath + ': ' + resp.status + ' ' + body);
|
||
}
|
||
var et = resp.headers.get('ETag');
|
||
return et ? et.replace(/"/g, '') : null;
|
||
}
|
||
|
||
// Detect at construction time whether a directory handle is the
|
||
// HTTP polyfill or a real FS Access API handle. Useful for tools
|
||
// that want to take the optimized path (e.g. atomic moveFile)
|
||
// when in HTTP mode rather than the FS-API copy+remove fallback.
|
||
function isHttpHandle(handle) {
|
||
return !!(handle && handle.isHttp === true);
|
||
}
|
||
|
||
window.zddc.source = {
|
||
HttpDirectoryHandle: HttpDirectoryHandle,
|
||
HttpFileHandle: HttpFileHandle,
|
||
detectServerRoot: detectServerRoot,
|
||
moveFile: moveFile,
|
||
isHttpHandle: isHttpHandle,
|
||
// Lower-level helpers exposed for tools that want to call the
|
||
// server directly without going through the polyfill.
|
||
httpListing: httpListing,
|
||
joinUrl: joinUrl,
|
||
stripSlash: stripSlash
|
||
};
|
||
})();
|
||
|
||
/**
|
||
* ZDDC shared theme toggle — light / dark / auto.
|
||
* Persists choice to localStorage under 'zddc-theme'.
|
||
* Works with all four tools regardless of their module pattern.
|
||
* Expects: #theme-btn in the DOM (optional — skips gracefully if absent).
|
||
*
|
||
* Theme cycle: auto → light → dark → auto …
|
||
* 'auto' honours the OS prefers-color-scheme media query (CSS handles it).
|
||
* 'light' sets data-theme="light" on <html> (overrides dark media query).
|
||
* 'dark' sets data-theme="dark" on <html>.
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
var STORAGE_KEY = 'zddc-theme';
|
||
var THEMES = ['auto', 'light', 'dark'];
|
||
|
||
var LABELS = {
|
||
auto: '◐',
|
||
light: '☀',
|
||
dark: '☾'
|
||
};
|
||
|
||
var TITLES = {
|
||
auto: 'Theme: auto (follows OS)',
|
||
light: 'Theme: light',
|
||
dark: 'Theme: dark'
|
||
};
|
||
|
||
function load() {
|
||
var stored = localStorage.getItem(STORAGE_KEY);
|
||
return THEMES.indexOf(stored) !== -1 ? stored : 'auto';
|
||
}
|
||
|
||
function apply(theme) {
|
||
if (theme === 'dark') {
|
||
document.documentElement.setAttribute('data-theme', 'dark');
|
||
} else if (theme === 'light') {
|
||
document.documentElement.setAttribute('data-theme', 'light');
|
||
} else {
|
||
document.documentElement.removeAttribute('data-theme');
|
||
}
|
||
}
|
||
|
||
function save(theme) {
|
||
try { localStorage.setItem(STORAGE_KEY, theme); } catch (e) {}
|
||
}
|
||
|
||
function updateButton(btn, theme) {
|
||
btn.textContent = LABELS[theme];
|
||
btn.title = TITLES[theme];
|
||
btn.setAttribute('aria-label', TITLES[theme]);
|
||
}
|
||
|
||
function next(theme) {
|
||
return THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length];
|
||
}
|
||
|
||
function init() {
|
||
var current = load();
|
||
apply(current);
|
||
|
||
var btn = document.getElementById('theme-btn');
|
||
if (!btn) { return; }
|
||
|
||
updateButton(btn, current);
|
||
|
||
btn.addEventListener('click', function () {
|
||
current = next(current);
|
||
apply(current);
|
||
save(current);
|
||
updateButton(btn, current);
|
||
});
|
||
}
|
||
|
||
/* Apply theme immediately (before DOM ready) to avoid flash */
|
||
apply(load());
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
}());
|
||
|
||
/**
|
||
* ZDDC shared help panel — open/close logic.
|
||
* Works with all four tools regardless of their module pattern.
|
||
* Expects: #help-btn, #help-panel, #help-panel-close in the DOM.
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
function init() {
|
||
var helpBtn = document.getElementById('help-btn');
|
||
var panel = document.getElementById('help-panel');
|
||
var closeBtn = document.getElementById('help-panel-close');
|
||
|
||
if (!helpBtn || !panel) { return; }
|
||
|
||
function isOpen() { return !panel.hidden; }
|
||
|
||
function openPanel() {
|
||
panel.hidden = false;
|
||
document.body.classList.add('help-open');
|
||
}
|
||
|
||
function closePanel() {
|
||
panel.hidden = true;
|
||
document.body.classList.remove('help-open');
|
||
}
|
||
|
||
helpBtn.addEventListener('click', function () {
|
||
if (isOpen()) { closePanel(); } else { openPanel(); }
|
||
});
|
||
|
||
if (closeBtn) {
|
||
closeBtn.addEventListener('click', closePanel);
|
||
}
|
||
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape' && isOpen()) { closePanel(); }
|
||
});
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
}());
|
||
|
||
(function (global) {
|
||
'use strict';
|
||
if (global.tablesApp) {
|
||
return;
|
||
}
|
||
global.tablesApp = {
|
||
context: null,
|
||
state: {
|
||
rows: [],
|
||
sort: [],
|
||
filter: {}
|
||
},
|
||
modules: {}
|
||
};
|
||
})(window);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
// load() resolves to the table context the rest of the app renders:
|
||
// { title?, description?, columns, rows, defaults? }
|
||
//
|
||
// Two paths:
|
||
//
|
||
// 1. Inline JSON (test seam, and also any host that wants to
|
||
// pre-render a context server-side): if #table-context parses
|
||
// to a non-empty object, return it as-is.
|
||
//
|
||
// 2. File-backed walk (the real-world path served by zddc-server):
|
||
// fetch <dir>/.zddc, find tables[<name>], fetch the *.table.yaml
|
||
// spec, list <dir>/<name>/*.yaml row files, parse each, and
|
||
// assemble the same shape.
|
||
//
|
||
// file:// mode without a directory handle is unsupported in v1 — the
|
||
// walk only runs against http(s). file:// users must either inject an
|
||
// inline context (tests) or open the page through zddc-server.
|
||
async function load() {
|
||
const inline = readInlineContext();
|
||
if (inline && Object.keys(inline).length > 0) {
|
||
return inline;
|
||
}
|
||
if (typeof location !== 'undefined' &&
|
||
(location.protocol === 'http:' || location.protocol === 'https:')) {
|
||
try {
|
||
const walked = await walkServer();
|
||
if (walked) {
|
||
return walked;
|
||
}
|
||
} catch (err) {
|
||
console.error('[tables] failed to load table from server', err);
|
||
showStatus('Could not load table: ' + (err && err.message ? err.message : err));
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
function readInlineContext() {
|
||
const el = document.getElementById('table-context');
|
||
if (!el) {
|
||
return null;
|
||
}
|
||
try {
|
||
return JSON.parse(el.textContent || '{}');
|
||
} catch (err) {
|
||
console.error('[tables] failed to parse #table-context', err);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function showStatus(msg) {
|
||
const el = document.getElementById('table-status');
|
||
if (!el) return;
|
||
el.textContent = msg;
|
||
el.hidden = false;
|
||
}
|
||
|
||
async function walkServer() {
|
||
const source = window.zddc && window.zddc.source;
|
||
if (!source) {
|
||
throw new Error('zddc.source not available');
|
||
}
|
||
const tableName = tableNameFromUrl(location.pathname);
|
||
if (!tableName) {
|
||
throw new Error('Unrecognized table URL: ' + location.pathname);
|
||
}
|
||
const probe = await source.detectServerRoot();
|
||
if (!probe.handle) {
|
||
throw new Error(probe.status === 403
|
||
? 'No permission to list this directory'
|
||
: 'Server unreachable');
|
||
}
|
||
const dir = probe.handle;
|
||
|
||
const zddcDoc = await readYaml(dir, '.zddc');
|
||
const tablesMap = (zddcDoc && zddcDoc.tables) || {};
|
||
const specRel = tablesMap[tableName];
|
||
if (!specRel) {
|
||
throw new Error('No tables.' + tableName + ' declared in .zddc');
|
||
}
|
||
const spec = await readYaml(dir, stripDotSlash(specRel));
|
||
if (!spec || !Array.isArray(spec.columns)) {
|
||
throw new Error('Spec ' + specRel + ' missing columns[]');
|
||
}
|
||
|
||
const rowsRel = stripDotSlash(spec.rows || ('./' + tableName));
|
||
const rowsDir = await resolveDirectory(dir, rowsRel);
|
||
const rows = await readRows(rowsDir, rowsRel, tableName);
|
||
|
||
return {
|
||
title: spec.title,
|
||
description: spec.description,
|
||
columns: spec.columns,
|
||
defaults: spec.defaults,
|
||
rows: rows
|
||
};
|
||
}
|
||
|
||
function tableNameFromUrl(pathname) {
|
||
const m = String(pathname || '').match(/\/([^\/]+)\.table\.html$/);
|
||
return m ? m[1] : null;
|
||
}
|
||
|
||
function stripDotSlash(p) {
|
||
let out = String(p || '');
|
||
if (out.startsWith('./')) out = out.slice(2);
|
||
if (out.startsWith('/')) out = out.slice(1);
|
||
if (out.endsWith('/')) out = out.slice(0, -1);
|
||
return out;
|
||
}
|
||
|
||
async function readYaml(dir, relPath) {
|
||
const fileHandle = await resolveFile(dir, relPath);
|
||
const file = await fileHandle.getFile();
|
||
const text = await file.text();
|
||
if (!window.jsyaml) {
|
||
throw new Error('js-yaml not loaded');
|
||
}
|
||
return window.jsyaml.load(text);
|
||
}
|
||
|
||
// Walk a "/"-separated relative path under dir, returning the
|
||
// FileSystemFileHandle (or HttpFileHandle) at the leaf.
|
||
async function resolveFile(dir, relPath) {
|
||
const parts = relPath.split('/').filter(Boolean);
|
||
if (parts.length === 0) {
|
||
throw new Error('Empty file path');
|
||
}
|
||
const fileName = parts.pop();
|
||
let cur = dir;
|
||
for (let i = 0; i < parts.length; i++) {
|
||
cur = await cur.getDirectoryHandle(parts[i]);
|
||
}
|
||
return cur.getFileHandle(fileName);
|
||
}
|
||
|
||
async function resolveDirectory(dir, relPath) {
|
||
const parts = relPath.split('/').filter(Boolean);
|
||
let cur = dir;
|
||
for (let i = 0; i < parts.length; i++) {
|
||
cur = await cur.getDirectoryHandle(parts[i]);
|
||
}
|
||
return cur;
|
||
}
|
||
|
||
async function readRows(rowsDir, rowsRel, tableName) {
|
||
const rows = [];
|
||
for await (const entry of rowsDir.values()) {
|
||
if (entry.kind !== 'file') continue;
|
||
if (!entry.name.endsWith('.yaml')) continue;
|
||
try {
|
||
const file = await (await rowsDir.getFileHandle(entry.name)).getFile();
|
||
const data = window.jsyaml.load(await file.text());
|
||
rows.push({
|
||
url: rowEditUrl(rowsRel, tableName, entry.name),
|
||
data: data || {},
|
||
editable: true
|
||
});
|
||
} catch (err) {
|
||
console.warn('[tables] skipping unparseable row', entry.name, err);
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
// Build the form-handler URL for editing one row. The page is at
|
||
// <dir>/<tableName>.table.html; the row file lives at
|
||
// <dir>/<rowsRel>/<basename>.yaml; the form re-edit URL is
|
||
// <dir>/<rowsRel>/<basename>.yaml.html.
|
||
function rowEditUrl(rowsRel, tableName, rowFileName) {
|
||
const pageDir = location.pathname.replace(/\/[^\/]+\.table\.html$/, '/');
|
||
const rowsPath = pageDir + (rowsRel || tableName) + '/';
|
||
return rowsPath + rowFileName + '.html';
|
||
}
|
||
|
||
app.modules.context = { load: load };
|
||
})(window.tablesApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const util = {};
|
||
|
||
util.h = function (tag, attrs) {
|
||
const el = document.createElement(tag);
|
||
if (attrs) {
|
||
for (const k of Object.keys(attrs)) {
|
||
const v = attrs[k];
|
||
if (v == null || v === false) {
|
||
continue;
|
||
}
|
||
if (k === 'className') {
|
||
el.className = v;
|
||
} else if (k.length > 2 && k.slice(0, 2) === 'on' && typeof v === 'function') {
|
||
el.addEventListener(k.slice(2).toLowerCase(), v);
|
||
} else if (v === true) {
|
||
el.setAttribute(k, '');
|
||
} else {
|
||
el.setAttribute(k, v);
|
||
}
|
||
}
|
||
}
|
||
for (let i = 2; i < arguments.length; i++) {
|
||
const c = arguments[i];
|
||
if (c == null || c === false) {
|
||
continue;
|
||
}
|
||
if (typeof c === 'string' || typeof c === 'number') {
|
||
el.appendChild(document.createTextNode(String(c)));
|
||
} else {
|
||
el.appendChild(c);
|
||
}
|
||
}
|
||
return el;
|
||
};
|
||
|
||
// Resolve a column's `field` against a row data object.
|
||
// - "" or "/" → the whole object
|
||
// - "/foo/bar" → JSON Pointer (RFC 6901) lookup
|
||
// - "foo" → top-level key
|
||
util.resolveField = function (data, field) {
|
||
if (data == null) {
|
||
return undefined;
|
||
}
|
||
if (!field || field === '/') {
|
||
return data;
|
||
}
|
||
if (field.charAt(0) !== '/') {
|
||
return data[field];
|
||
}
|
||
const segments = field.split('/').slice(1).map(function (s) {
|
||
return s.replace(/~1/g, '/').replace(/~0/g, '~');
|
||
});
|
||
let cur = data;
|
||
for (let i = 0; i < segments.length; i++) {
|
||
if (cur == null) {
|
||
return undefined;
|
||
}
|
||
if (Array.isArray(cur)) {
|
||
const idx = parseInt(segments[i], 10);
|
||
if (Number.isNaN(idx)) {
|
||
return undefined;
|
||
}
|
||
cur = cur[idx];
|
||
} else if (typeof cur === 'object') {
|
||
cur = cur[segments[i]];
|
||
} else {
|
||
return undefined;
|
||
}
|
||
}
|
||
return cur;
|
||
};
|
||
|
||
// Format a raw cell value per column's `format` hint.
|
||
util.formatCell = function (value, format) {
|
||
if (value == null || value === '') {
|
||
return '';
|
||
}
|
||
if (format === 'date') {
|
||
const d = new Date(value);
|
||
if (!isNaN(d.getTime())) {
|
||
return d.toISOString().slice(0, 10);
|
||
}
|
||
return String(value);
|
||
}
|
||
if (format === 'datetime') {
|
||
const d = new Date(value);
|
||
if (!isNaN(d.getTime())) {
|
||
return d.toLocaleString();
|
||
}
|
||
return String(value);
|
||
}
|
||
if (format === 'number') {
|
||
const n = Number(value);
|
||
if (Number.isFinite(n)) {
|
||
return n.toLocaleString();
|
||
}
|
||
return String(value);
|
||
}
|
||
if (format === 'bool' || typeof value === 'boolean') {
|
||
return value ? '✓' : '';
|
||
}
|
||
if (typeof value === 'object') {
|
||
try {
|
||
return JSON.stringify(value);
|
||
} catch (e) {
|
||
return String(value);
|
||
}
|
||
}
|
||
return String(value);
|
||
};
|
||
|
||
// Compare two cell values for sorting. null/undefined sort last.
|
||
// Numbers compared numerically, dates compared as Date, otherwise string compare.
|
||
util.compareCells = function (a, b, format) {
|
||
const aMissing = a == null || a === '';
|
||
const bMissing = b == null || b === '';
|
||
if (aMissing && bMissing) {
|
||
return 0;
|
||
}
|
||
if (aMissing) {
|
||
return 1;
|
||
}
|
||
if (bMissing) {
|
||
return -1;
|
||
}
|
||
if (format === 'date' || format === 'datetime') {
|
||
const da = new Date(a).getTime();
|
||
const db = new Date(b).getTime();
|
||
if (!isNaN(da) && !isNaN(db)) {
|
||
return da - db;
|
||
}
|
||
}
|
||
if (format === 'number' || (typeof a === 'number' && typeof b === 'number')) {
|
||
const na = Number(a);
|
||
const nb = Number(b);
|
||
if (Number.isFinite(na) && Number.isFinite(nb)) {
|
||
return na - nb;
|
||
}
|
||
}
|
||
const sa = String(a).toLowerCase();
|
||
const sb = String(b).toLowerCase();
|
||
if (sa < sb) return -1;
|
||
if (sa > sb) return 1;
|
||
return 0;
|
||
};
|
||
|
||
app.modules.util = util;
|
||
})(window.tablesApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
// A filter is per-column and has one of two shapes:
|
||
// - free-text: { kind: 'contains', value: '<string>' }
|
||
// - enum: { kind: 'enum', value: ['<choice>', ...] }
|
||
// An empty value (empty string or empty array) matches everything.
|
||
|
||
function isEnumColumn(col) {
|
||
return Array.isArray(col.enum) && col.enum.length > 0;
|
||
}
|
||
|
||
function defaultFilterFor(col) {
|
||
return isEnumColumn(col) ? { kind: 'enum', value: [] } : { kind: 'contains', value: '' };
|
||
}
|
||
|
||
function rowMatches(filter, cellValue) {
|
||
if (filter.kind === 'enum') {
|
||
if (!Array.isArray(filter.value) || filter.value.length === 0) {
|
||
return true;
|
||
}
|
||
const s = cellValue == null ? '' : String(cellValue);
|
||
return filter.value.indexOf(s) !== -1;
|
||
}
|
||
// contains
|
||
if (!filter.value) {
|
||
return true;
|
||
}
|
||
const needle = String(filter.value).toLowerCase();
|
||
const hay = cellValue == null ? '' : String(cellValue).toLowerCase();
|
||
return hay.indexOf(needle) !== -1;
|
||
}
|
||
|
||
function isEmpty(filter) {
|
||
if (filter.kind === 'enum') {
|
||
return !Array.isArray(filter.value) || filter.value.length === 0;
|
||
}
|
||
return !filter.value;
|
||
}
|
||
|
||
function apply(rows, columns, filterMap, resolveField) {
|
||
return rows.filter(function (row) {
|
||
for (let i = 0; i < columns.length; i++) {
|
||
const col = columns[i];
|
||
const filter = filterMap[col.field];
|
||
if (!filter || isEmpty(filter)) {
|
||
continue;
|
||
}
|
||
const cellValue = resolveField(row.data, col.field);
|
||
if (!rowMatches(filter, cellValue)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
app.modules.filters = {
|
||
defaultFilterFor: defaultFilterFor,
|
||
isEnumColumn: isEnumColumn,
|
||
isEmpty: isEmpty,
|
||
apply: apply
|
||
};
|
||
})(window.tablesApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
// Sort state is an ordered list of {field, dir} keys; the first is
|
||
// primary, additional keys break ties.
|
||
|
||
function defaultsFromContext(ctx) {
|
||
const defaults = ctx.defaults || {};
|
||
if (Array.isArray(defaults.sort) && defaults.sort.length > 0) {
|
||
return defaults.sort.slice();
|
||
}
|
||
// Fall back to any column with `sort:` set.
|
||
const fromCols = (ctx.columns || []).filter(function (c) { return c.sort; });
|
||
if (fromCols.length > 0) {
|
||
return fromCols.map(function (c) {
|
||
const dir = c.sort === 'desc' ? 'desc' : 'asc';
|
||
return { field: c.field, dir: dir };
|
||
});
|
||
}
|
||
return [];
|
||
}
|
||
|
||
function findColumn(columns, field) {
|
||
for (let i = 0; i < columns.length; i++) {
|
||
if (columns[i].field === field) {
|
||
return columns[i];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Click handler for a header: cycle the sort state for `field`.
|
||
// - Not currently a sort key → add as primary, asc
|
||
// - Currently primary asc → flip to desc
|
||
// - Currently primary desc → remove
|
||
// - Currently secondary → promote to primary, asc
|
||
// Shift-click is meant for additional accumulation but we keep the
|
||
// single-click semantics simple; advanced multi-sort can be a
|
||
// follow-up.
|
||
function cycle(state, field, multi) {
|
||
const idx = state.findIndex(function (s) { return s.field === field; });
|
||
if (multi) {
|
||
if (idx === -1) {
|
||
return state.concat([{ field: field, dir: 'asc' }]);
|
||
}
|
||
const cur = state[idx];
|
||
if (cur.dir === 'asc') {
|
||
const next = state.slice();
|
||
next[idx] = { field: field, dir: 'desc' };
|
||
return next;
|
||
}
|
||
return state.slice(0, idx).concat(state.slice(idx + 1));
|
||
}
|
||
if (idx === -1) {
|
||
return [{ field: field, dir: 'asc' }];
|
||
}
|
||
if (idx === 0) {
|
||
const cur = state[0];
|
||
if (cur.dir === 'asc') {
|
||
return [{ field: field, dir: 'desc' }];
|
||
}
|
||
return [];
|
||
}
|
||
return [{ field: field, dir: 'asc' }];
|
||
}
|
||
|
||
function apply(rows, sortState, columns, util) {
|
||
if (!sortState || sortState.length === 0) {
|
||
return rows;
|
||
}
|
||
const out = rows.slice();
|
||
out.sort(function (a, b) {
|
||
for (let i = 0; i < sortState.length; i++) {
|
||
const key = sortState[i];
|
||
const col = findColumn(columns, key.field);
|
||
const fmt = col ? col.format : '';
|
||
const av = util.resolveField(a.data, key.field);
|
||
const bv = util.resolveField(b.data, key.field);
|
||
const cmp = util.compareCells(av, bv, fmt);
|
||
if (cmp !== 0) {
|
||
return key.dir === 'desc' ? -cmp : cmp;
|
||
}
|
||
}
|
||
return 0;
|
||
});
|
||
return out;
|
||
}
|
||
|
||
function indicator(sortState, field) {
|
||
for (let i = 0; i < sortState.length; i++) {
|
||
if (sortState[i].field === field) {
|
||
const arrow = sortState[i].dir === 'desc' ? ' ▼' : ' ▲';
|
||
if (sortState.length > 1) {
|
||
return arrow + (i + 1);
|
||
}
|
||
return arrow;
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
app.modules.sort = {
|
||
defaultsFromContext: defaultsFromContext,
|
||
cycle: cycle,
|
||
apply: apply,
|
||
indicator: indicator
|
||
};
|
||
})(window.tablesApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
function renderHeader(theadEl, columns, sortState, filterMap, onHeaderClick, onFilterChange) {
|
||
const util = app.modules.util;
|
||
const filters = app.modules.filters;
|
||
const sort = app.modules.sort;
|
||
theadEl.innerHTML = '';
|
||
|
||
const titleRow = util.h('tr', { className: 'zddc-table__title-row' });
|
||
const filterRow = util.h('tr', { className: 'zddc-table__filter-row' });
|
||
|
||
for (let i = 0; i < columns.length; i++) {
|
||
const col = columns[i];
|
||
const indicator = sort.indicator(sortState, col.field);
|
||
const th = util.h('th', {
|
||
className: 'zddc-table__th',
|
||
'data-field': col.field,
|
||
style: col.width ? 'width:' + col.width : null,
|
||
onClick: function (ev) { onHeaderClick(col.field, ev.shiftKey); }
|
||
}, col.title || col.field, indicator);
|
||
titleRow.appendChild(th);
|
||
|
||
const td = util.h('td', { className: 'zddc-table__filter-cell' });
|
||
const f = filterMap[col.field] || filters.defaultFilterFor(col);
|
||
if (filters.isEnumColumn(col)) {
|
||
const select = util.h('select', {
|
||
multiple: true,
|
||
'aria-label': 'Filter ' + (col.title || col.field),
|
||
className: 'zddc-table__filter-enum',
|
||
onChange: function (ev) {
|
||
const opts = ev.target.options;
|
||
const picked = [];
|
||
for (let j = 0; j < opts.length; j++) {
|
||
if (opts[j].selected) {
|
||
picked.push(opts[j].value);
|
||
}
|
||
}
|
||
onFilterChange(col.field, { kind: 'enum', value: picked });
|
||
}
|
||
});
|
||
for (let j = 0; j < col.enum.length; j++) {
|
||
const v = col.enum[j];
|
||
const opt = util.h('option', { value: v }, v);
|
||
if (Array.isArray(f.value) && f.value.indexOf(v) !== -1) {
|
||
opt.selected = true;
|
||
}
|
||
select.appendChild(opt);
|
||
}
|
||
td.appendChild(select);
|
||
} else {
|
||
const input = util.h('input', {
|
||
type: 'text',
|
||
className: 'zddc-table__filter-text',
|
||
placeholder: 'filter…',
|
||
'aria-label': 'Filter ' + (col.title || col.field),
|
||
value: typeof f.value === 'string' ? f.value : '',
|
||
onInput: function (ev) {
|
||
onFilterChange(col.field, { kind: 'contains', value: ev.target.value });
|
||
}
|
||
});
|
||
td.appendChild(input);
|
||
}
|
||
filterRow.appendChild(td);
|
||
}
|
||
|
||
theadEl.appendChild(titleRow);
|
||
theadEl.appendChild(filterRow);
|
||
}
|
||
|
||
function renderBody(tbodyEl, rows, columns) {
|
||
const util = app.modules.util;
|
||
tbodyEl.innerHTML = '';
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const row = rows[i];
|
||
const tr = util.h('tr', {
|
||
className: 'zddc-table__row' + (row.editable ? ' zddc-table__row--editable' : ' zddc-table__row--readonly'),
|
||
'data-url': row.url,
|
||
'data-editable': row.editable ? '1' : '0',
|
||
onClick: function (ev) {
|
||
const target = ev.currentTarget;
|
||
const editable = target.getAttribute('data-editable') === '1';
|
||
const url = target.getAttribute('data-url');
|
||
if (editable && url) {
|
||
// Indirection so tests can intercept without
|
||
// fighting Chromium's location.assign property
|
||
// descriptor. Production calls window.location.assign.
|
||
const nav = (window.tablesApp && window.tablesApp.navigateTo) ||
|
||
function (u) { window.location.assign(u); };
|
||
nav(url);
|
||
}
|
||
}
|
||
});
|
||
for (let c = 0; c < columns.length; c++) {
|
||
const col = columns[c];
|
||
const raw = util.resolveField(row.data, col.field);
|
||
const text = util.formatCell(raw, col.format);
|
||
tr.appendChild(util.h('td', { className: 'zddc-table__cell' }, text));
|
||
}
|
||
tbodyEl.appendChild(tr);
|
||
}
|
||
}
|
||
|
||
function renderRowCount(el, displayed, total) {
|
||
if (!el) return;
|
||
if (displayed === total) {
|
||
el.textContent = total + (total === 1 ? ' row' : ' rows');
|
||
} else {
|
||
el.textContent = displayed + ' of ' + total + ' rows';
|
||
}
|
||
}
|
||
|
||
app.modules.render = {
|
||
header: renderHeader,
|
||
body: renderBody,
|
||
rowCount: renderRowCount
|
||
};
|
||
})(window.tablesApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
async function init() {
|
||
const ctx = await app.modules.context.load();
|
||
app.context = ctx;
|
||
|
||
const titleEl = document.getElementById('table-title');
|
||
if (ctx.title && titleEl) {
|
||
titleEl.textContent = ctx.title;
|
||
document.title = 'ZDDC — ' + ctx.title;
|
||
}
|
||
|
||
const descEl = document.getElementById('table-description');
|
||
if (descEl && ctx.description) {
|
||
descEl.textContent = ctx.description;
|
||
descEl.hidden = false;
|
||
}
|
||
|
||
const tableEl = document.getElementById('table-root');
|
||
const theadEl = tableEl.querySelector('thead');
|
||
const tbodyEl = tableEl.querySelector('tbody');
|
||
const emptyEl = document.getElementById('table-empty');
|
||
const countEl = document.getElementById('table-rowcount');
|
||
const clearBtn = document.getElementById('table-clear-filters');
|
||
|
||
const columns = Array.isArray(ctx.columns) ? ctx.columns : [];
|
||
const allRows = Array.isArray(ctx.rows) ? ctx.rows : [];
|
||
|
||
const state = app.state;
|
||
state.rows = allRows;
|
||
state.sort = app.modules.sort.defaultsFromContext(ctx);
|
||
state.filter = {};
|
||
|
||
// Seed default filters from context.defaults.filter (per-column).
|
||
if (ctx.defaults && ctx.defaults.filter && typeof ctx.defaults.filter === 'object') {
|
||
for (let i = 0; i < columns.length; i++) {
|
||
const col = columns[i];
|
||
const seeded = ctx.defaults.filter[col.field];
|
||
if (seeded == null) {
|
||
continue;
|
||
}
|
||
if (app.modules.filters.isEnumColumn(col)) {
|
||
state.filter[col.field] = {
|
||
kind: 'enum',
|
||
value: Array.isArray(seeded) ? seeded.slice() : [String(seeded)]
|
||
};
|
||
} else {
|
||
state.filter[col.field] = { kind: 'contains', value: String(seeded) };
|
||
}
|
||
}
|
||
}
|
||
|
||
function anyFilterActive() {
|
||
const filters = app.modules.filters;
|
||
const keys = Object.keys(state.filter);
|
||
for (let i = 0; i < keys.length; i++) {
|
||
if (!filters.isEmpty(state.filter[keys[i]])) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function paint() {
|
||
const filtered = app.modules.filters.apply(state.rows, columns, state.filter, app.modules.util.resolveField);
|
||
const sorted = app.modules.sort.apply(filtered, state.sort, columns, app.modules.util);
|
||
app.modules.render.header(theadEl, columns, state.sort, state.filter, onHeaderClick, onFilterChange);
|
||
app.modules.render.body(tbodyEl, sorted, columns);
|
||
app.modules.render.rowCount(countEl, sorted.length, state.rows.length);
|
||
if (emptyEl) {
|
||
emptyEl.hidden = sorted.length > 0 || state.rows.length === 0;
|
||
}
|
||
if (clearBtn) {
|
||
clearBtn.hidden = !anyFilterActive();
|
||
}
|
||
}
|
||
|
||
function onHeaderClick(field, shiftKey) {
|
||
state.sort = app.modules.sort.cycle(state.sort, field, shiftKey);
|
||
paint();
|
||
}
|
||
|
||
function onFilterChange(field, value) {
|
||
state.filter[field] = value;
|
||
paint();
|
||
}
|
||
|
||
if (clearBtn) {
|
||
clearBtn.addEventListener('click', function () {
|
||
state.filter = {};
|
||
paint();
|
||
});
|
||
}
|
||
|
||
paint();
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})(window.tablesApp);
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|