All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 5s
The recent history rewrite (squash of 4 thrash CI commits) made the
chart's previous appVersion (0.0.16-beta-8df0def) reference a now-
dangling commit. The dev pipeline failed clone "remote ref not
found" until we re-bumped to a SHA in the new history.
Re-cut beta with the new HEAD parent (ae75855) so notify-chart-dev
rewrites the chart's appVersion to a SHA the BMCD dev pipeline can
actually fetch. Combined with the Dockerfile clone-via-fetch fix in
tnd-zddc-chart 86c5758 (handles bare SHAs), the dev pipeline should
build cleanly.
11653 lines
607 KiB
HTML
11653 lines
607 KiB
HTML
<!--
|
||
This is a html transmittal template.
|
||
It is meant to be an editable form to fill out transmittal information and
|
||
scan documents to be included in the transmittal, and then complete the
|
||
transmittal files table by parsing the file names according to ZDDC naming
|
||
conventions at https://codeberg.org/VARASYS/ZDDC#file-naming-convention.
|
||
-->
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<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);
|
||
}
|
||
|
||
/* Placeholder for contenteditable elements */
|
||
[data-placeholder]:empty::before {
|
||
content: attr(data-placeholder);
|
||
color: var(--text-muted);
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Hide elements that should be hidden when JavaScript is available */
|
||
[data-hydrate-hide] {
|
||
display: none;
|
||
}
|
||
|
||
@media screen {
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.stack-below-600 {
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.page-container {
|
||
width: 100%;
|
||
max-width: 8.5in;
|
||
margin: 20px auto;
|
||
padding: 0.375in;
|
||
min-height: 0;
|
||
background: var(--bg);
|
||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||
border: 1px solid var(--border);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.stack-below-600 {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.page-container {
|
||
padding: 12px;
|
||
}
|
||
}
|
||
|
||
/* ── Dark mode overrides for transmittal-specific hardcoded colours ──────── */
|
||
/* Covers verify cards, table rows, path-diff, integrity cards, workflow badge */
|
||
/* and Tailwind utility classes used as attributes in template.html. */
|
||
@media screen {
|
||
/* Integrity verify cards */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) .verify-card--ok { background: #052e16; }
|
||
:root:not([data-theme="light"]) .verify-card--fail { background: #2d0c0c; }
|
||
:root:not([data-theme="light"]) .verify-card--draft { background: #2d2305; }
|
||
:root:not([data-theme="light"]) .verify-card--info { background: #0c1a2e; }
|
||
}
|
||
[data-theme="dark"] .verify-card--ok { background: #052e16; }
|
||
[data-theme="dark"] .verify-card--fail { background: #2d0c0c; }
|
||
[data-theme="dark"] .verify-card--draft { background: #2d2305; }
|
||
[data-theme="dark"] .verify-card--info { background: #0c1a2e; }
|
||
|
||
/* Per-row verify result rows in file table */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) tr.verify-match td { background: #052e16; }
|
||
:root:not([data-theme="light"]) tr.verify-mismatch td { background: #2d0c0c; }
|
||
:root:not([data-theme="light"]) tr.verify-missing td { background: #2d2305; }
|
||
:root:not([data-theme="light"]) tr.verify-new td { background: #0c1a2e; }
|
||
}
|
||
[data-theme="dark"] tr.verify-match td { background: #052e16; }
|
||
[data-theme="dark"] tr.verify-mismatch td { background: #2d0c0c; }
|
||
[data-theme="dark"] tr.verify-missing td { background: #2d2305; }
|
||
[data-theme="dark"] tr.verify-new td { background: #0c1a2e; }
|
||
|
||
/* Workflow warning badge */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) .workflow-badge--warn {
|
||
background: #2d2305;
|
||
color: #fcd34d;
|
||
border-color: #92400e;
|
||
}
|
||
}
|
||
[data-theme="dark"] .workflow-badge--warn {
|
||
background: #2d2305;
|
||
color: #fcd34d;
|
||
border-color: #92400e;
|
||
}
|
||
|
||
/* Path diff semantic colours (verify mismatch) */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) .path-diff del {
|
||
color: #fca5a5;
|
||
background: #2d0c0c;
|
||
}
|
||
:root:not([data-theme="light"]) .path-diff ins {
|
||
color: #86efac;
|
||
background: #052e16;
|
||
border-bottom-color: #86efac;
|
||
}
|
||
}
|
||
[data-theme="dark"] .path-diff del { color: #fca5a5; background: #2d0c0c; }
|
||
[data-theme="dark"] .path-diff ins {
|
||
color: #86efac;
|
||
background: #052e16;
|
||
border-bottom-color: #86efac;
|
||
}
|
||
|
||
/* Owner/Project names area and inline bg-white / bg-gray-50 utility classes */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) .header-names {
|
||
background-color: var(--bg-secondary) !important;
|
||
border-color: var(--border) !important;
|
||
}
|
||
:root:not([data-theme="light"]) .text-gray-700 { color: var(--text-muted) !important; }
|
||
:root:not([data-theme="light"]) .bg-white { background-color: var(--bg) !important; }
|
||
:root:not([data-theme="light"]) .bg-gray-50 { background-color: var(--bg-secondary) !important; }
|
||
:root:not([data-theme="light"]) .bg-gray-100 { background-color: var(--bg-secondary) !important; }
|
||
:root:not([data-theme="light"]) .border-gray-100,
|
||
:root:not([data-theme="light"]) .border-gray-200,
|
||
:root:not([data-theme="light"]) .border-gray-300 { border-color: var(--border) !important; }
|
||
:root:not([data-theme="light"]) .text-gray-900 { color: var(--text) !important; }
|
||
}
|
||
[data-theme="dark"] .header-names {
|
||
background-color: var(--bg-secondary) !important;
|
||
border-color: var(--border) !important;
|
||
}
|
||
[data-theme="dark"] .text-gray-700 { color: var(--text-muted) !important; }
|
||
[data-theme="dark"] .bg-white { background-color: var(--bg) !important; }
|
||
[data-theme="dark"] .bg-gray-50 { background-color: var(--bg-secondary) !important; }
|
||
[data-theme="dark"] .bg-gray-100 { background-color: var(--bg-secondary) !important; }
|
||
[data-theme="dark"] .border-gray-100,
|
||
[data-theme="dark"] .border-gray-200,
|
||
[data-theme="dark"] .border-gray-300 { border-color: var(--border) !important; }
|
||
[data-theme="dark"] .text-gray-900 { color: var(--text) !important; }
|
||
|
||
/* Filter inputs in table column headers */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) .table-filter-input {
|
||
background-color: var(--bg);
|
||
color: var(--text);
|
||
border-color: var(--border);
|
||
}
|
||
:root:not([data-theme="light"]) .table-header__caption { color: var(--text-muted); }
|
||
:root:not([data-theme="light"]) .focus\:bg-white:focus { background-color: var(--bg) !important; }
|
||
}
|
||
[data-theme="dark"] .table-filter-input {
|
||
background-color: var(--bg);
|
||
color: var(--text);
|
||
border-color: var(--border);
|
||
}
|
||
[data-theme="dark"] .table-header__caption { color: var(--text-muted); }
|
||
[data-theme="dark"] .focus\:bg-white:focus { background-color: var(--bg) !important; }
|
||
}
|
||
|
||
/* Logo row: flex layout — logo | title | logo */
|
||
.logo-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
width: 100%;
|
||
}
|
||
|
||
.logo-cell {
|
||
width: 250px;
|
||
height: 60px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.logo-img {
|
||
display: block;
|
||
width: 250px;
|
||
height: 60px;
|
||
object-fit: contain;
|
||
user-select: none;
|
||
}
|
||
|
||
#left-logo-cell .logo-img { object-position: left center; }
|
||
#right-logo-cell .logo-img { object-position: right center; }
|
||
|
||
.logo-img:not([src]),
|
||
.logo-img[src=""] {
|
||
display: none;
|
||
}
|
||
|
||
.logo-placeholder {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 2px dashed var(--border-dark);
|
||
border-radius: 0.375rem;
|
||
color: var(--text-muted);
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.logo-cell.has-logo .logo-placeholder,
|
||
.logo-placeholder.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.title-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2px;
|
||
text-align: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Type combo dropdown */
|
||
.type-combo {
|
||
position: relative;
|
||
width: 100%;
|
||
}
|
||
|
||
.type-display {
|
||
display: block;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
outline: none;
|
||
min-height: 1.5em;
|
||
}
|
||
|
||
.type-display:empty::before {
|
||
content: attr(data-placeholder);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.type-combo__menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 0.375rem;
|
||
box-shadow: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -2px rgba(0,0,0,.1);
|
||
z-index: 50;
|
||
min-width: 160px;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.type-combo__option {
|
||
display: block;
|
||
width: 100%;
|
||
text-align: center;
|
||
padding: 6px 16px;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: var(--text);
|
||
}
|
||
|
||
.type-combo__option:hover {
|
||
background: var(--bg-hover);
|
||
color: var(--primary);
|
||
}
|
||
|
||
/* ── Targeted drop zones ─────────────────────────────────────────────────── */
|
||
|
||
/* Base: relative so the label can be absolute-positioned inside */
|
||
[data-drop-zone] {
|
||
position: relative;
|
||
border-radius: 0.375rem;
|
||
transition: outline 0.12s, background 0.12s, opacity 0.12s;
|
||
}
|
||
|
||
/* Label: hidden by default, centered overlay inside the zone */
|
||
.drop-zone-label {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: absolute;
|
||
inset: 0;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 0.375rem;
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
line-height: 1.3;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
transition: opacity 0.12s;
|
||
z-index: 30;
|
||
white-space: pre-line;
|
||
}
|
||
|
||
/* --- Visible state: outline appears, label fades in --- */
|
||
[data-drop-zone].dz-visible .drop-zone-label {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Eligible zone: blue dashed outline + subtle blue tint */
|
||
[data-drop-zone].dz-eligible {
|
||
outline: 2px dashed #60a5fa;
|
||
outline-offset: 2px;
|
||
background: rgba(239, 246, 255, 0.55);
|
||
}
|
||
[data-drop-zone].dz-eligible .drop-zone-label {
|
||
color: #1e40af;
|
||
background: rgba(239, 246, 255, 0.9);
|
||
}
|
||
|
||
/* Ineligible zone: grey dashed outline, dimmed */
|
||
[data-drop-zone].dz-ineligible {
|
||
outline: 2px dashed #d1d5db;
|
||
outline-offset: 2px;
|
||
opacity: 0.5;
|
||
}
|
||
[data-drop-zone].dz-ineligible .drop-zone-label {
|
||
color: #6b7280;
|
||
background: rgba(249, 250, 251, 0.85);
|
||
}
|
||
|
||
/* Hover over eligible zone: solid outline, stronger tint, white-on-blue label */
|
||
[data-drop-zone].dz-hover {
|
||
outline: 2px solid #2563eb;
|
||
outline-offset: 2px;
|
||
background: rgba(219, 234, 254, 0.85);
|
||
opacity: 1;
|
||
}
|
||
[data-drop-zone].dz-hover .drop-zone-label {
|
||
color: #1e3a8a;
|
||
background: rgba(191, 219, 254, 0.95);
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* Logo cells: label sits centered over the cell content */
|
||
#left-logo-cell[data-drop-zone] .drop-zone-label,
|
||
#right-logo-cell[data-drop-zone] .drop-zone-label {
|
||
font-size: 0.65rem;
|
||
}
|
||
|
||
/* File-table zone wrapper: display:block, no extra spacing */
|
||
#file-table-drop-zone {
|
||
display: block;
|
||
}
|
||
|
||
@media screen {
|
||
.page-header .header-names {
|
||
padding: 6px 8px;
|
||
}
|
||
|
||
.page-header .header-names h2 {
|
||
margin: 0;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.page-header #project-number {
|
||
margin: 2px 0 6px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.page-header .grid {
|
||
column-gap: 1rem;
|
||
row-gap: 0.5rem;
|
||
}
|
||
|
||
/* .action-button removed — transmittal buttons now use .btn/.btn-primary/.btn-secondary */
|
||
|
||
|
||
/* ── Integrity section layout ──────────────────────── */
|
||
.integrity-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 0.5rem;
|
||
width: 100%;
|
||
}
|
||
|
||
/* ── Verify cards ──────────────────────────────────── */
|
||
.verify-card {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 0.25rem 0.75rem;
|
||
padding: 0.5rem 0.75rem;
|
||
border-left: 4px solid;
|
||
border-radius: 0.25rem;
|
||
font-size: 0.75rem;
|
||
line-height: 1.5;
|
||
}
|
||
.verify-card--ok { background: #f0fdf4; border-left-color: #22c55e; }
|
||
.verify-card--fail { background: #fef2f2; border-left-color: #ef4444; }
|
||
.verify-card--draft { background: #fefce8; border-left-color: #f59e0b; }
|
||
.verify-card--info { background: #eff6ff; border-left-color: #3b82f6; }
|
||
|
||
.verify-card__status {
|
||
font-weight: 700;
|
||
font-size: 0.8rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
}
|
||
.verify-card__status--ok { color: #166534; }
|
||
.verify-card__status--fail { color: #991b1b; }
|
||
.verify-card__status--draft { color: #92400e; }
|
||
.verify-card__status--info { color: #1e40af; }
|
||
|
||
.verify-card__detail {
|
||
font-size: 0.7rem;
|
||
color: var(--text-muted);
|
||
}
|
||
.verify-card__detail code {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.65rem;
|
||
word-break: break-all;
|
||
color: var(--primary);
|
||
}
|
||
|
||
/* Workflow Area */
|
||
.workflow-area {
|
||
border-top: 1px solid var(--border);
|
||
padding-top: 0.75rem;
|
||
margin-top: 0.75rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.875rem;
|
||
}
|
||
|
||
.workflow-step {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.375rem;
|
||
}
|
||
|
||
.workflow-step__label {
|
||
font-size: 0.6875rem;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
margin: 0;
|
||
}
|
||
|
||
.workflow-step__body {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.workflow-dir {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.625rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.workflow-hint {
|
||
font-size: 0.625rem;
|
||
color: var(--text-muted);
|
||
font-style: italic;
|
||
}
|
||
|
||
.workflow-badge {
|
||
font-size: 0.625rem;
|
||
padding: 0.0625rem 0.375rem;
|
||
border: 1px solid;
|
||
border-radius: 0.25rem;
|
||
margin-left: 0.375rem;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.workflow-badge--warn {
|
||
background: #fef9c3;
|
||
color: #92400e;
|
||
border-color: #fcd34d;
|
||
}
|
||
|
||
/* Tools row — compact, de-emphasized */
|
||
.workflow-tools {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
padding-top: 0.5rem;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
/* ── Table toolbar (above table) ───────────────────────── */
|
||
.table-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 0.25rem 0.5rem;
|
||
padding: 0.375rem 0;
|
||
border-top: 1px solid var(--border);
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.toolbar-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
color: var(--primary);
|
||
cursor: pointer;
|
||
padding: 0.15rem 0.35rem;
|
||
border-radius: 0.25rem;
|
||
}
|
||
|
||
.toolbar-btn:hover {
|
||
text-decoration: underline;
|
||
background: var(--primary-light);
|
||
}
|
||
|
||
/* ── App chrome header (lives outside the transmittal) ── */
|
||
/* shared/base.css provides display, align-items, background, border-bottom */
|
||
.app-header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1200;
|
||
gap: 0.5rem;
|
||
padding: 0.3rem 0.75rem;
|
||
/* Expose height for downstream sticky offsets */
|
||
--app-header-height: 2.1rem;
|
||
height: var(--app-header-height);
|
||
box-sizing: content-box;
|
||
}
|
||
|
||
/* ── Fixed footer status bar at viewport bottom ───────── */
|
||
.page-footer {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 50;
|
||
background: var(--bg-secondary);
|
||
border-top: 1px solid var(--border);
|
||
padding: 0.2rem 0.75rem;
|
||
}
|
||
|
||
.page-footer__inner {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
max-width: 8.5in;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-container {
|
||
padding-bottom: 2rem;
|
||
}
|
||
|
||
/* ── Split-button ─────────────────────────────────────── */
|
||
.split-button {
|
||
position: relative;
|
||
display: inline-flex;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.split-button > .btn:last-of-type {
|
||
border-top-left-radius: 0;
|
||
border-bottom-left-radius: 0;
|
||
}
|
||
|
||
.split-button__toggle {
|
||
border-top-right-radius: 0;
|
||
border-bottom-right-radius: 0;
|
||
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||
min-width: 28px;
|
||
padding-left: 6px;
|
||
padding-right: 6px;
|
||
font-size: 0.75rem;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* ── Dropdown menu ──────────────────────────────────────── */
|
||
.dropdown {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.dropdown-toggle {
|
||
min-width: 28px;
|
||
font-size: 1rem;
|
||
letter-spacing: 0.1em;
|
||
line-height: 1;
|
||
}
|
||
|
||
.dropdown-menu {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 100%;
|
||
margin-top: 2px;
|
||
min-width: 120px;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 0.375rem;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
z-index: 1100;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.dropdown-menu.dropdown-menu--up {
|
||
bottom: 100%;
|
||
top: auto;
|
||
margin-top: 0;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
/* ── Dropdown separators ─────────────────────────────────── */
|
||
.dropdown-separator {
|
||
border-top: 1px solid var(--border);
|
||
margin: 4px 0;
|
||
padding: 0;
|
||
font-size: 0.625rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.dropdown-separator:empty {
|
||
padding: 0;
|
||
}
|
||
|
||
.dropdown-separator:not(:empty) {
|
||
padding: 4px 12px 2px;
|
||
}
|
||
|
||
/* ── Scanning blur on table ─────────────────────────────── */
|
||
.table-wrapper.scanning {
|
||
filter: blur(1px);
|
||
opacity: 0.7;
|
||
pointer-events: none;
|
||
transition: filter 0.3s ease, opacity 0.3s ease;
|
||
}
|
||
|
||
.table-wrapper {
|
||
transition: filter 0.3s ease, opacity 0.3s ease;
|
||
}
|
||
|
||
/* ── Hash validation indicators ──────────────────────────── */
|
||
.hash-match {
|
||
color: #16a34a;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.hash-mismatch {
|
||
color: #dc2626;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* ── Inline hash progress bar ────────────────────────────── */
|
||
.hash-progress {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
width: 100%;
|
||
}
|
||
|
||
.hash-progress-bar {
|
||
flex: 1;
|
||
height: 6px;
|
||
background: var(--border);
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hash-progress-fill {
|
||
height: 100%;
|
||
width: 0%;
|
||
background: var(--primary);
|
||
border-radius: 3px;
|
||
transition: width 0.15s ease;
|
||
}
|
||
|
||
.hash-progress-fill.done {
|
||
background: #22c55e;
|
||
width: 100%;
|
||
}
|
||
|
||
/* ── Per-row verify result styling ───────────────────────── */
|
||
tr.verify-match td { background: #dcfce7; }
|
||
tr.verify-mismatch td { background: #fee2e2; }
|
||
tr.verify-missing td { background: #fef3c7; }
|
||
tr.verify-new td { background: #dbeafe; }
|
||
tr.verify-progress td:last-child { color: #6b7280; font-style: italic; }
|
||
|
||
.dropdown-item {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 6px 12px;
|
||
border: none;
|
||
background: none;
|
||
text-align: left;
|
||
font-size: 0.8rem;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.dropdown-item:hover {
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.dropdown-item:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
}
|
||
|
||
@media screen {
|
||
#transmittal-form .page-header .relative > input[type="text"] {
|
||
padding-top: 0.625rem;
|
||
padding-bottom: 0.25rem;
|
||
border-top: 0 !important;
|
||
border-left: 0 !important;
|
||
border-right: 0 !important;
|
||
border-bottom: 1px solid var(--border) !important;
|
||
border-radius: 0 !important;
|
||
background-color: var(--bg);
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
#transmittal-form .page-header .relative > input[type="text"]:focus {
|
||
border-bottom-color: var(--primary);
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
/* Remove underlines from title area inputs (type and title) */
|
||
#transmittal-form .page-header .title-area input[type="text"] {
|
||
border-bottom: 0 !important;
|
||
}
|
||
|
||
#transmittal-form .page-header label[for],
|
||
#transmittal-form .page-header .relative > label {
|
||
position: absolute;
|
||
left: 0.5rem;
|
||
top: -0.5rem;
|
||
font-weight: 700;
|
||
background: var(--bg);
|
||
padding: 0 0.25rem;
|
||
font-size: 10px;
|
||
color: var(--text-muted);
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
|
||
#transmittal-form .page-header .relative {
|
||
margin-top: 0.375rem;
|
||
}
|
||
|
||
/* Boxed inputs (full-border, floating label) inside the grid need extra top
|
||
margin so the absolute-positioned label (-top-2 = -0.5rem) is not clipped */
|
||
#transmittal-form .page-header .grid .relative {
|
||
margin-top: 0.75rem;
|
||
}
|
||
|
||
/* ── From field rendered mailto link ──────────────────────── */
|
||
.from-render {
|
||
font-size: 12px;
|
||
font-family: var(--font-mono);
|
||
line-height: 1.6;
|
||
padding: 0.625rem 0.5rem 0.25rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.from-mailto {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.from-mailto:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* ── To field rendered mailto links ───────────────────────── */
|
||
.to-render {
|
||
font-size: 12px;
|
||
font-family: var(--font-mono);
|
||
line-height: 1.6;
|
||
padding: 0.625rem 0.5rem 0.25rem;
|
||
border-bottom: 1px solid var(--border);
|
||
word-break: break-word;
|
||
}
|
||
|
||
.to-mailto {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.to-mailto:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.to-sep {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
}
|
||
|
||
@media screen {
|
||
.table-wrapper {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.table-wrapper table {
|
||
table-layout: auto;
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
/* Header cells — stick below app-header (height + padding + border) */
|
||
.table-wrapper thead th {
|
||
white-space: nowrap;
|
||
position: sticky;
|
||
top: calc(2.1rem + 0.6rem + 1px);
|
||
z-index: 1000;
|
||
background-color: var(--bg-secondary);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
border-left: 1px solid var(--border);
|
||
border-right: 1px solid var(--border);
|
||
border-bottom: 1px solid var(--border);
|
||
border-top: none;
|
||
}
|
||
.table-wrapper thead th:first-child { border-left: none; }
|
||
.table-wrapper thead th:last-child { border-right: none; }
|
||
|
||
/* Body cells: all gray borders */
|
||
.table-wrapper tbody td {
|
||
white-space: nowrap;
|
||
border-left: 1px solid var(--border);
|
||
border-right: 1px solid var(--border);
|
||
border-top: 1px solid var(--border);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.table-wrapper tbody td:first-child { border-left: none; }
|
||
.table-wrapper tbody td:last-child { border-right: none; }
|
||
.table-wrapper tbody tr:first-child td { border-top: none; }
|
||
.table-wrapper tbody tr:last-child td { border-bottom: none; }
|
||
|
||
/* Self-entry row */
|
||
.table-wrapper tbody tr.self-entry td {
|
||
background-color: var(--bg-hover);
|
||
}
|
||
|
||
/* 3: Title — only column that wraps */
|
||
.table-wrapper thead th:nth-child(3) {
|
||
white-space: normal;
|
||
}
|
||
|
||
.table-wrapper tbody td:nth-child(3) {
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
/* 7: Size — right aligned */
|
||
.table-wrapper thead th:nth-child(7),
|
||
.table-wrapper tbody td:nth-child(7) {
|
||
text-align: right;
|
||
}
|
||
|
||
/* Row delete button */
|
||
.row-delete-btn {
|
||
display: none;
|
||
border: none;
|
||
background: none;
|
||
color: #dc2626;
|
||
font-size: 0.85rem;
|
||
line-height: 1;
|
||
padding: 0 0.2rem;
|
||
margin-right: 0.15rem;
|
||
cursor: pointer;
|
||
vertical-align: middle;
|
||
opacity: 0.5;
|
||
}
|
||
.row-delete-btn:hover {
|
||
opacity: 1;
|
||
}
|
||
.table-wrapper tbody tr:hover .row-delete-btn {
|
||
display: inline;
|
||
}
|
||
|
||
/* Path diff indicator (verify mismatch) */
|
||
.path-diff {
|
||
font-size: 0.65rem;
|
||
line-height: 1.3;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.path-diff del {
|
||
color: #991b1b;
|
||
background: #fef2f2;
|
||
}
|
||
|
||
.path-diff ins {
|
||
color: #166534;
|
||
background: #f0fdf4;
|
||
text-decoration: none;
|
||
border-bottom: 1px solid #166534;
|
||
}
|
||
}
|
||
|
||
#remarks-render-container {
|
||
padding: 6px;
|
||
}
|
||
|
||
/* In edit mode the rendered preview is clickable to re-enter the editor */
|
||
#remarks-render-container.remarks-clickable {
|
||
cursor: text;
|
||
border: 1px dashed #d1d5db;
|
||
border-radius: 0.25rem;
|
||
min-height: 3.5rem;
|
||
}
|
||
|
||
#remarks-render-container.remarks-clickable:hover {
|
||
border-color: #93c5fd;
|
||
background: #f0f7ff;
|
||
}
|
||
|
||
/* Placeholder shown when remarks are empty and editable */
|
||
#remarks-render-container .remarks-placeholder {
|
||
color: #9ca3af;
|
||
font-style: italic;
|
||
font-size: 12px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
#remarks-render h1,
|
||
#remarks-render h2,
|
||
#remarks-render h3,
|
||
#remarks-render h4,
|
||
#remarks-render h5,
|
||
#remarks-render h6 {
|
||
margin: 0.5rem 0 0.25rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
#remarks-render h1 {
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
#remarks-render h2 {
|
||
font-size: 1.125rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
#remarks-render h3 {
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
#remarks-render h4 {
|
||
font-size: 0.9375rem;
|
||
}
|
||
|
||
#remarks-render h5 {
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
#remarks-render h6 {
|
||
font-size: 0.8125rem;
|
||
}
|
||
|
||
#remarks-render p {
|
||
margin: 0.5rem 0 0.85rem;
|
||
}
|
||
|
||
#remarks-render ul {
|
||
list-style: disc inside;
|
||
padding-left: 0.75rem;
|
||
margin: 0.25rem 0;
|
||
}
|
||
|
||
#remarks-render ol {
|
||
list-style: decimal inside;
|
||
padding-left: 0.75rem;
|
||
margin: 0.25rem 0;
|
||
}
|
||
|
||
#remarks-render li {
|
||
margin: 0.125rem 0;
|
||
}
|
||
|
||
#remarks-render blockquote {
|
||
border-left: 3px solid #e5e7eb;
|
||
padding-left: 0.75rem;
|
||
color: #374151;
|
||
margin: 0.25rem 0;
|
||
}
|
||
|
||
#remarks-render pre {
|
||
background: #f9fafb;
|
||
border: 1px solid #e5e7eb;
|
||
padding: 0.5rem;
|
||
border-radius: 0.25rem;
|
||
overflow: auto;
|
||
}
|
||
|
||
#remarks-render code {
|
||
background: #f3f4f6;
|
||
border: 1px solid #e5e7eb;
|
||
padding: 0 0.25rem;
|
||
border-radius: 0.125rem;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||
}
|
||
|
||
#remarks-render hr {
|
||
border: 0;
|
||
border-top: 1px solid #e5e7eb;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
#remarks-render del {
|
||
text-decoration: line-through;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
#remarks-render a {
|
||
color: #2563eb;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
#remarks-render table {
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
margin: 0.25rem 0;
|
||
}
|
||
|
||
#remarks-render th,
|
||
#remarks-render td {
|
||
border: 1px solid #d1d5db;
|
||
padding: 0.25rem 0.5rem;
|
||
text-align: left;
|
||
}
|
||
|
||
#remarks-render th {
|
||
background: #f3f4f6;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* ── Button bar ───────────────────────────────────────────── */
|
||
.md-toolbar {
|
||
display: flex;
|
||
gap: 2px;
|
||
padding: 3px 4px;
|
||
background: #f3f4f6;
|
||
border: 1px solid #d1d5db;
|
||
border-bottom: none;
|
||
border-radius: 0.25rem 0.25rem 0 0;
|
||
}
|
||
|
||
.md-toolbar-btn {
|
||
min-width: 26px;
|
||
height: 24px;
|
||
padding: 0 5px;
|
||
border: 1px solid transparent;
|
||
border-radius: 3px;
|
||
background: transparent;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
cursor: pointer;
|
||
line-height: 1;
|
||
}
|
||
|
||
.md-toolbar-btn:hover {
|
||
background: #e5e7eb;
|
||
border-color: #d1d5db;
|
||
}
|
||
|
||
.md-toolbar-btn:active {
|
||
background: #d1d5db;
|
||
}
|
||
|
||
/* ── Edit area ────────────────────────────────────────── */
|
||
.md-edit-area {
|
||
position: relative;
|
||
min-height: 80px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 0 0 0.25rem 0.25rem;
|
||
}
|
||
|
||
.md-input {
|
||
display: block;
|
||
width: 100%;
|
||
min-height: 80px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
padding: 6px 8px;
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
overflow-wrap: break-word;
|
||
tab-size: 4;
|
||
border: none;
|
||
outline: none;
|
||
resize: vertical;
|
||
background: #fff;
|
||
color: #111827;
|
||
}
|
||
|
||
.md-edit-area:focus-within {
|
||
border-color: #93c5fd;
|
||
box-shadow: 0 0 0 1px #93c5fd;
|
||
}
|
||
|
||
.table-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 0.2rem;
|
||
width: 100%;
|
||
padding: 0;
|
||
}
|
||
|
||
.table-header__caption {
|
||
font-weight: 600;
|
||
font-size: 0.7rem;
|
||
letter-spacing: 0.04em;
|
||
color: var(--text-muted);
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.table-header-cell {
|
||
padding: 0;
|
||
}
|
||
|
||
/* Transmittal uses a denser table — override shared .column-filter sizing */
|
||
.column-filter {
|
||
font-size: 0.65rem;
|
||
padding: 0.1rem 0.3rem;
|
||
margin-top: 0.15rem;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
/* ── Native <dialog> base ─────────────────────────────── */
|
||
dialog.modal {
|
||
border: none;
|
||
border-radius: 0.75rem;
|
||
max-width: 32rem;
|
||
width: min(90%, 32rem);
|
||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25);
|
||
padding: 1.5rem;
|
||
gap: 1.25rem;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
}
|
||
|
||
dialog.modal[open] {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
dialog.modal::backdrop {
|
||
background: rgba(17, 24, 39, 0.55);
|
||
}
|
||
|
||
/* Prevent background scroll while a modal dialog is open */
|
||
html:has(dialog.modal[open]) {
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ── Shared modal layout ─────────────────────────────── */
|
||
.modal__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.modal__title {
|
||
font-size: 1.25rem;
|
||
line-height: 1.75rem;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin: 0;
|
||
}
|
||
|
||
.modal__close {
|
||
background: transparent;
|
||
border: none;
|
||
font-size: 1.5rem;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.modal__body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.modal__options {
|
||
display: grid;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.modal__footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.modal__feedback {
|
||
min-height: 1.25rem;
|
||
font-size: 0.85rem;
|
||
color: var(--danger);
|
||
}
|
||
|
||
/* ── Narrow modal variant ────────────────────────────── */
|
||
dialog.modal--narrow {
|
||
max-width: 24rem;
|
||
width: min(90%, 24rem);
|
||
}
|
||
|
||
/* ── Publish field styles ────────────────────────────── */
|
||
.publish-field__label {
|
||
display: block;
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.publish-field__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.publish-field__input {
|
||
border: 1px solid var(--border);
|
||
border-radius: 0.375rem;
|
||
padding: 0.35rem 0.5rem;
|
||
font-size: 0.85rem;
|
||
width: 100%;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
}
|
||
|
||
.publish-field__warning {
|
||
font-size: 0.7rem;
|
||
color: #92400e;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.publish-outputs {
|
||
border: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.35rem;
|
||
}
|
||
|
||
.publish-outputs legend {
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.publish-check {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* ── Key dialog ──────────────────────────────────────── */
|
||
.key-dialog__desc {
|
||
font-size: 0.85rem;
|
||
color: var(--text-muted);
|
||
margin: 0 0 0.75rem;
|
||
}
|
||
|
||
.key-dialog__actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
/* ── Success notification ────────────────────────────── */
|
||
@keyframes notification-slide-in {
|
||
from { transform: translateX(100%); opacity: 0; }
|
||
to { transform: translateX(0); opacity: 1; }
|
||
}
|
||
|
||
.publish-notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: #10b981;
|
||
color: white;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 0.5rem;
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||
z-index: 10000;
|
||
max-width: 400px;
|
||
animation: notification-slide-in 0.3s ease-out;
|
||
}
|
||
|
||
.publish-notification__title {
|
||
font-weight: 600;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.publish-notification__file {
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.publish-notification__close {
|
||
margin-top: 0.5rem;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border: none;
|
||
color: white;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 0.25rem;
|
||
cursor: pointer;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* ── Responsive ──────────────────────────────────────── */
|
||
@media (max-width: 480px) {
|
||
dialog.modal {
|
||
padding: 1.25rem;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.modal__footer {
|
||
flex-direction: column-reverse;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.key-dialog__actions {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
}
|
||
|
||
/* Tailwind-inspired utility subset required by template.html */
|
||
|
||
/* Typography */
|
||
.font-sans { font-family: "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
|
||
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||
.font-semibold { font-weight: 600; }
|
||
.font-bold { font-weight: 700; }
|
||
.italic { font-style: italic; }
|
||
.text-left { text-align: left; }
|
||
.text-center { text-align: center; }
|
||
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||
.text-base { font-size: 1rem; line-height: 1.5rem; }
|
||
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
|
||
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||
.text-\[12px\] { font-size: 12px; line-height: 1.4; }
|
||
.text-\[10px\] { font-size: 10px; line-height: 1.3; }
|
||
.text-gray-900 { color: #111827; }
|
||
.text-gray-700 { color: #374151; }
|
||
.text-gray-600 { color: #4b5563; }
|
||
.text-gray-500 { color: #6b7280; }
|
||
.text-gray-400 { color: #9ca3af; }
|
||
.text-blue-600 { color: #2563eb; }
|
||
.text-green-600 { color: #16a34a; }
|
||
.text-red-600 { color: #dc2626; }
|
||
.uppercase { text-transform: uppercase; }
|
||
.tracking-wide { letter-spacing: 0.1em; }
|
||
.leading-6 { line-height: 1.5rem; }
|
||
.leading-snug { line-height: 1.375rem; }
|
||
|
||
/* Backgrounds */
|
||
.bg-white { background-color: #ffffff; }
|
||
.bg-transparent { background-color: transparent; }
|
||
.bg-gray-50 { background-color: #f9fafb; }
|
||
.bg-gray-100 { background-color: #f3f4f6; }
|
||
|
||
/* Borders */
|
||
.border { border: 1px solid #d1d5db; }
|
||
.border-0 { border: 0; }
|
||
.border-b { border-bottom: 1px solid #d1d5db; }
|
||
.border-t { border-top: 1px solid #d1d5db; }
|
||
.border-gray-300 { border-color: #d1d5db; }
|
||
.border-gray-200 { border-color: #e5e7eb; }
|
||
.border-gray-100 { border-color: #f3f4f6; }
|
||
.rounded-none { border-radius: 0; }
|
||
.rounded-sm { border-radius: 0.125rem; }
|
||
.rounded { border-radius: 0.25rem; }
|
||
.rounded-md { border-radius: 0.375rem; }
|
||
|
||
/* Effects */
|
||
.shadow-lg { box-shadow: 0 10px 25px rgba(15, 23, 42, 0.15); }
|
||
.transition-colors { transition: color 150ms ease, background-color 150ms ease, border-color 150ms ease, box-shadow 150ms ease; }
|
||
|
||
/* Flex / Grid */
|
||
.flex { display: flex; }
|
||
.flex-col { flex-direction: column; }
|
||
.flex-row { flex-direction: row; }
|
||
.flex-1 { flex: 1 1 0%; }
|
||
.items-center { align-items: center; }
|
||
.items-start { align-items: flex-start; }
|
||
.justify-between { justify-content: space-between; }
|
||
.justify-center { justify-content: center; }
|
||
.gap-2 { gap: 0.5rem; }
|
||
.gap-0\.5 { gap: 0.125rem; }
|
||
.grid { display: grid; }
|
||
.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
|
||
.col-span-4 { grid-column: span 4 / span 4; }
|
||
.col-span-6 { grid-column: span 6 / span 6; }
|
||
.col-span-12 { grid-column: span 12 / span 12; }
|
||
|
||
/* Positioning */
|
||
.block { display: block; }
|
||
.relative { position: relative; }
|
||
.absolute { position: absolute; }
|
||
.sticky { position: sticky; }
|
||
.left-0 { left: 0; }
|
||
.top-0 { top: 0; }
|
||
.top-6 { top: 1.5rem; }
|
||
.top-full { top: 100%; }
|
||
.z-10 { z-index: 10; }
|
||
.z-20 { z-index: 20; }
|
||
.z-50 { z-index: 50; }
|
||
|
||
/* Sizing */
|
||
.w-full { width: 100%; }
|
||
.w-auto { width: auto; }
|
||
.w-64 { width: 16rem; }
|
||
.w-5 { width: 1.25rem; }
|
||
.h-5 { height: 1.25rem; }
|
||
.h-6 { height: 1.5rem; }
|
||
.h-auto { height: auto; }
|
||
.min-w-0 { min-width: 0; }
|
||
.max-w-\[1200px\] { max-width: 1200px; }
|
||
.max-w-\[1600px\] { max-width: 1600px; }
|
||
.max-h-96 { max-height: 24rem; }
|
||
.min-h-\[3.5rem\] { min-height: 3.5rem; }
|
||
.overflow-auto { overflow: auto; }
|
||
|
||
/* Spacing */
|
||
.p-2 { padding: 0.5rem; }
|
||
.px-0 { padding-left: 0; padding-right: 0; }
|
||
.px-1 { padding-left: 0.25rem; padding-right: 0.25rem; }
|
||
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
|
||
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||
.px-1\.5 { padding-left: 0.375rem; padding-right: 0.375rem; }
|
||
.py-0 { padding-top: 0; padding-bottom: 0; }
|
||
.py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; }
|
||
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
||
.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
|
||
.pt-1 { padding-top: 0.25rem; }
|
||
.pt-5 { padding-top: 1.25rem; }
|
||
.pb-2 { padding-bottom: 0.5rem; }
|
||
.pb-3 { padding-bottom: 0.75rem; }
|
||
.mt-1 { margin-top: 0.25rem; }
|
||
.mt-2 { margin-top: 0.5rem; }
|
||
.mb-1 { margin-bottom: 0.25rem; }
|
||
.my-1 { margin-top: 0.25rem; margin-bottom: 0.25rem; }
|
||
.gap-x-6 { column-gap: 1.5rem; }
|
||
.gap-y-3 { row-gap: 0.75rem; }
|
||
|
||
/* Drag-hover utilities (used by drop zone handlers) */
|
||
.ring-2 { box-shadow: 0 0 0 2px var(--tw-ring-color, rgba(59, 130, 246, 0.5)); }
|
||
.ring-blue-400 { --tw-ring-color: rgba(96, 165, 250, 0.7); }
|
||
.bg-blue-50 { background-color: #eff6ff; }
|
||
|
||
/* Validation error state (applied via JS in validation.js) */
|
||
.ring-red-400 { box-shadow: 0 0 0 2px rgba(248, 113, 113, 0.5); }
|
||
.border-red-500 { border-color: #ef4444 !important; }
|
||
|
||
/* Hover & focus states */
|
||
.hover\:bg-gray-50:hover { background-color: #f9fafb; }
|
||
.hover\:bg-gray-100:hover { background-color: #f3f4f6; }
|
||
.hover\:underline:hover { text-decoration: underline; }
|
||
.focus\:outline-none:focus { outline: none; }
|
||
.focus\:border-blue-400:focus { border-color: #60a5fa; }
|
||
.focus\:ring-1:focus { box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35); }
|
||
.focus\:ring-blue-400:focus { box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.45); }
|
||
.focus\:bg-white:focus { background-color: #ffffff; }
|
||
.disabled\:pointer-events-none:disabled { pointer-events: none; }
|
||
|
||
/* Table helpers */
|
||
.table-auto { table-layout: auto; }
|
||
.border-collapse { border-collapse: collapse; }
|
||
|
||
/* Print helpers */
|
||
@media print {
|
||
.print\:hidden { display: none !important; }
|
||
}
|
||
|
||
@media print {
|
||
body {
|
||
background: none;
|
||
padding: 0;
|
||
}
|
||
|
||
.page-container {
|
||
width: 100%;
|
||
margin: 0;
|
||
padding: 0;
|
||
min-height: 0;
|
||
box-shadow: none;
|
||
border: none;
|
||
}
|
||
|
||
@page {
|
||
margin: 0.375in;
|
||
size: 8.5in 11in;
|
||
}
|
||
|
||
.table-wrapper {
|
||
overflow: visible !important;
|
||
max-height: none !important;
|
||
}
|
||
|
||
thead th {
|
||
position: static !important;
|
||
top: auto !important;
|
||
}
|
||
|
||
/* Hide interactive-only elements in print */
|
||
.table-filter-input,
|
||
.logo-placeholder,
|
||
.drop-zone-label {
|
||
display: none !important;
|
||
}
|
||
|
||
|
||
/* Adjust table header caption spacing when filters are hidden */
|
||
.table-header__caption {
|
||
display: block;
|
||
padding: 0.25rem 0;
|
||
}
|
||
|
||
table {
|
||
width: 100% !important;
|
||
table-layout: fixed !important;
|
||
border: 1px solid #999 !important;
|
||
border-collapse: collapse !important;
|
||
}
|
||
|
||
th {
|
||
background-color: #e0e0e0 !important;
|
||
-webkit-print-color-adjust: exact;
|
||
print-color-adjust: exact;
|
||
border-left: 1px solid #999 !important;
|
||
border-right: 1px solid #999 !important;
|
||
border-bottom: 1px solid #999 !important;
|
||
border-top: none !important;
|
||
}
|
||
|
||
td {
|
||
border-left: 1px solid #999 !important;
|
||
border-right: 1px solid #999 !important;
|
||
border-top: 1px solid #999 !important;
|
||
border-bottom: 1px solid #999 !important;
|
||
}
|
||
|
||
tr.self-entry td {
|
||
background-color: #f3f4f6 !important;
|
||
-webkit-print-color-adjust: exact;
|
||
print-color-adjust: exact;
|
||
}
|
||
|
||
/* Fixed column widths for print */
|
||
thead th:nth-child(1),
|
||
tbody td:nth-child(1) {
|
||
width: 18% !important;
|
||
}
|
||
|
||
thead th:nth-child(2),
|
||
tbody td:nth-child(2) {
|
||
width: 30% !important;
|
||
}
|
||
|
||
thead th:nth-child(3),
|
||
tbody td:nth-child(3) {
|
||
width: 10% !important;
|
||
}
|
||
|
||
thead th:nth-child(4),
|
||
tbody td:nth-child(4) {
|
||
width: 8% !important;
|
||
}
|
||
|
||
thead th:nth-child(5),
|
||
tbody td:nth-child(5) {
|
||
width: 6% !important;
|
||
}
|
||
|
||
thead th:nth-child(6),
|
||
tbody td:nth-child(6) {
|
||
width: 8% !important;
|
||
}
|
||
|
||
thead th:nth-child(7),
|
||
tbody td:nth-child(7) {
|
||
width: 20% !important;
|
||
}
|
||
|
||
/* Ensure text wraps properly in print */
|
||
tbody td {
|
||
word-wrap: break-word;
|
||
overflow-wrap: break-word;
|
||
}
|
||
|
||
/* SHA256 column - allow breaking */
|
||
tbody td:nth-child(7) {
|
||
word-break: break-all;
|
||
font-size: 7pt !important;
|
||
}
|
||
|
||
/* From field: show rendered mailto link, hide input */
|
||
#from { display: none !important; }
|
||
.from-render {
|
||
display: block !important;
|
||
}
|
||
.from-mailto {
|
||
color: #2563eb !important;
|
||
-webkit-print-color-adjust: exact;
|
||
print-color-adjust: exact;
|
||
}
|
||
|
||
/* To field: show rendered mailto links, hide input */
|
||
#to { display: none !important; }
|
||
.to-render {
|
||
display: block !important;
|
||
}
|
||
.to-mailto {
|
||
color: #2563eb !important;
|
||
-webkit-print-color-adjust: exact;
|
||
print-color-adjust: exact;
|
||
}
|
||
}
|
||
|
||
</style>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ZDDC Transmittal</title>
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZTNhNWYiLz4KICA8ZyBmaWxsPSIjZmZmIj4KICAgIDxyZWN0IHg9IjE0IiB5PSIxOCIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iNDMsMjUgNTAsMjUgMjEsNDMgMTQsNDMiLz4KICAgIDxyZWN0IHg9IjE0IiB5PSI0MyIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICA8L2c+Cjwvc3ZnPgo=">
|
||
</head>
|
||
|
||
<body class="font-sans text-gray-900">
|
||
<header class="app-header print:hidden" data-no-disable="true">
|
||
<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">ZDDC Transmittal</span>
|
||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.16-beta · 2026-05-04 · ae75855</span></span>
|
||
</div>
|
||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||
<!-- Publish split-button (Transmittal-specific primary action;
|
||
other tools have "Add Local Directory" here instead) -->
|
||
<div class="split-button" id="bottom-menu" hidden>
|
||
<button id="bottom-toggle" type="button" class="btn btn-primary split-button__toggle" data-no-disable="true" aria-haspopup="true" aria-expanded="false">▾</button>
|
||
<button id="bottom-primary" type="button" class="btn btn-primary" data-no-disable="true">Publish</button>
|
||
<div class="dropdown-menu hidden" role="menu" id="bottom-dropdown"></div>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<button type="button" id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
||
<button type="button" id="help-btn" class="btn btn-secondary" aria-label="Help" title="Help">?</button>
|
||
</div>
|
||
</header>
|
||
<div class="page-container">
|
||
<form id="transmittal-form">
|
||
<input type="hidden" id="mode" value="edit">
|
||
<input type="hidden" id="published" value="false">
|
||
<header class="page-header flex flex-col gap-2 p-2 max-w-[1600px] relative">
|
||
<div class="logo-row">
|
||
<div class="logo-cell" id="left-logo-cell" data-drop-zone="logo-left">
|
||
<img id="left-logo" class="logo-img" alt="Sender Logo">
|
||
<span class="logo-placeholder">Drop sender logo (optional)</span>
|
||
<span class="drop-zone-label">Drop sender logo</span>
|
||
</div>
|
||
<div class="title-area">
|
||
<div class="type-combo" id="type-combo">
|
||
<input type="hidden" name="type" id="type" value="Transmittal">
|
||
<span id="type-display" role="combobox" contenteditable="true" class="type-display w-full text-2xl font-bold bg-transparent border-0 p-0 focus:outline-none text-center" data-placeholder="Type">Transmittal</span>
|
||
<div class="type-combo__menu hidden" id="type-menu" role="listbox">
|
||
<button type="button" class="type-combo__option" role="option" data-value="Transmittal">Transmittal</button>
|
||
<button type="button" class="type-combo__option" role="option" data-value="Submittal">Submittal</button>
|
||
</div>
|
||
</div>
|
||
<input type="text" name="title" id="title" placeholder="Title" class="w-full text-base italic bg-transparent border-0 p-0 focus:outline-none disabled:pointer-events-none text-center" value="">
|
||
</div>
|
||
<div class="logo-cell" id="right-logo-cell" data-drop-zone="logo-right">
|
||
<img id="right-logo" class="logo-img" alt="Receiver Logo">
|
||
<span class="logo-placeholder">Drop receiver logo (optional)</span>
|
||
<span class="drop-zone-label">Drop receiver logo</span>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<div id="header-info" data-drop-zone="header">
|
||
<div class="drop-zone-label">Drop HTML or JSON to import transmittal data</div>
|
||
<div class="header-names w-full bg-gray-50 border border-gray-100 rounded-md px-3 py-1.5 flex flex-col gap-0.5">
|
||
<h2 id="owner-name" class="text-xl font-semibold" data-placeholder="Owner Name"></h2>
|
||
<h2 id="project-name" class="text-lg font-semibold" data-placeholder="Project Name"></h2>
|
||
<div id="project-number" class="text-sm text-gray-700" data-placeholder="Project Number"></div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-12 gap-x-6 gap-y-3 items-start">
|
||
<!-- Row 1: Tracking Number (A), Date (B) -->
|
||
<div class="col-span-6">
|
||
<div class="relative">
|
||
<input type="text" name="tracking-number" id="tracking-number" placeholder="" class="w-full min-w-0 bg-white border border-gray-300 rounded px-2 pt-5 pb-2 text-[12px] font-mono focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400">
|
||
<label for="tracking-number" class="absolute left-2 -top-2 bg-white px-1 text-[10px] text-gray-700 font-semibold pointer-events-none">Tracking Number</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-span-6">
|
||
<div class="relative">
|
||
<input type="text" name="date" id="date" placeholder="YYYY-MM-DD" class="w-full min-w-0 bg-white border border-gray-300 rounded px-2 pt-5 pb-2 text-[12px] font-mono focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400">
|
||
<label for="date" class="absolute left-2 -top-2 bg-white px-1 text-[10px] text-gray-700 font-semibold pointer-events-none">Date</label>
|
||
<input type="date" id="date-picker-hidden" style="position:absolute;width:0;height:0;padding:0;border:0;overflow:hidden;clip:rect(0,0,0,0);" tabindex="-1" aria-hidden="true">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: From | Purpose (+ Response Due for Submittals) -->
|
||
<div class="col-span-6">
|
||
<div class="relative">
|
||
<input type="text" name="from" id="from" placeholder="" class="w-full min-w-0 bg-white border border-gray-300 rounded px-2 pt-5 pb-2 text-[12px] font-mono focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400">
|
||
<div id="from-render" class="from-render hidden"></div>
|
||
<label for="from" class="absolute left-2 -top-2 bg-white px-1 text-[10px] text-gray-700 font-semibold pointer-events-none">From</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-span-6">
|
||
<div class="flex gap-x-6" id="purpose-group">
|
||
<div class="relative flex-1">
|
||
<input type="text" name="purpose" id="purpose" placeholder="" class="w-full min-w-0 bg-white border border-gray-300 rounded px-2 pt-5 pb-2 text-[12px] font-mono focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400">
|
||
<label for="purpose" class="absolute left-2 -top-2 bg-white px-1 text-[10px] text-gray-700 font-semibold pointer-events-none">Purpose</label>
|
||
</div>
|
||
<div class="relative flex-1 hidden" id="response-due-wrapper">
|
||
<input type="text" name="response-due" id="response-due" placeholder="YYYY-MM-DD" class="w-full min-w-0 bg-white border border-gray-300 rounded px-2 pt-5 pb-2 text-[12px] font-mono focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400">
|
||
<label for="response-due" class="absolute left-2 -top-2 bg-white px-1 text-[10px] text-gray-700 font-semibold pointer-events-none">Response Due</label>
|
||
<input type="date" id="response-due-picker-hidden" style="position:absolute;width:0;height:0;padding:0;border:0;overflow:hidden;clip:rect(0,0,0,0);" tabindex="-1" aria-hidden="true">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 3: To full width -->
|
||
<div class="col-span-12">
|
||
<div class="relative">
|
||
<input type="text" name="to" id="to" placeholder="" class="w-full min-w-0 bg-white border border-gray-300 rounded px-2 pt-5 pb-2 text-[12px] font-mono focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400">
|
||
<div id="to-render" class="to-render hidden"></div>
|
||
<label for="to" class="absolute left-2 -top-2 bg-white px-1 text-[10px] text-gray-700 font-semibold pointer-events-none">To</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 4: Subject full width -->
|
||
<div class="col-span-12">
|
||
<div class="relative">
|
||
<input type="text" name="subject" id="subject" placeholder="" class="w-full min-w-0 bg-white border border-gray-300 rounded px-2 pt-5 pb-2 text-[12px] font-bold focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400" />
|
||
<label for="subject" class="absolute left-2 -top-2 bg-white px-1 text-[10px] text-gray-700 font-semibold pointer-events-none">Subject</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 5: Remarks full width (Markdown) -->
|
||
<div class="col-span-12">
|
||
<div class="relative w-full">
|
||
<div id="remarks-wrapper" class="w-full">
|
||
<textarea id="remarks" name="remarks" placeholder="Remarks" aria-label="Remarks" class="px-2 pt-1 pb-3 bg-white border-0 rounded-none text-[12px] w-full min-h-[3.5rem] leading-snug resize-y"></textarea>
|
||
<div id="remarks-render-container" class="w-full p-2 overflow-auto">
|
||
<div id="remarks-render" class="text-sm"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="p-2 max-w-[1600px]">
|
||
|
||
<!-- Step: Integrity — "Is this verified?" -->
|
||
<div class="workflow-step" id="integrity-section">
|
||
<h3 class="workflow-step__label">
|
||
Integrity
|
||
<span id="signature-status-static" class="workflow-badge workflow-badge--warn" data-hydrate-hide="true">Not Validated (requires JavaScript)</span>
|
||
</h3>
|
||
<div id="integrity-body" class="integrity-body">
|
||
<div id="digest-display"></div>
|
||
<div id="signatures-list"></div>
|
||
<button id="add-signature-btn" type="button" class="btn btn-secondary btn-sm hidden print:hidden">Add Signature</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- file table goes here (tracking number, title, revision, status) -->
|
||
<div id="file-table-drop-zone" data-drop-zone="file-table">
|
||
<div class="drop-zone-label">Drop folder to scan / verify Drop HTML or JSON to import</div>
|
||
<div class="table-wrapper mt-2">
|
||
<table class="table-auto text-[10px] w-full">
|
||
<thead>
|
||
<tr>
|
||
<th class="bg-gray-100 text-center px-2 py-1 sticky top-0 z-20 whitespace-nowrap"><span class="table-header__caption">#</span></th>
|
||
<th class="bg-gray-100 text-left px-2 py-1 sticky top-0 z-20 whitespace-nowrap">
|
||
<label class="table-header">
|
||
<span class="table-header__caption">TRACKING NUMBER</span>
|
||
<input type="text" data-no-disable="true" data-filter-field="trackingNumber" class="column-filter" placeholder="filter" inputmode="text" spellcheck="false" aria-label="Filter by tracking number">
|
||
</label>
|
||
</th>
|
||
<th class="bg-gray-100 text-left px-2 py-1 sticky top-0 z-20 w-full">
|
||
<label class="table-header">
|
||
<span class="table-header__caption">TITLE</span>
|
||
<input type="text" data-no-disable="true" data-filter-field="title" class="column-filter" placeholder="filter" inputmode="text" spellcheck="false" aria-label="Filter by title">
|
||
</label>
|
||
</th>
|
||
<th class="bg-gray-100 text-center px-2 py-1 sticky top-0 z-20 whitespace-nowrap">
|
||
<label class="table-header">
|
||
<span class="table-header__caption">REVISION</span>
|
||
<input type="text" data-no-disable="true" data-filter-field="revision" class="column-filter" placeholder="filter" inputmode="text" spellcheck="false" aria-label="Filter by revision">
|
||
</label>
|
||
</th>
|
||
<th class="bg-gray-100 text-center px-2 py-1 sticky top-0 z-20 whitespace-nowrap">
|
||
<label class="table-header">
|
||
<span class="table-header__caption">STATUS</span>
|
||
<input type="text" data-no-disable="true" data-filter-field="status" class="column-filter" placeholder="filter" inputmode="text" spellcheck="false" aria-label="Filter by status">
|
||
</label>
|
||
</th>
|
||
<th class="bg-gray-100 text-center px-2 py-1 sticky top-0 z-20 whitespace-nowrap">
|
||
<label class="table-header">
|
||
<span class="table-header__caption">EXT</span>
|
||
<input type="text" data-no-disable="true" data-filter-field="extension" class="column-filter" placeholder="filter" inputmode="text" spellcheck="false" aria-label="Filter by extension">
|
||
</label>
|
||
</th>
|
||
<th class="bg-gray-100 text-right px-2 py-1 sticky top-0 z-20 whitespace-nowrap">
|
||
<label class="table-header">
|
||
<span class="table-header__caption">SIZE</span>
|
||
<input type="text" data-no-disable="true" data-filter-field="fileSize" class="column-filter" placeholder="filter" inputmode="text" spellcheck="false" aria-label="Filter by size">
|
||
</label>
|
||
</th>
|
||
<th class="bg-gray-100 text-left px-2 py-1 sticky top-0 z-20 whitespace-nowrap">
|
||
<label class="table-header">
|
||
<span class="table-header__caption">SHA256</span>
|
||
<input type="text" data-no-disable="true" data-filter-field="sha256" class="column-filter" placeholder="filter" inputmode="text" spellcheck="false" aria-label="Filter by SHA256">
|
||
</label>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td></td>
|
||
<td></td>
|
||
<td></td>
|
||
<td></td>
|
||
<td></td>
|
||
<td></td>
|
||
<td></td>
|
||
<td></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<footer class="page-footer print:hidden">
|
||
<div class="page-footer__inner">
|
||
<span id="data-status" class="text-gray-600 text-[10px]" aria-live="polite"></span>
|
||
<span id="selected-directory" class="workflow-dir text-[10px]"></span>
|
||
</div>
|
||
</footer>
|
||
</form>
|
||
</div>
|
||
|
||
<dialog id="publish-modal" class="modal" data-edit-only="true" aria-labelledby="publish-modal-title">
|
||
<div class="modal__header">
|
||
<h2 id="publish-modal-title" class="modal__title">Publish Transmittal</h2>
|
||
<button type="button" class="modal__close" data-modal-close aria-label="Close">×</button>
|
||
</div>
|
||
<div class="modal__body">
|
||
<div id="publish-date-row" class="publish-field">
|
||
<label for="publish-date-input" class="publish-field__label">Date</label>
|
||
<div class="publish-field__row">
|
||
<input type="text" id="publish-date-input" class="publish-field__input" placeholder="YYYY-MM-DD" pattern="\d{4}-\d{2}-\d{2}">
|
||
<span id="publish-date-warning" class="publish-field__warning" hidden></span>
|
||
</div>
|
||
</div>
|
||
<fieldset class="publish-outputs">
|
||
<legend class="publish-field__label">Output</legend>
|
||
<label class="publish-check">
|
||
<input type="checkbox" id="publish-save-folder"> Save in directory
|
||
</label>
|
||
<label class="publish-check">
|
||
<input type="checkbox" id="publish-download-html"> Download HTML
|
||
</label>
|
||
</fieldset>
|
||
</div>
|
||
<div class="modal__feedback" id="publish-modal-feedback" role="status" aria-live="polite"></div>
|
||
<div class="modal__footer">
|
||
<button type="button" class="btn btn-secondary" data-modal-close>Cancel</button>
|
||
<button type="button" class="btn btn-secondary" id="publish-draft-btn">Save Draft</button>
|
||
<button type="button" class="btn btn-secondary" id="publish-signed-btn">Publish Signed</button>
|
||
<button type="button" class="btn btn-primary" id="publish-confirm">Publish</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="key-dialog" class="modal modal--narrow" aria-labelledby="key-dialog-title">
|
||
<div class="modal__header">
|
||
<h2 id="key-dialog-title" class="modal__title">Signing Key</h2>
|
||
<button type="button" class="modal__close" data-modal-close aria-label="Close">×</button>
|
||
</div>
|
||
<div class="modal__body">
|
||
<p class="key-dialog__desc">Select your signing key file or generate a new key pair.</p>
|
||
<div class="key-dialog__actions">
|
||
<button type="button" class="btn btn-secondary" id="key-select-file">Select Key File</button>
|
||
<button type="button" class="btn btn-secondary" id="key-generate">Generate New Key</button>
|
||
</div>
|
||
<div id="key-generate-panel" hidden>
|
||
<label class="publish-field__label" for="key-password">Password (optional)</label>
|
||
<input type="password" id="key-password" class="publish-field__input" placeholder="Leave blank for no password" autocomplete="new-password">
|
||
<label class="publish-field__label" for="key-password-confirm" style="margin-top:0.25rem;">Confirm password</label>
|
||
<input type="password" id="key-password-confirm" class="publish-field__input" placeholder="Confirm password" autocomplete="new-password">
|
||
<div class="key-dialog__actions" style="margin-top:0.5rem;">
|
||
<button type="button" class="btn btn-primary" id="key-generate-confirm">Generate & Download</button>
|
||
<button type="button" class="btn btn-secondary" id="key-generate-cancel">Back</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal__feedback" id="key-dialog-feedback" role="status" aria-live="polite"></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="password-dialog" class="modal modal--narrow" aria-labelledby="password-dialog-title">
|
||
<div class="modal__header">
|
||
<h2 id="password-dialog-title" class="modal__title">Enter Key Password</h2>
|
||
<button type="button" class="modal__close" data-modal-close aria-label="Close">×</button>
|
||
</div>
|
||
<div class="modal__body">
|
||
<p id="password-dialog-fingerprint" class="key-dialog__desc"></p>
|
||
<input type="password" id="password-dialog-input" class="publish-field__input" placeholder="Password" autocomplete="current-password">
|
||
<div class="modal__feedback" id="password-dialog-feedback" role="status" aria-live="polite"></div>
|
||
</div>
|
||
<div class="modal__footer">
|
||
<button type="button" class="btn btn-secondary" data-modal-close>Cancel</button>
|
||
<button type="button" class="btn btn-primary" id="password-dialog-ok">Unlock</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<!-- Help panel (non-modal slide-out) -->
|
||
<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</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 a Transmittal?</h3>
|
||
<p>A transmittal is a cover sheet that travels with a set of files. It records <em>what</em> was sent, <em>to whom</em>, and <em>when</em>. This tool produces a single self-contained HTML file that serves as both the cover sheet and a cryptographic proof of what was delivered.</p>
|
||
<p><strong>How it works:</strong> Each file in the transmittal is fingerprinted with a SHA-256 hash — a value computed from the file’s contents. Change even one byte and the hash changes, so a matching hash proves the file is unaltered. The transmittal then hashes the entire payload (header, file list, and all per-file hashes) into a single <em>digest</em>. Optionally, one or more people can digitally sign that digest.</p>
|
||
|
||
<h3>Typical Workflow</h3>
|
||
<ol>
|
||
<li><strong>Fill in the header</strong> — tracking number, date, recipient, project, etc.</li>
|
||
<li>Optionally <em>Save Draft</em> to create a reusable template.</li>
|
||
<li><strong>Add files</strong> — paste rows from a spreadsheet, or use <em>Scan Directory</em> to read a folder and compute hashes automatically.</li>
|
||
<li><strong>Review</strong> the file table and remarks.</li>
|
||
<li>Use <em>Verify Directory</em> to confirm every listed file is present and unchanged.</li>
|
||
<li><strong>Publish</strong> — click <em>Publish</em> for an unsigned digest, or use the dropdown for <em>Publish Signed</em> (with a signing key) or <em>Save Draft</em>. The output is saved to the working directory, or downloaded if no directory is selected.</li>
|
||
<li><strong>Distribute</strong> — send the HTML alongside the actual files.</li>
|
||
</ol>
|
||
|
||
<h3>Signing Keys</h3>
|
||
<p>Signing keys are stored in <code>.zddc-key</code> files. When generating a new key, you can optionally protect it with a password. Password-protected keys require the password each time they are used to sign.</p>
|
||
<p>Keys without a password work immediately, like an unprotected SSH key.</p>
|
||
|
||
<h3>Verification Levels</h3>
|
||
<p>A published transmittal provides up to four escalating levels of integrity protection. Higher levels guard against increasingly sophisticated threats:</p>
|
||
<ol>
|
||
<li><strong>Static HTML</strong> — A human-readable record of everything sent. Works without JavaScript (e.g. on a SharePoint site). No cryptographic protection.</li>
|
||
<li><strong>Digest check</strong> — With JavaScript, the tool recomputes the SHA-256 digest and compares it to the stored value. A match proves nothing changed since publication. Detects accidental corruption, but someone could alter both the data and the digest.</li>
|
||
<li><strong>Digital signatures</strong> — ECDSA signatures bind the digest to a signer’s private key. Any change invalidates the signature, preventing undetected tampering. Add signatures after publishing via <em>Add Signature</em>.</li>
|
||
<li><strong>Independent verification</strong> — A tampered HTML file could ship modified JavaScript that always says “Verified.” To rule this out, the recipient opens the sender’s file in their <em>own</em> trusted copy of the tool using <em>Import HTML</em>. This extracts the raw data and re-verifies it with the recipient’s own code. Use this level for anything that matters.</li>
|
||
</ol>
|
||
<p class="text-sm text-gray-500">Level 4 assumes the recipient’s tool is trustworthy (downloaded from a known source or built from source). A reference instance is at <a href="https://zddc.varasys.io/releases/transmittal_stable.html" target="_blank" rel="noopener">zddc.varasys.io</a>.</p>
|
||
|
||
<h3>Menu Actions</h3>
|
||
<p>Actions available from the dropdown button. <span class="help-badge help-badge--draft">draft</span> items appear only while editing. <span class="help-badge help-badge--published">published</span> items appear only after publishing. Unmarked items appear in both modes.</p>
|
||
|
||
<dl>
|
||
<dt>Save Draft <span class="help-badge help-badge--draft">draft</span></dt>
|
||
<dd>Downloads the current state as an HTML file. Reopen it later to continue editing, or use it as a template for new transmittals.</dd>
|
||
|
||
<dt>Revise <span class="help-badge help-badge--published">published</span></dt>
|
||
<dd>Unlocks a published transmittal back into an editable draft.</dd>
|
||
|
||
<dt>Add Signature <span class="help-badge help-badge--published">published</span></dt>
|
||
<dd>Signs the digest with a new ECDSA key and downloads the updated file. Multiple people can sign sequentially.</dd>
|
||
|
||
<dt>Acknowledge Receipt <span class="help-badge help-badge--published">published</span></dt>
|
||
<dd>Same as Add Signature, but labeled “Received By” in the signature list — used by the recipient to confirm delivery.</dd>
|
||
</dl>
|
||
|
||
<h4>Table Clipboard</h4>
|
||
<dl>
|
||
<dt>Paste New Rows <span class="help-badge help-badge--draft">draft</span></dt>
|
||
<dd>Replaces the file table with tab-separated data from the clipboard (e.g. copied from Excel).</dd>
|
||
<dt>Paste Append Rows <span class="help-badge help-badge--draft">draft</span></dt>
|
||
<dd>Appends clipboard rows to the existing file table.</dd>
|
||
<dt>Copy Table</dt>
|
||
<dd>Copies the file table as tab-separated text for pasting into spreadsheets.</dd>
|
||
</dl>
|
||
|
||
<h4>Data Import & Export</h4>
|
||
<dl>
|
||
<dt>Import HTML</dt>
|
||
<dd>Opens a published transmittal HTML, extracts its data, and re-verifies it with your own code (Level 4). You can also drag-and-drop an HTML or JSON file directly onto the <strong>Header</strong> or <strong>File table</strong> drop zones — drag anything over the page to reveal the zones.</dd>
|
||
<dt>Copy JSON</dt>
|
||
<dd>Copies the raw transmittal data as JSON for backup or transfer.</dd>
|
||
<dt>Paste JSON <span class="help-badge help-badge--draft">draft</span></dt>
|
||
<dd>Loads transmittal data from JSON on the clipboard.</dd>
|
||
</dl>
|
||
|
||
<h4>Drag-and-Drop Zones</h4>
|
||
<p>Drag any file or folder over the page to reveal labelled drop zones. Zones that can accept your data are highlighted in blue; others are dimmed.</p>
|
||
<dl>
|
||
<dt>Sender / Receiver logo zones</dt>
|
||
<dd>Top-left and top-right areas. Accept image files. Edit mode only.</dd>
|
||
<dt>Header zone <span class="help-badge help-badge--draft">draft</span></dt>
|
||
<dd>The form header area. Accepts a transmittal HTML or JSON file to import all fields.</dd>
|
||
<dt>File table zone</dt>
|
||
<dd>The document list area. Accepts a folder (triggers Scan or Verify), or a transmittal HTML/JSON file.</dd>
|
||
</dl>
|
||
|
||
<h4>Directory Operations</h4>
|
||
<p class="text-sm text-gray-500">Requires a Chromium-based desktop browser (File System Access API).</p>
|
||
<dl>
|
||
<dt>Scan Directory <span class="help-badge help-badge--draft">draft</span></dt>
|
||
<dd>Picks a local folder, lists every file, and computes SHA-256 hashes to populate the file table.</dd>
|
||
<dt>Verify Directory</dt>
|
||
<dd>Picks a local folder and checks that each file on disk matches its hash in the transmittal.</dd>
|
||
</dl>
|
||
|
||
<h4>Other</h4>
|
||
<dl>
|
||
<dt>Create Index</dt>
|
||
<dd>Scans transmittal folders and builds a <code>.archive</code> directory of small HTML redirects keyed by tracking number, revision, and hash — enabling direct links between documents in a file-based archive.</dd>
|
||
<dt>Reset</dt>
|
||
<dd>Clears everything and restores the blank template.</dd>
|
||
</dl>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Single source of truth for all data; replace this block to swap content -->
|
||
<!-- Hydration: Populate static content from JSON on publish -->
|
||
<script id="transmittal-data" type="application/json">
|
||
{
|
||
"envelope": {
|
||
"version": 1,
|
||
"digestAlgorithm": "SHA-256",
|
||
"digest": "",
|
||
"digestedAt": "",
|
||
"signatureAlgorithm": "ECDSA-P256-SHA256",
|
||
"signatures": []
|
||
},
|
||
"payload": {
|
||
"version": 1,
|
||
"type": "Transmittal",
|
||
"title": "",
|
||
"client": "",
|
||
"project": "",
|
||
"projectNumber": "",
|
||
"date": "",
|
||
"trackingNumber": "",
|
||
"from": "",
|
||
"to": "",
|
||
"purpose": "",
|
||
"responseDue": "",
|
||
"subject": "",
|
||
"remarks": "",
|
||
"files": []
|
||
},
|
||
"presentation": {
|
||
"leftLogo": "",
|
||
"rightLogo": "",
|
||
"theme": "default",
|
||
"customCss": ""
|
||
}
|
||
}
|
||
</script>
|
||
<script>
|
||
/*!
|
||
|
||
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
|
||
<http://stuartk.com/jszip>
|
||
|
||
(c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com>
|
||
Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
|
||
|
||
JSZip uses the library pako released under the MIT license :
|
||
https://github.com/nodeca/pako/blob/main/LICENSE
|
||
*/
|
||
|
||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta})}},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){"use strict";function A(e,t){var r,n="";for(r=0;r<t;r++)n+=String.fromCharCode(255&e),e>>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}))},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t<this.dirRecords.length;t++)this.push({data:this.dirRecords[t],meta:{percent:100}});var r=this.bytesWritten-e,n=function(e,t,r,n,i){var s=I.transformTo("string",i(n));return R.CENTRAL_DIRECTORY_END+"\0\0\0\0"+A(e,2)+A(e,2)+A(t,4)+A(r,4)+A(s.length,2)+s}(this.dirRecords.length,r,e,this.zipComment,this.encodeFileName);this.push({data:n,meta:{percent:100}})},s.prototype.prepareNextSource=function(){this.previous=this._sources.shift(),this.openedSource(this.previous.streamInfo),this.isPaused?this.previous.pause():this.previous.resume()},s.prototype.registerPrevious=function(e){this._sources.push(e);var t=this;return e.on("data",function(e){t.processChunk(e)}),e.on("end",function(){t.closedSource(t.previous.streamInfo),t._sources.length?t.prepareNextSource():t.end()}),e.on("error",function(e){t.error(e)}),this},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this.previous&&this._sources.length?(this.prepareNextSource(),!0):this.previous||this._sources.length||this.generatedError?void 0:(this.end(),!0))},s.prototype.error=function(e){var t=this._sources;if(!i.prototype.error.call(this,e))return!1;for(var r=0;r<t.length;r++)try{t[r].error(e)}catch(e){}return!0},s.prototype.lock=function(){i.prototype.lock.call(this);for(var e=this._sources,t=0;t<e.length;t++)e[t].lock()},t.exports=s},{"../crc32":4,"../signature":23,"../stream/GenericWorker":28,"../utf8":31,"../utils":32}],9:[function(e,t,r){"use strict";var u=e("../compressions"),n=e("./ZipFileWorker");r.generateWorker=function(e,a,t){var o=new n(a.streamFiles,t,a.platform,a.encodeFileName),h=0;try{e.forEach(function(e,t){h++;var r=function(e,t){var r=e||t,n=u[r];if(!n)throw new Error(r+" is not a valid compression method !");return n}(t.options.compression,a.compression),n=t.options.compressionOptions||a.compressionOptions||{},i=t.dir,s=t.date;t._compressWorker(r,n).withStreamInfo("file",{name:e,dir:i,date:s,comment:t.comment||"",unixPermissions:t.unixPermissions,dosPermissions:t.dosPermissions}).pipe(o)}),o.entriesCount=h}catch(e){o.error(e)}return o}},{"../compressions":3,"./ZipFileWorker":8}],10:[function(e,t,r){"use strict";function n(){if(!(this instanceof n))return new n;if(arguments.length)throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide.");this.files=Object.create(null),this.comment=null,this.root="",this.clone=function(){var e=new n;for(var t in this)"function"!=typeof this[t]&&(e[t]=this[t]);return e}}(n.prototype=e("./object")).loadAsync=e("./load"),n.support=e("./support"),n.defaults=e("./defaults"),n.version="3.10.1",n.loadAsync=function(e,t){return(new n).loadAsync(e,t)},n.external=e("./external"),t.exports=n},{"./defaults":5,"./external":6,"./load":11,"./object":15,"./support":30}],11:[function(e,t,r){"use strict";var u=e("./utils"),i=e("./external"),n=e("./utf8"),s=e("./zipEntries"),a=e("./stream/Crc32Probe"),l=e("./nodejsUtils");function f(n){return new i.Promise(function(e,t){var r=n.decompressed.getContentWorker().pipe(new a);r.on("error",function(e){t(e)}).on("end",function(){r.streamInfo.crc32!==n.decompressed.crc32?t(new Error("Corrupted zip : CRC32 mismatch")):e()}).resume()})}t.exports=function(e,o){var h=this;return o=u.extend(o||{},{base64:!1,checkCRC32:!1,optimizedBinaryString:!1,createFolders:!1,decodeFileName:n.utf8decode}),l.isNode&&l.isStream(e)?i.Promise.reject(new Error("JSZip can't accept a stream when loading a zip file.")):u.prepareContent("the loaded zip file",e,!0,o.optimizedBinaryString,o.base64).then(function(e){var t=new s(o);return t.load(e),t}).then(function(e){var t=[i.Promise.resolve(e)],r=e.files;if(o.checkCRC32)for(var n=0;n<r.length;n++)t.push(f(r[n]));return i.Promise.all(t)}).then(function(e){for(var t=e.shift(),r=t.files,n=0;n<r.length;n++){var i=r[n],s=i.fileNameStr,a=u.resolve(i.fileNameStr);h.file(a,i.decompressed,{binary:!0,optimizedBinaryString:!0,date:i.date,dir:i.dir,comment:i.fileCommentStr.length?i.fileCommentStr:null,unixPermissions:i.unixPermissions,dosPermissions:i.dosPermissions,createFolders:o.createFolders}),i.dir||(h.file(a).unsafeOriginalName=s)}return t.zipComment.length&&(h.comment=t.zipComment),h})}},{"./external":6,"./nodejsUtils":14,"./stream/Crc32Probe":25,"./utf8":31,"./utils":32,"./zipEntries":33}],12:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../stream/GenericWorker");function s(e,t){i.call(this,"Nodejs stream input adapter for "+e),this._upstreamEnded=!1,this._bindStream(t)}n.inherits(s,i),s.prototype._bindStream=function(e){var t=this;(this._stream=e).pause(),e.on("data",function(e){t.push({data:e,meta:{percent:0}})}).on("error",function(e){t.isPaused?this.generatedError=e:t.error(e)}).on("end",function(){t.isPaused?t._upstreamEnded=!0:t.end()})},s.prototype.pause=function(){return!!i.prototype.pause.call(this)&&(this._stream.pause(),!0)},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(this._upstreamEnded?this.end():this._stream.resume(),!0)},t.exports=s},{"../stream/GenericWorker":28,"../utils":32}],13:[function(e,t,r){"use strict";var i=e("readable-stream").Readable;function n(e,t,r){i.call(this,t),this._helper=e;var n=this;e.on("data",function(e,t){n.push(e)||n._helper.pause(),r&&r(t)}).on("error",function(e){n.emit("error",e)}).on("end",function(){n.push(null)})}e("../utils").inherits(n,i),n.prototype._read=function(){this._helper.resume()},t.exports=n},{"../utils":32,"readable-stream":16}],14:[function(e,t,r){"use strict";t.exports={isNode:"undefined"!=typeof Buffer,newBufferFrom:function(e,t){if(Buffer.from&&Buffer.from!==Uint8Array.from)return Buffer.from(e,t);if("number"==typeof e)throw new Error('The "data" argument must not be a number');return new Buffer(e,t)},allocBuffer:function(e){if(Buffer.alloc)return Buffer.alloc(e);var t=new Buffer(e);return t.fill(0),t},isBuffer:function(e){return Buffer.isBuffer(e)},isStream:function(e){return e&&"function"==typeof e.on&&"function"==typeof e.pause&&"function"==typeof e.resume}}},{}],15:[function(e,t,r){"use strict";function s(e,t,r){var n,i=u.getTypeOf(t),s=u.extend(r||{},f);s.date=s.date||new Date,null!==s.compression&&(s.compression=s.compression.toUpperCase()),"string"==typeof s.unixPermissions&&(s.unixPermissions=parseInt(s.unixPermissions,8)),s.unixPermissions&&16384&s.unixPermissions&&(s.dir=!0),s.dosPermissions&&16&s.dosPermissions&&(s.dir=!0),s.dir&&(e=g(e)),s.createFolders&&(n=_(e))&&b.call(this,n,!0);var a="string"===i&&!1===s.binary&&!1===s.base64;r&&void 0!==r.binary||(s.binary=!a),(t instanceof c&&0===t.uncompressedSize||s.dir||!t||0===t.length)&&(s.base64=!1,s.binary=!0,t="",s.compression="STORE",i="string");var o=null;o=t instanceof c||t instanceof l?t:p.isNode&&p.isStream(t)?new m(e,t):u.prepareContent(e,t,s.binary,s.optimizedBinaryString,s.base64);var h=new d(e,o,s);this.files[e]=h}var i=e("./utf8"),u=e("./utils"),l=e("./stream/GenericWorker"),a=e("./stream/StreamHelper"),f=e("./defaults"),c=e("./compressedObject"),d=e("./zipObject"),o=e("./generate"),p=e("./nodejsUtils"),m=e("./nodejs/NodejsStreamInputAdapter"),_=function(e){"/"===e.slice(-1)&&(e=e.substring(0,e.length-1));var t=e.lastIndexOf("/");return 0<t?e.substring(0,t):""},g=function(e){return"/"!==e.slice(-1)&&(e+="/"),e},b=function(e,t){return t=void 0!==t?t:f.createFolders,e=g(e),this.files[e]||s.call(this,e,null,{dir:!0,createFolders:t}),this.files[e]};function h(e){return"[object RegExp]"===Object.prototype.toString.call(e)}var n={load:function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},forEach:function(e){var t,r,n;for(t in this.files)n=this.files[t],(r=t.slice(this.root.length,t.length))&&t.slice(0,this.root.length)===this.root&&e(r,n)},filter:function(r){var n=[];return this.forEach(function(e,t){r(e,t)&&n.push(t)}),n},file:function(e,t,r){if(1!==arguments.length)return e=this.root+e,s.call(this,e,t,r),this;if(h(e)){var n=e;return this.filter(function(e,t){return!t.dir&&n.test(e)})}var i=this.files[this.root+e];return i&&!i.dir?i:null},folder:function(r){if(!r)return this;if(h(r))return this.filter(function(e,t){return t.dir&&r.test(e)});var e=this.root+r,t=b.call(this,e),n=this.clone();return n.root=t.name,n},remove:function(r){r=this.root+r;var e=this.files[r];if(e||("/"!==r.slice(-1)&&(r+="/"),e=this.files[r]),e&&!e.dir)delete this.files[r];else for(var t=this.filter(function(e,t){return t.name.slice(0,r.length)===r}),n=0;n<t.length;n++)delete this.files[t[n].name];return this},generate:function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},generateInternalStream:function(e){var t,r={};try{if((r=u.extend(e||{},{streamFiles:!1,compression:"STORE",compressionOptions:null,type:"",platform:"DOS",comment:null,mimeType:"application/zip",encodeFileName:i.utf8encode})).type=r.type.toLowerCase(),r.compression=r.compression.toUpperCase(),"binarystring"===r.type&&(r.type="string"),!r.type)throw new Error("No output type specified.");u.checkSupport(r.type),"darwin"!==r.platform&&"freebsd"!==r.platform&&"linux"!==r.platform&&"sunos"!==r.platform||(r.platform="UNIX"),"win32"===r.platform&&(r.platform="DOS");var n=r.comment||this.comment||"";t=o.generateWorker(this,r,n)}catch(e){(t=new l("error")).error(e)}return new a(t,r.type||"string",r.mimeType)},generateAsync:function(e,t){return this.generateInternalStream(e).accumulate(t)},generateNodeStream:function(e,t){return(e=e||{}).type||(e.type="nodebuffer"),this.generateInternalStream(e).toNodejsStream(t)}};t.exports=n},{"./compressedObject":2,"./defaults":5,"./generate":9,"./nodejs/NodejsStreamInputAdapter":12,"./nodejsUtils":14,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31,"./utils":32,"./zipObject":35}],16:[function(e,t,r){"use strict";t.exports=e("stream")},{stream:void 0}],17:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e);for(var t=0;t<this.data.length;t++)e[t]=255&e[t]}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data[this.zero+e]},i.prototype.lastIndexOfSignature=function(e){for(var t=e.charCodeAt(0),r=e.charCodeAt(1),n=e.charCodeAt(2),i=e.charCodeAt(3),s=this.length-4;0<=s;--s)if(this.data[s]===t&&this.data[s+1]===r&&this.data[s+2]===n&&this.data[s+3]===i)return s-this.zero;return-1},i.prototype.readAndCheckSignature=function(e){var t=e.charCodeAt(0),r=e.charCodeAt(1),n=e.charCodeAt(2),i=e.charCodeAt(3),s=this.readData(4);return t===s[0]&&r===s[1]&&n===s[2]&&i===s[3]},i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return[];var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],18:[function(e,t,r){"use strict";var n=e("../utils");function i(e){this.data=e,this.length=e.length,this.index=0,this.zero=0}i.prototype={checkOffset:function(e){this.checkIndex(this.index+e)},checkIndex:function(e){if(this.length<this.zero+e||e<0)throw new Error("End of data reached (data length = "+this.length+", asked index = "+e+"). Corrupted zip ?")},setIndex:function(e){this.checkIndex(e),this.index=e},skip:function(e){this.setIndex(this.index+e)},byteAt:function(){},readInt:function(e){var t,r=0;for(this.checkOffset(e),t=this.index+e-1;t>=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i},{"../utils":32}],19:[function(e,t,r){"use strict";var n=e("./Uint8ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r<this._listeners[e].length;r++)this._listeners[e][r].call(this,t)},pipe:function(e){return e.registerPrevious(this)},registerPrevious:function(e){if(this.isLocked)throw new Error("The stream '"+this+"' has already been used.");this.streamInfo=e.streamInfo,this.mergeStreamInfo(),this.previous=e;var t=this;return e.on("data",function(e){t.processChunk(e)}),e.on("end",function(){t.end()}),e.on("error",function(e){t.error(e)}),this},pause:function(){return!this.isPaused&&!this.isFinished&&(this.isPaused=!0,this.previous&&this.previous.pause(),!0)},resume:function(){if(!this.isPaused||this.isFinished)return!1;var e=this.isPaused=!1;return this.generatedError&&(this.error(this.generatedError),e=!0),this.previous&&this.previous.resume(),!e},flush:function(){},processChunk:function(e){this.push(e)},withStreamInfo:function(e,t){return this.extraStreamInfo[e]=t,this.mergeStreamInfo(),this},mergeStreamInfo:function(){for(var e in this.extraStreamInfo)Object.prototype.hasOwnProperty.call(this.extraStreamInfo,e)&&(this.streamInfo[e]=this.extraStreamInfo[e])},lock:function(){if(this.isLocked)throw new Error("The stream '"+this+"' has already been used.");this.isLocked=!0,this.previous&&this.previous.lock()},toString:function(){var e="Worker "+this.name;return this.previous?this.previous+" -> "+e:e}},t.exports=n},{}],29:[function(e,t,r){"use strict";var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t)}).on("error",function(e){n=[],r(e)}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r<t.length;r++)s+=t[r].length;switch(e){case"string":return t.join("");case"array":return Array.prototype.concat.apply([],t);case"uint8array":for(i=new Uint8Array(s),r=0;r<t.length;r++)i.set(t[r],n),n+=t[r].length;return i;case"nodebuffer":return Buffer.concat(t);default:throw new Error("concat : unsupported type '"+e+"'")}}(i,n),a);t(e)}catch(e){r(e)}n=[]}).resume()})}function f(e,t,r){var n=t;switch(t){case"blob":case"arraybuffer":n="uint8array";break;case"base64":n="string"}try{this._internalType=n,this._outputType=t,this._mimeType=r,h.checkSupport(n),this._worker=e.pipe(new i(n)),e.lock()}catch(e){this._worker=new s("error"),this._worker.error(e)}}f.prototype={accumulate:function(e){return l(this,e)},on:function(e,t){var r=this;return"data"===e?this._worker.on(e,function(e){t.call(r,e.data,e.meta)}):this._worker.on(e,function(){h.delay(t,arguments,r)}),this},resume:function(){return h.delay(this._worker.resume,[],this._worker),this},pause:function(){return this._worker.pause(),this},toNodejsStream:function(e){if(h.checkSupport("nodestream"),"nodebuffer"!==this._outputType)throw new Error(this._outputType+" is not supported by this method");return new o(this,{objectMode:"nodebuffer"!==this._outputType},e)}},t.exports=f},{"../base64":1,"../external":6,"../nodejs/NodejsStreamOutputAdapter":13,"../support":30,"../utils":32,"./ConvertWorker":24,"./GenericWorker":28}],30:[function(e,t,r){"use strict";if(r.base64=!0,r.array=!0,r.string=!0,r.arraybuffer="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8Array,r.nodebuffer="undefined"!=typeof Buffer,r.uint8array="undefined"!=typeof Uint8Array,"undefined"==typeof ArrayBuffer)r.blob=!1;else{var n=new ArrayBuffer(0);try{r.blob=0===new Blob([n],{type:"application/zip"}).size}catch(e){try{var i=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);i.append(n),r.blob=0===i.getBlob("application/zip").size}catch(e){r.blob=!1}}}try{r.nodestream=!!e("readable-stream").Readable}catch(e){r.nodestream=!1}},{"readable-stream":16}],31:[function(e,t,s){"use strict";for(var o=e("./utils"),h=e("./support"),r=e("./nodejsUtils"),n=e("./stream/GenericWorker"),u=new Array(256),i=0;i<256;i++)u[i]=252<=i?6:248<=i?5:240<=i?4:224<=i?3:192<=i?2:1;u[254]=u[254]=1;function a(){n.call(this,"utf-8 decode"),this.leftOver=null}function l(){n.call(this,"utf-8 encode")}s.utf8encode=function(e){return h.nodebuffer?r.newBufferFrom(e,"utf-8"):function(e){var t,r,n,i,s,a=e.length,o=0;for(i=0;i<a;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),o+=r<128?1:r<2048?2:r<65536?3:4;for(t=h.uint8array?new Uint8Array(o):new Array(o),i=s=0;s<o;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),r<128?t[s++]=r:(r<2048?t[s++]=192|r>>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t<s;)if((n=e[t++])<128)a[r++]=n;else if(4<(i=u[n]))a[r++]=65533,t+=i-1;else{for(n&=2===i?31:3===i?15:7;1<i&&t<s;)n=n<<6|63&e[t++],i--;1<i?a[r++]=65533:n<65536?a[r++]=n:(n-=65536,a[r++]=55296|n>>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r<e.length;++r)t[r]=255&e.charCodeAt(r);return t}e("setimmediate"),a.newBlob=function(t,r){a.checkSupport("blob");try{return new Blob([t],{type:r})}catch(e){try{var n=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);return n.append(t),n.getBlob(r)}catch(e){throw new Error("Bug : can't construct the Blob.")}}};var i={stringifyByChunk:function(e,t,r){var n=[],i=0,s=e.length;if(s<=r)return String.fromCharCode.apply(null,e);for(;i<s;)"array"===t||"nodebuffer"===t?n.push(String.fromCharCode.apply(null,e.slice(i,Math.min(i+r,s)))):n.push(String.fromCharCode.apply(null,e.subarray(i,Math.min(i+r,s)))),i+=r;return n.join("")},stringifyByChar:function(e){for(var t="",r=0;r<e.length;r++)t+=String.fromCharCode(e[r]);return t},applyCanBeUsed:{uint8array:function(){try{return o.uint8array&&1===String.fromCharCode.apply(null,new Uint8Array(1)).length}catch(e){return!1}}(),nodebuffer:function(){try{return o.nodebuffer&&1===String.fromCharCode.apply(null,r.allocBuffer(1)).length}catch(e){return!1}}()}};function s(e){var t=65536,r=a.getTypeOf(e),n=!0;if("uint8array"===r?n=i.applyCanBeUsed.uint8array:"nodebuffer"===r&&(n=i.applyCanBeUsed.nodebuffer),n)for(;1<t;)try{return i.stringifyByChunk(e,r,t)}catch(e){t=Math.floor(t/2)}return i.stringifyByChar(e)}function f(e,t){for(var r=0;r<e.length;r++)t[r]=e[r];return t}a.applyFromCharCode=s;var c={};c.string={string:n,array:function(e){return l(e,new Array(e.length))},arraybuffer:function(e){return c.string.uint8array(e).buffer},uint8array:function(e){return l(e,new Uint8Array(e.length))},nodebuffer:function(e){return l(e,r.allocBuffer(e.length))}},c.array={string:s,array:n,arraybuffer:function(e){return new Uint8Array(e).buffer},uint8array:function(e){return new Uint8Array(e)},nodebuffer:function(e){return r.newBufferFrom(e)}},c.arraybuffer={string:function(e){return s(new Uint8Array(e))},array:function(e){return f(new Uint8Array(e),new Array(e.byteLength))},arraybuffer:n,uint8array:function(e){return new Uint8Array(e)},nodebuffer:function(e){return r.newBufferFrom(new Uint8Array(e))}},c.uint8array={string:s,array:function(e){return f(e,new Array(e.length))},arraybuffer:function(e){return e.buffer},uint8array:n,nodebuffer:function(e){return r.newBufferFrom(e)}},c.nodebuffer={string:s,array:function(e){return f(e,new Array(e.length))},arraybuffer:function(e){return c.nodebuffer.uint8array(e).buffer},uint8array:function(e){return f(e,new Uint8Array(e.length))},nodebuffer:n},a.transformTo=function(e,t){if(t=t||"",!e)return t;a.checkSupport(e);var r=a.getTypeOf(t);return c[r][e](t)},a.resolve=function(e){for(var t=e.split("/"),r=[],n=0;n<t.length;n++){var i=t[n];"."===i||""===i&&0!==n&&n!==t.length-1||(".."===i?r.pop():r.push(i))}return r.join("/")},a.getTypeOf=function(e){return"string"==typeof e?"string":"[object Array]"===Object.prototype.toString.call(e)?"array":o.nodebuffer&&r.isBuffer(e)?"nodebuffer":o.uint8array&&e instanceof Uint8Array?"uint8array":o.arraybuffer&&e instanceof ArrayBuffer?"arraybuffer":void 0},a.checkSupport=function(e){if(!o[e.toLowerCase()])throw new Error(e+" is not supported by this platform")},a.MAX_VALUE_16BITS=65535,a.MAX_VALUE_32BITS=-1,a.pretty=function(e){var t,r,n="";for(r=0;r<(e||"").length;r++)n+="\\x"+((t=e.charCodeAt(r))<16?"0":"")+t.toString(16).toUpperCase();return n},a.delay=function(e,t,r){setImmediate(function(){e.apply(r||null,t||[])})},a.inherits=function(e,t){function r(){}r.prototype=t.prototype,e.prototype=new r},a.extend=function(){var e,t,r={};for(e=0;e<arguments.length;e++)for(t in arguments[e])Object.prototype.hasOwnProperty.call(arguments[e],t)&&void 0===r[t]&&(r[t]=arguments[e][t]);return r},a.prepareContent=function(r,e,n,i,s){return u.Promise.resolve(e).then(function(n){return o.blob&&(n instanceof Blob||-1!==["[object File]","[object Blob]"].indexOf(Object.prototype.toString.call(n)))&&"undefined"!=typeof FileReader?new u.Promise(function(t,r){var e=new FileReader;e.onload=function(e){t(e.target.result)},e.onerror=function(e){r(e.target.error)},e.readAsArrayBuffer(n)}):n}).then(function(e){var t=a.getTypeOf(e);return t?("arraybuffer"===t?e=a.transformTo("uint8array",e):"string"===t&&(s?e=h.decode(e):n&&!0!==i&&(e=function(e){return l(e,o.uint8array?new Uint8Array(e.length):new Array(e.length))}(e))),e):u.Promise.reject(new Error("Can't read the data of '"+r+"'. Is it in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?"))})}},{"./base64":1,"./external":6,"./nodejsUtils":14,"./support":30,setimmediate:54}],33:[function(e,t,r){"use strict";var n=e("./reader/readerFor"),i=e("./utils"),s=e("./signature"),a=e("./zipEntry"),o=e("./support");function h(e){this.files=[],this.loadOptions=e}h.prototype={checkSignature:function(e){if(!this.reader.readAndCheckSignature(e)){this.reader.index-=4;var t=this.reader.readString(4);throw new Error("Corrupted zip or bug: unexpected signature ("+i.pretty(t)+", expected "+i.pretty(e)+")")}},isSignature:function(e,t){var r=this.reader.index;this.reader.setIndex(e);var n=this.reader.readString(4)===t;return this.reader.setIndex(r),n},readBlockEndOfCentral:function(){this.diskNumber=this.reader.readInt(2),this.diskWithCentralDirStart=this.reader.readInt(2),this.centralDirRecordsOnThisDisk=this.reader.readInt(2),this.centralDirRecords=this.reader.readInt(2),this.centralDirSize=this.reader.readInt(4),this.centralDirOffset=this.reader.readInt(4),this.zipCommentLength=this.reader.readInt(2);var e=this.reader.readData(this.zipCommentLength),t=o.uint8array?"uint8array":"array",r=i.transformTo(t,e);this.zipComment=this.loadOptions.decodeFileName(r)},readBlockZip64EndOfCentral:function(){this.zip64EndOfCentralSize=this.reader.readInt(8),this.reader.skip(4),this.diskNumber=this.reader.readInt(4),this.diskWithCentralDirStart=this.reader.readInt(4),this.centralDirRecordsOnThisDisk=this.reader.readInt(8),this.centralDirRecords=this.reader.readInt(8),this.centralDirSize=this.reader.readInt(8),this.centralDirOffset=this.reader.readInt(8),this.zip64ExtensibleData={};for(var e,t,r,n=this.zip64EndOfCentralSize-44;0<n;)e=this.reader.readInt(2),t=this.reader.readInt(4),r=this.reader.readData(t),this.zip64ExtensibleData[e]={id:e,length:t,value:r}},readBlockZip64EndOfCentralLocator:function(){if(this.diskWithZip64CentralDirStart=this.reader.readInt(4),this.relativeOffsetEndOfZip64CentralDir=this.reader.readInt(8),this.disksCount=this.reader.readInt(4),1<this.disksCount)throw new Error("Multi-volumes zip are not supported")},readLocalFiles:function(){var e,t;for(e=0;e<this.files.length;e++)t=this.files[e],this.reader.setIndex(t.localHeaderOffset),this.checkSignature(s.LOCAL_FILE_HEADER),t.readLocalPart(this.reader),t.handleUTF8(),t.processAttributes()},readCentralDir:function(){var e;for(this.reader.setIndex(this.centralDirOffset);this.reader.readAndCheckSignature(s.CENTRAL_FILE_HEADER);)(e=new a({zip64:this.zip64},this.loadOptions)).readCentralPart(this.reader),this.files.push(e);if(this.centralDirRecords!==this.files.length&&0!==this.centralDirRecords&&0===this.files.length)throw new Error("Corrupted zip or bug: expected "+this.centralDirRecords+" records in central dir, got "+this.files.length)},readEndOfCentral:function(){var e=this.reader.lastIndexOfSignature(s.CENTRAL_DIRECTORY_END);if(e<0)throw!this.isSignature(0,s.LOCAL_FILE_HEADER)?new Error("Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html"):new Error("Corrupted zip: can't find end of central directory");this.reader.setIndex(e);var t=e;if(this.checkSignature(s.CENTRAL_DIRECTORY_END),this.readBlockEndOfCentral(),this.diskNumber===i.MAX_VALUE_16BITS||this.diskWithCentralDirStart===i.MAX_VALUE_16BITS||this.centralDirRecordsOnThisDisk===i.MAX_VALUE_16BITS||this.centralDirRecords===i.MAX_VALUE_16BITS||this.centralDirSize===i.MAX_VALUE_32BITS||this.centralDirOffset===i.MAX_VALUE_32BITS){if(this.zip64=!0,(e=this.reader.lastIndexOfSignature(s.ZIP64_CENTRAL_DIRECTORY_LOCATOR))<0)throw new Error("Corrupted zip: can't find the ZIP64 end of central directory locator");if(this.reader.setIndex(e),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_LOCATOR),this.readBlockZip64EndOfCentralLocator(),!this.isSignature(this.relativeOffsetEndOfZip64CentralDir,s.ZIP64_CENTRAL_DIRECTORY_END)&&(this.relativeOffsetEndOfZip64CentralDir=this.reader.lastIndexOfSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.relativeOffsetEndOfZip64CentralDir<0))throw new Error("Corrupted zip: can't find the ZIP64 end of central directory");this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.readBlockZip64EndOfCentral()}var r=this.centralDirOffset+this.centralDirSize;this.zip64&&(r+=20,r+=12+this.zip64EndOfCentralSize);var n=t-r;if(0<n)this.isSignature(t,s.CENTRAL_FILE_HEADER)||(this.reader.zero=n);else if(n<0)throw new Error("Corrupted zip: missing "+Math.abs(n)+" bytes.")},prepareReader:function(e){this.reader=n(e)},load:function(e){this.prepareReader(e),this.readEndOfCentral(),this.readCentralDir(),this.readLocalFiles()}},t.exports=h},{"./reader/readerFor":22,"./signature":23,"./support":30,"./utils":32,"./zipEntry":34}],34:[function(e,t,r){"use strict";var n=e("./reader/readerFor"),s=e("./utils"),i=e("./compressedObject"),a=e("./crc32"),o=e("./utf8"),h=e("./compressions"),u=e("./support");function l(e,t){this.options=e,this.loadOptions=t}l.prototype={isEncrypted:function(){return 1==(1&this.bitFlag)},useUTF8:function(){return 2048==(2048&this.bitFlag)},readLocalPart:function(e){var t,r;if(e.skip(22),this.fileNameLength=e.readInt(2),r=e.readInt(2),this.fileName=e.readData(this.fileNameLength),e.skip(r),-1===this.compressedSize||-1===this.uncompressedSize)throw new Error("Bug or corrupted zip : didn't get enough information from the central directory (compressedSize === -1 || uncompressedSize === -1)");if(null===(t=function(e){for(var t in h)if(Object.prototype.hasOwnProperty.call(h,t)&&h[t].magic===e)return h[t];return null}(this.compressionMethod)))throw new Error("Corrupted zip : compression "+s.pretty(this.compressionMethod)+" unknown (inner file : "+s.transformTo("string",this.fileName)+")");this.decompressed=new i(this.compressedSize,this.uncompressedSize,this.crc32,t,e.readData(this.compressedSize))},readCentralPart:function(e){this.versionMadeBy=e.readInt(2),e.skip(2),this.bitFlag=e.readInt(2),this.compressionMethod=e.readString(2),this.date=e.readDate(),this.crc32=e.readInt(4),this.compressedSize=e.readInt(4),this.uncompressedSize=e.readInt(4);var t=e.readInt(2);if(this.extraFieldsLength=e.readInt(2),this.fileCommentLength=e.readInt(2),this.diskNumberStart=e.readInt(2),this.internalFileAttributes=e.readInt(2),this.externalFileAttributes=e.readInt(4),this.localHeaderOffset=e.readInt(4),this.isEncrypted())throw new Error("Encrypted zip are not supported");e.skip(t),this.readExtraFields(e),this.parseZIP64ExtraField(e),this.fileComment=e.readData(this.fileCommentLength)},processAttributes:function(){this.unixPermissions=null,this.dosPermissions=null;var e=this.versionMadeBy>>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4<i;)t=e.readInt(2),r=e.readInt(2),n=e.readData(r),this.extraFields[t]={id:t,length:r,value:n};e.setIndex(i)},handleUTF8:function(){var e=u.uint8array?"uint8array":"array";if(this.useUTF8())this.fileNameStr=o.utf8decode(this.fileName),this.fileCommentStr=o.utf8decode(this.fileComment);else{var t=this.findExtraFieldUnicodePath();if(null!==t)this.fileNameStr=t;else{var r=s.transformTo(e,this.fileName);this.fileNameStr=this.loadOptions.decodeFileName(r)}var n=this.findExtraFieldUnicodeComment();if(null!==n)this.fileCommentStr=n;else{var i=s.transformTo(e,this.fileComment);this.fileCommentStr=this.loadOptions.decodeFileName(i)}}},findExtraFieldUnicodePath:function(){var e=this.extraFields[28789];if(e){var t=n(e.value);return 1!==t.readInt(1)?null:a(this.fileName)!==t.readInt(4)?null:o.utf8decode(t.readData(e.length-5))}return null},findExtraFieldUnicodeComment:function(){var e=this.extraFields[25461];if(e){var t=n(e.value);return 1!==t.readInt(1)?null:a(this.fileComment)!==t.readInt(4)?null:o.utf8decode(t.readData(e.length-5))}return null}},t.exports=l},{"./compressedObject":2,"./compressions":3,"./crc32":4,"./reader/readerFor":22,"./support":30,"./utf8":31,"./utils":32}],35:[function(e,t,r){"use strict";function n(e,t,r){this.name=e,this.dir=r.dir,this.date=r.date,this.comment=r.comment,this.unixPermissions=r.unixPermissions,this.dosPermissions=r.dosPermissions,this._data=t,this._dataBinary=r.binary,this.options={compression:r.compression,compressionOptions:r.compressionOptions}}var s=e("./stream/StreamHelper"),i=e("./stream/DataWorker"),a=e("./utf8"),o=e("./compressedObject"),h=e("./stream/GenericWorker");n.prototype={internalStream:function(e){var t=null,r="string";try{if(!e)throw new Error("No output type specified.");var n="string"===(r=e.toLowerCase())||"text"===r;"binarystring"!==r&&"text"!==r||(r="string"),t=this._decompressWorker();var i=!this._dataBinary;i&&!n&&(t=t.pipe(new a.Utf8EncodeWorker)),!i&&n&&(t=t.pipe(new a.Utf8DecodeWorker))}catch(e){(t=new h("error")).error(e)}return new s(t,r,"")},async:function(e,t){return this.internalStream(e).accumulate(t)},nodeStream:function(e,t){return this.internalStream(e||"nodebuffer").toNodejsStream(t)},_compressWorker:function(e,t){if(this._data instanceof o&&this._data.compression.magic===e.magic)return this._data.getCompressedWorker();var r=this._decompressWorker();return this._dataBinary||(r=r.pipe(new a.Utf8EncodeWorker)),o.createWorkerFrom(r,e,t)},_decompressWorker:function(){return this._data instanceof o?this._data.getContentWorker():this._data instanceof h?this._data:new i(this._data)}};for(var u=["asText","asBinary","asNodeBuffer","asUint8Array","asArrayBuffer"],l=function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},f=0;f<u.length;f++)n.prototype[u[f]]=l;t.exports=n},{"./compressedObject":2,"./stream/DataWorker":27,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31}],36:[function(e,l,t){(function(t){"use strict";var r,n,e=t.MutationObserver||t.WebKitMutationObserver;if(e){var i=0,s=new e(u),a=t.document.createTextNode("");s.observe(a,{characterData:!0}),r=function(){a.data=i=++i%2}}else if(t.setImmediate||void 0===t.MessageChannel)r="document"in t&&"onreadystatechange"in t.document.createElement("script")?function(){var e=t.document.createElement("script");e.onreadystatechange=function(){u(),e.onreadystatechange=null,e.parentNode.removeChild(e),e=null},t.document.documentElement.appendChild(e)}:function(){setTimeout(u,0)};else{var o=new t.MessageChannel;o.port1.onmessage=u,r=function(){o.port2.postMessage(0)}}var h=[];function u(){var e,t;n=!0;for(var r=h.length;r;){for(t=h,h=[],e=-1;++e<r;)t[e]();r=h.length}n=!1}l.exports=function(e){1!==h.push(e)||n||r()}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],37:[function(e,t,r){"use strict";var i=e("immediate");function u(){}var l={},s=["REJECTED"],a=["FULFILLED"],n=["PENDING"];function o(e){if("function"!=typeof e)throw new TypeError("resolver must be a function");this.state=n,this.queue=[],this.outcome=void 0,e!==u&&d(this,e)}function h(e,t,r){this.promise=e,"function"==typeof t&&(this.onFulfilled=t,this.callFulfilled=this.otherCallFulfilled),"function"==typeof r&&(this.onRejected=r,this.callRejected=this.otherCallRejected)}function f(t,r,n){i(function(){var e;try{e=r(n)}catch(e){return l.reject(t,e)}e===t?l.reject(t,new TypeError("Cannot resolve promise with itself")):l.resolve(t,e)})}function c(e){var t=e&&e.then;if(e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof t)return function(){t.apply(e,arguments)}}function d(t,e){var r=!1;function n(e){r||(r=!0,l.reject(t,e))}function i(e){r||(r=!0,l.resolve(t,e))}var s=p(function(){e(i,n)});"error"===s.status&&n(s.value)}function p(e,t){var r={};try{r.value=e(t),r.status="success"}catch(e){r.status="error",r.value=e}return r}(t.exports=o).prototype.finally=function(t){if("function"!=typeof t)return this;var r=this.constructor;return this.then(function(e){return r.resolve(t()).then(function(){return e})},function(e){return r.resolve(t()).then(function(){throw e})})},o.prototype.catch=function(e){return this.then(null,e)},o.prototype.then=function(e,t){if("function"!=typeof e&&this.state===a||"function"!=typeof t&&this.state===s)return this;var r=new this.constructor(u);this.state!==n?f(r,this.state===a?e:t,this.outcome):this.queue.push(new h(r,e,t));return r},h.prototype.callFulfilled=function(e){l.resolve(this.promise,e)},h.prototype.otherCallFulfilled=function(e){f(this.promise,this.onFulfilled,e)},h.prototype.callRejected=function(e){l.reject(this.promise,e)},h.prototype.otherCallRejected=function(e){f(this.promise,this.onRejected,e)},l.resolve=function(e,t){var r=p(c,t);if("error"===r.status)return l.reject(e,r.value);var n=r.value;if(n)d(e,n);else{e.state=a,e.outcome=t;for(var i=-1,s=e.queue.length;++i<s;)e.queue[i].callFulfilled(t)}return e},l.reject=function(e,t){e.state=s,e.outcome=t;for(var r=-1,n=e.queue.length;++r<n;)e.queue[r].callRejected(t);return e},o.resolve=function(e){if(e instanceof this)return e;return l.resolve(new this(u),e)},o.reject=function(e){var t=new this(u);return l.reject(t,e)},o.all=function(e){var r=this;if("[object Array]"!==Object.prototype.toString.call(e))return this.reject(new TypeError("must be an array"));var n=e.length,i=!1;if(!n)return this.resolve([]);var s=new Array(n),a=0,t=-1,o=new this(u);for(;++t<n;)h(e[t],t);return o;function h(e,t){r.resolve(e).then(function(e){s[t]=e,++a!==n||i||(i=!0,l.resolve(o,s))},function(e){i||(i=!0,l.reject(o,e))})}},o.race=function(e){var t=this;if("[object Array]"!==Object.prototype.toString.call(e))return this.reject(new TypeError("must be an array"));var r=e.length,n=!1;if(!r)return this.resolve([]);var i=-1,s=new this(u);for(;++i<r;)a=e[i],t.resolve(a).then(function(e){n||(n=!0,l.resolve(s,e))},function(e){n||(n=!0,l.reject(s,e))});var a;return s}},{immediate:36}],38:[function(e,t,r){"use strict";var n={};(0,e("./lib/utils/common").assign)(n,e("./lib/deflate"),e("./lib/inflate"),e("./lib/zlib/constants")),t.exports=n},{"./lib/deflate":39,"./lib/inflate":40,"./lib/utils/common":41,"./lib/zlib/constants":44}],39:[function(e,t,r){"use strict";var a=e("./zlib/deflate"),o=e("./utils/common"),h=e("./utils/strings"),i=e("./zlib/messages"),s=e("./zlib/zstream"),u=Object.prototype.toString,l=0,f=-1,c=0,d=8;function p(e){if(!(this instanceof p))return new p(e);this.options=o.assign({level:f,method:d,chunkSize:16384,windowBits:15,memLevel:8,strategy:c,to:""},e||{});var t=this.options;t.raw&&0<t.windowBits?t.windowBits=-t.windowBits:t.gzip&&0<t.windowBits&&t.windowBits<16&&(t.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new s,this.strm.avail_out=0;var r=a.deflateInit2(this.strm,t.level,t.method,t.windowBits,t.memLevel,t.strategy);if(r!==l)throw new Error(i[r]);if(t.header&&a.deflateSetHeader(this.strm,t.header),t.dictionary){var n;if(n="string"==typeof t.dictionary?h.string2buf(t.dictionary):"[object ArrayBuffer]"===u.call(t.dictionary)?new Uint8Array(t.dictionary):t.dictionary,(r=a.deflateSetDictionary(this.strm,n))!==l)throw new Error(i[r]);this._dict_set=!0}}function n(e,t){var r=new p(t);if(r.push(e,!0),r.err)throw r.msg||i[r.err];return r.result}p.prototype.push=function(e,t){var r,n,i=this.strm,s=this.options.chunkSize;if(this.ended)return!1;n=t===~~t?t:!0===t?4:0,"string"==typeof e?i.input=h.string2buf(e):"[object ArrayBuffer]"===u.call(e)?i.input=new Uint8Array(e):i.input=e,i.next_in=0,i.avail_in=i.input.length;do{if(0===i.avail_out&&(i.output=new o.Buf8(s),i.next_out=0,i.avail_out=s),1!==(r=a.deflate(i,n))&&r!==l)return this.onEnd(r),!(this.ended=!0);0!==i.avail_out&&(0!==i.avail_in||4!==n&&2!==n)||("string"===this.options.to?this.onData(h.buf2binstring(o.shrinkBuf(i.output,i.next_out))):this.onData(o.shrinkBuf(i.output,i.next_out)))}while((0<i.avail_in||0===i.avail_out)&&1!==r);return 4===n?(r=a.deflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===l):2!==n||(this.onEnd(l),!(i.avail_out=0))},p.prototype.onData=function(e){this.chunks.push(e)},p.prototype.onEnd=function(e){e===l&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=o.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},r.Deflate=p,r.deflate=n,r.deflateRaw=function(e,t){return(t=t||{}).raw=!0,n(e,t)},r.gzip=function(e,t){return(t=t||{}).gzip=!0,n(e,t)}},{"./utils/common":41,"./utils/strings":42,"./zlib/deflate":46,"./zlib/messages":51,"./zlib/zstream":53}],40:[function(e,t,r){"use strict";var c=e("./zlib/inflate"),d=e("./utils/common"),p=e("./utils/strings"),m=e("./zlib/constants"),n=e("./zlib/messages"),i=e("./zlib/zstream"),s=e("./zlib/gzheader"),_=Object.prototype.toString;function a(e){if(!(this instanceof a))return new a(e);this.options=d.assign({chunkSize:16384,windowBits:0,to:""},e||{});var t=this.options;t.raw&&0<=t.windowBits&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(0<=t.windowBits&&t.windowBits<16)||e&&e.windowBits||(t.windowBits+=32),15<t.windowBits&&t.windowBits<48&&0==(15&t.windowBits)&&(t.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new i,this.strm.avail_out=0;var r=c.inflateInit2(this.strm,t.windowBits);if(r!==m.Z_OK)throw new Error(n[r]);this.header=new s,c.inflateGetHeader(this.strm,this.header)}function o(e,t){var r=new a(t);if(r.push(e,!0),r.err)throw r.msg||n[r.err];return r.result}a.prototype.push=function(e,t){var r,n,i,s,a,o,h=this.strm,u=this.options.chunkSize,l=this.options.dictionary,f=!1;if(this.ended)return!1;n=t===~~t?t:!0===t?m.Z_FINISH:m.Z_NO_FLUSH,"string"==typeof e?h.input=p.binstring2buf(e):"[object ArrayBuffer]"===_.call(e)?h.input=new Uint8Array(e):h.input=e,h.next_in=0,h.avail_in=h.input.length;do{if(0===h.avail_out&&(h.output=new d.Buf8(u),h.next_out=0,h.avail_out=u),(r=c.inflate(h,m.Z_NO_FLUSH))===m.Z_NEED_DICT&&l&&(o="string"==typeof l?p.string2buf(l):"[object ArrayBuffer]"===_.call(l)?new Uint8Array(l):l,r=c.inflateSetDictionary(this.strm,o)),r===m.Z_BUF_ERROR&&!0===f&&(r=m.Z_OK,f=!1),r!==m.Z_STREAM_END&&r!==m.Z_OK)return this.onEnd(r),!(this.ended=!0);h.next_out&&(0!==h.avail_out&&r!==m.Z_STREAM_END&&(0!==h.avail_in||n!==m.Z_FINISH&&n!==m.Z_SYNC_FLUSH)||("string"===this.options.to?(i=p.utf8border(h.output,h.next_out),s=h.next_out-i,a=p.buf2string(h.output,i),h.next_out=s,h.avail_out=u-s,s&&d.arraySet(h.output,h.output,i,s,0),this.onData(a)):this.onData(d.shrinkBuf(h.output,h.next_out)))),0===h.avail_in&&0===h.avail_out&&(f=!0)}while((0<h.avail_in||0===h.avail_out)&&r!==m.Z_STREAM_END);return r===m.Z_STREAM_END&&(n=m.Z_FINISH),n===m.Z_FINISH?(r=c.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===m.Z_OK):n!==m.Z_SYNC_FLUSH||(this.onEnd(m.Z_OK),!(h.avail_out=0))},a.prototype.onData=function(e){this.chunks.push(e)},a.prototype.onEnd=function(e){e===m.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=d.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},r.Inflate=a,r.inflate=o,r.inflateRaw=function(e,t){return(t=t||{}).raw=!0,o(e,t)},r.ungzip=o},{"./utils/common":41,"./utils/strings":42,"./zlib/constants":44,"./zlib/gzheader":47,"./zlib/inflate":49,"./zlib/messages":51,"./zlib/zstream":53}],41:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;r.assign=function(e){for(var t=Array.prototype.slice.call(arguments,1);t.length;){var r=t.shift();if(r){if("object"!=typeof r)throw new TypeError(r+"must be non-object");for(var n in r)r.hasOwnProperty(n)&&(e[n]=r[n])}}return e},r.shrinkBuf=function(e,t){return e.length===t?e:e.subarray?e.subarray(0,t):(e.length=t,e)};var i={arraySet:function(e,t,r,n,i){if(t.subarray&&e.subarray)e.set(t.subarray(r,r+n),i);else for(var s=0;s<n;s++)e[i+s]=t[r+s]},flattenChunks:function(e){var t,r,n,i,s,a;for(t=n=0,r=e.length;t<r;t++)n+=e[t].length;for(a=new Uint8Array(n),t=i=0,r=e.length;t<r;t++)s=e[t],a.set(s,i),i+=s.length;return a}},s={arraySet:function(e,t,r,n,i){for(var s=0;s<n;s++)e[i+s]=t[r+s]},flattenChunks:function(e){return[].concat.apply([],e)}};r.setTyped=function(e){e?(r.Buf8=Uint8Array,r.Buf16=Uint16Array,r.Buf32=Int32Array,r.assign(r,i)):(r.Buf8=Array,r.Buf16=Array,r.Buf32=Array,r.assign(r,s))},r.setTyped(n)},{}],42:[function(e,t,r){"use strict";var h=e("./common"),i=!0,s=!0;try{String.fromCharCode.apply(null,[0])}catch(e){i=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(e){s=!1}for(var u=new h.Buf8(256),n=0;n<256;n++)u[n]=252<=n?6:248<=n?5:240<=n?4:224<=n?3:192<=n?2:1;function l(e,t){if(t<65537&&(e.subarray&&s||!e.subarray&&i))return String.fromCharCode.apply(null,h.shrinkBuf(e,t));for(var r="",n=0;n<t;n++)r+=String.fromCharCode(e[n]);return r}u[254]=u[254]=1,r.string2buf=function(e){var t,r,n,i,s,a=e.length,o=0;for(i=0;i<a;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),o+=r<128?1:r<2048?2:r<65536?3:4;for(t=new h.Buf8(o),i=s=0;s<o;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),r<128?t[s++]=r:(r<2048?t[s++]=192|r>>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r<n;r++)t[r]=e.charCodeAt(r);return t},r.buf2string=function(e,t){var r,n,i,s,a=t||e.length,o=new Array(2*a);for(r=n=0;r<a;)if((i=e[r++])<128)o[n++]=i;else if(4<(s=u[i]))o[n++]=65533,r+=s-1;else{for(i&=2===s?31:3===s?15:7;1<s&&r<a;)i=i<<6|63&e[r++],s--;1<s?o[n++]=65533:i<65536?o[n++]=i:(i-=65536,o[n++]=55296|i>>10&1023,o[n++]=56320|1023&i)}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}},{"./common":41}],43:[function(e,t,r){"use strict";t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3<r?2e3:r;s=s+(i=i+t[n++]|0)|0,--a;);i%=65521,s%=65521}return i|s<<16|0}},{}],44:[function(e,t,r){"use strict";t.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],45:[function(e,t,r){"use strict";var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}},{}],46:[function(e,t,r){"use strict";var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return(e<<1)-(4<e?9:0)}function D(e){for(var t=e.length;0<=--t;)e[t]=0}function F(e){var t=e.state,r=t.pending;r>e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&s<c);if(n=S-(c-s),s=c-S,a<n){if(e.match_start=t,o<=(a=n))break;d=u[s+a-1],p=u[s+a]}}}while((t=f[t&l])>h&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u<l&&(l=u),r=0===l?0:(a.avail_in-=l,c.arraySet(o,a.input,a.next_in,l,h),1===a.state.wrap?a.adler=d(a.adler,o,l,h):2===a.state.wrap&&(a.adler=p(a.adler,o,l,h)),a.next_in+=l,a.total_in+=l,l),e.lookahead+=r,e.lookahead+e.insert>=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<<e.hash_shift^e.window[s+1])&e.hash_mask;e.insert&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[s+x-1])&e.hash_mask,e.prev[s&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=s,s++,e.insert--,!(e.lookahead+e.insert<x)););}while(e.lookahead<z&&0!==e.strm.avail_in)}function Z(e,t){for(var r,n;;){if(e.lookahead<z){if(j(e),e.lookahead<z&&t===l)return A;if(0===e.lookahead)break}if(r=0,e.lookahead>=x&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!==r&&e.strstart-r<=e.w_size-z&&(e.match_length=L(e,r)),e.match_length>=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart,0!=--e.match_length;);e.strstart++}else e.strstart+=e.match_length,e.match_length=0,e.ins_h=e.window[e.strstart],e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+1])&e.hash_mask;else n=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++;if(n&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=e.strstart<x-1?e.strstart:x-1,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}function W(e,t){for(var r,n,i;;){if(e.lookahead<z){if(j(e),e.lookahead<z&&t===l)return A;if(0===e.lookahead)break}if(r=0,e.lookahead>=x&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),e.prev_length=e.match_length,e.prev_match=e.match_start,e.match_length=x-1,0!==r&&e.prev_length<e.max_lazy_match&&e.strstart-r<=e.w_size-z&&(e.match_length=L(e,r),e.match_length<=5&&(1===e.strategy||e.match_length===x&&4096<e.strstart-e.match_start)&&(e.match_length=x-1)),e.prev_length>=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!=--e.prev_length;);if(e.match_available=0,e.match_length=x-1,e.strstart++,n&&(N(e,!1),0===e.strm.avail_out))return A}else if(e.match_available){if((n=u._tr_tally(e,0,e.window[e.strstart-1]))&&N(e,!1),e.strstart++,e.lookahead--,0===e.strm.avail_out)return A}else e.match_available=1,e.strstart++,e.lookahead--}return e.match_available&&(n=u._tr_tally(e,0,e.window[e.strstart-1]),e.match_available=0),e.insert=e.strstart<x-1?e.strstart:x-1,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}function M(e,t,r,n,i){this.good_length=e,this.max_lazy=t,this.nice_length=r,this.max_chain=n,this.func=i}function H(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=v,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new c.Buf16(2*w),this.dyn_dtree=new c.Buf16(2*(2*a+1)),this.bl_tree=new c.Buf16(2*(2*o+1)),D(this.dyn_ltree),D(this.dyn_dtree),D(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new c.Buf16(k+1),this.heap=new c.Buf16(2*s+1),D(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new c.Buf16(2*s+1),D(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function G(e){var t;return e&&e.state?(e.total_in=e.total_out=0,e.data_type=i,(t=e.state).pending=0,t.pending_out=0,t.wrap<0&&(t.wrap=-t.wrap),t.status=t.wrap?C:E,e.adler=2===t.wrap?0:1,t.last_flush=l,u._tr_init(t),m):R(e,_)}function K(e){var t=G(e);return t===m&&function(e){e.window_size=2*e.w_size,D(e.head),e.max_lazy_match=h[e.level].max_lazy,e.good_match=h[e.level].good_length,e.nice_match=h[e.level].nice_length,e.max_chain_length=h[e.level].max_chain,e.strstart=0,e.block_start=0,e.lookahead=0,e.insert=0,e.match_length=e.prev_length=x-1,e.match_available=0,e.ins_h=0}(e.state),t}function Y(e,t,r,n,i,s){if(!e)return _;var a=1;if(t===g&&(t=6),n<0?(a=0,n=-n):15<n&&(a=2,n-=16),i<1||y<i||r!==v||n<8||15<n||t<0||9<t||s<0||b<s)return R(e,_);8===n&&(n=9);var o=new H;return(e.state=o).strm=e,o.wrap=a,o.gzhead=null,o.w_bits=n,o.w_size=1<<o.w_bits,o.w_mask=o.w_size-1,o.hash_bits=i+7,o.hash_size=1<<o.hash_bits,o.hash_mask=o.hash_size-1,o.hash_shift=~~((o.hash_bits+x-1)/x),o.window=new c.Buf8(2*o.w_size),o.head=new c.Buf16(o.hash_size),o.prev=new c.Buf16(o.w_size),o.lit_bufsize=1<<i+6,o.pending_buf_size=4*o.lit_bufsize,o.pending_buf=new c.Buf8(o.pending_buf_size),o.d_buf=1*o.lit_bufsize,o.l_buf=3*o.lit_bufsize,o.level=t,o.strategy=s,o.method=r,K(e)}h=[new M(0,0,0,0,function(e,t){var r=65535;for(r>e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5<t||t<0)return e?R(e,_):_;if(n=e.state,!e.output||!e.input&&0!==e.avail_in||666===n.status&&t!==f)return R(e,0===e.avail_out?-5:_);if(n.strm=e,r=n.last_flush,n.last_flush=t,n.status===C)if(2===n.wrap)e.adler=0,U(n,31),U(n,139),U(n,8),n.gzhead?(U(n,(n.gzhead.text?1:0)+(n.gzhead.hcrc?2:0)+(n.gzhead.extra?4:0)+(n.gzhead.name?8:0)+(n.gzhead.comment?16:0)),U(n,255&n.gzhead.time),U(n,n.gzhead.time>>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else{var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindex<n.gzhead.name.length?255&n.gzhead.name.charCodeAt(n.gzindex++):0,U(n,s)}while(0!==s);n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindex<n.gzhead.comment.length?255&n.gzhead.comment.charCodeAt(n.gzindex++):0,U(n,s)}while(0!==s);n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0<e.strstart&&(n=a[i=e.strstart-1])===a[++i]&&n===a[++i]&&n===a[++i]){s=e.strstart+S;do{}while(n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&i<s);e.match_length=S-(s-i),e.match_length>e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0<n.wrap&&(n.wrap=-n.wrap),0!==n.pending?m:1)},r.deflateEnd=function(e){var t;return e&&e.state?(t=e.state.status)!==C&&69!==t&&73!==t&&91!==t&&103!==t&&t!==E&&666!==t?R(e,_):(e.state=null,t===E?R(e,-3):m):_},r.deflateSetDictionary=function(e,t){var r,n,i,s,a,o,h,u,l=t.length;if(!e||!e.state)return _;if(2===(s=(r=e.state).wrap)||1===s&&r.status!==C||r.lookahead)return _;for(1===s&&(e.adler=d(e.adler,t,l,0)),r.wrap=0,l>=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<<r.hash_shift^r.window[n+x-1])&r.hash_mask,r.prev[n&r.w_mask]=r.head[r.ins_h],r.head[r.ins_h]=n,n++,--i;);r.strstart=n,r.lookahead=x-1,j(r)}return r.strstart+=r.lookahead,r.block_start=r.strstart,r.insert=r.lookahead,r.lookahead=0,r.match_length=r.prev_length=x-1,r.match_available=0,e.next_in=o,e.input=h,e.avail_in=a,r.wrap=s,m},r.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./messages":51,"./trees":52}],47:[function(e,t,r){"use strict";t.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],48:[function(e,t,r){"use strict";t.exports=function(e,t){var r,n,i,s,a,o,h,u,l,f,c,d,p,m,_,g,b,v,y,w,k,x,S,z,C;r=e.state,n=e.next_in,z=e.input,i=n+(e.avail_in-5),s=e.next_out,C=e.output,a=s-(t-e.avail_out),o=s+(e.avail_out-257),h=r.dmax,u=r.wsize,l=r.whave,f=r.wnext,c=r.window,d=r.hold,p=r.bits,m=r.lencode,_=r.distcode,g=(1<<r.lenbits)-1,b=(1<<r.distbits)-1;e:do{p<15&&(d+=z[n++]<<p,p+=8,d+=z[n++]<<p,p+=8),v=m[d&g];t:for(;;){if(d>>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<<y)-1)];continue t}if(32&y){r.mode=12;break e}e.msg="invalid literal/length code",r.mode=30;break e}w=65535&v,(y&=15)&&(p<y&&(d+=z[n++]<<p,p+=8),w+=d&(1<<y)-1,d>>>=y,p-=y),p<15&&(d+=z[n++]<<p,p+=8,d+=z[n++]<<p,p+=8),v=_[d&b];r:for(;;){if(d>>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<<y)-1)];continue r}e.msg="invalid distance code",r.mode=30;break e}if(k=65535&v,p<(y&=15)&&(d+=z[n++]<<p,(p+=8)<y&&(d+=z[n++]<<p,p+=8)),h<(k+=d&(1<<y)-1)){e.msg="invalid distance too far back",r.mode=30;break e}if(d>>>=y,p-=y,(y=s-a)<k){if(l<(y=k-y)&&r.sane){e.msg="invalid distance too far back",r.mode=30;break e}if(S=c,(x=0)===f){if(x+=u-y,y<w){for(w-=y;C[s++]=c[x++],--y;);x=s-k,S=C}}else if(f<y){if(x+=u+f-y,(y-=f)<w){for(w-=y;C[s++]=c[x++],--y;);if(x=0,f<w){for(w-=y=f;C[s++]=c[x++],--y;);x=s-k,S=C}}}else if(x+=f-y,y<w){for(w-=y;C[s++]=c[x++],--y;);x=s-k,S=C}for(;2<w;)C[s++]=S[x++],C[s++]=S[x++],C[s++]=S[x++],w-=3;w&&(C[s++]=S[x++],1<w&&(C[s++]=S[x++]))}else{for(x=s-k;C[s++]=C[x++],C[s++]=C[x++],C[s++]=C[x++],2<(w-=3););w&&(C[s++]=C[x++],1<w&&(C[s++]=C[x++]))}break}}break}}while(n<i&&s<o);n-=w=p>>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n<i?i-n+5:5-(n-i),e.avail_out=s<o?o-s+257:257-(s-o),r.hold=d,r.bits=p}},{}],49:[function(e,t,r){"use strict";var I=e("../utils/common"),O=e("./adler32"),B=e("./crc32"),R=e("./inffast"),T=e("./inftrees"),D=1,F=2,N=0,U=-2,P=1,n=852,i=592;function L(e){return(e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15<t)?U:(null!==n.window&&n.wbits!==t&&(n.window=null),n.wrap=r,n.wbits=t,o(e))):U}function u(e,t){var r,n;return e?(n=new s,(e.state=n).window=null,(r=h(e,t))!==N&&(e.state=null),r):U}var l,f,c=!0;function j(e){if(c){var t;for(l=new I.Buf32(512),f=new I.Buf32(32),t=0;t<144;)e.lens[t++]=8;for(;t<256;)e.lens[t++]=9;for(;t<280;)e.lens[t++]=7;for(;t<288;)e.lens[t++]=8;for(T(D,e.lens,0,288,l,0,e.work,{bits:9}),t=0;t<32;)e.lens[t++]=5;T(F,e.lens,0,32,f,0,e.work,{bits:5}),c=!1}e.lencode=l,e.lenbits=9,e.distcode=f,e.distbits=5}function Z(e,t,r,n){var i,s=e.state;return null===s.window&&(s.wsize=1<<s.wbits,s.wnext=0,s.whave=0,s.window=new I.Buf8(s.wsize)),n>=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave<s.wsize&&(s.whave+=i))),0}r.inflateReset=o,r.inflateReset2=h,r.inflateResetKeep=a,r.inflateInit=function(e){return u(e,15)},r.inflateInit2=u,r.inflate=function(e,t){var r,n,i,s,a,o,h,u,l,f,c,d,p,m,_,g,b,v,y,w,k,x,S,z,C=0,E=new I.Buf8(4),A=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!e||!e.state||!e.output||!e.input&&0!==e.avail_in)return U;12===(r=e.state).mode&&(r.mode=13),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,f=o,c=h,x=N;e:for(;;)switch(r.mode){case P:if(0===r.wrap){r.mode=13;break}for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(2&r.wrap&&35615===u){E[r.check=0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<<k,e.adler=r.check=1,r.mode=512&u?10:12,l=u=0;break;case 2:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(r.flags=u,8!=(255&r.flags)){e.msg="unknown compression method",r.mode=30;break}if(57344&r.flags){e.msg="unknown header flags set",r.mode=30;break}r.head&&(r.head.text=u>>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.head&&(r.head.time=u),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.head&&(r.head.xflags=255&u,r.head.os=u>>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.length=u,r.head&&(r.head.extra_len=u),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d<o;);if(512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,k)break e}else r.head&&(r.head.name=null);r.length=0,r.mode=8;case 8:if(4096&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.comment+=String.fromCharCode(k)),k&&d<o;);if(512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,k)break e}else r.head&&(r.head.comment=null);r.mode=9;case 9:if(512&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u!==(65535&r.check)){e.msg="header crc mismatch",r.mode=30;break}l=u=0}r.head&&(r.head.hcrc=r.flags>>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}e.adler=r.check=L(u),l=u=0,r.mode=11;case 11:if(0===r.havedict)return e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,2;e.adler=r.check=1,r.mode=12;case 12:if(5===t||6===t)break e;case 13:if(r.last){u>>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}switch(r.last=1&u,l-=1,3&(u>>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if((65535&u)!=(u>>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o<d&&(d=o),h<d&&(d=h),0===d)break e;I.arraySet(i,n,s,d,a),o-=d,s+=d,h-=d,a+=d,r.length-=d;break}r.mode=12;break;case 17:for(;l<14;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(r.nlen=257+(31&u),u>>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286<r.nlen||30<r.ndist){e.msg="too many length or distance symbols",r.mode=30;break}r.have=0,r.mode=18;case 18:for(;r.have<r.ncode;){for(;l<3;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.lens[A[r.have++]]=7&u,u>>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have<r.nlen+r.ndist;){for(;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(b<16)u>>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u>>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}l-=_,k=0,d=3+(7&(u>>>=_)),u>>>=3,l-=3}else{for(z=_+7;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}l-=_,k=0,d=11+(127&(u>>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(g&&0==(240&g)){for(v=_,y=g,w=b;g=(C=r.lencode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}u>>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.length+=u&(1<<r.extra)-1,u>>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<<r.distbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(0==(240&g)){for(v=_,y=g,w=b;g=(C=r.distcode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}u>>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.offset+=u&(1<<r.extra)-1,u>>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(h<d&&(d=h),h-=d,r.length-=d;i[a++]=m[p++],--d;);0===r.length&&(r.mode=21);break;case 26:if(0===h)break e;i[a++]=r.length,h--,r.mode=21;break;case 27:if(r.wrap){for(;l<32;){if(0===o)break e;o--,u|=n[s++]<<l,l+=8}if(c-=h,e.total_out+=c,r.total+=c,c&&(e.adler=r.check=r.flags?B(r.check,i,c,a-c):O(r.check,i,c,a-c)),c=h,(r.flags?u:L(u))!==r.check){e.msg="incorrect data check",r.mode=30;break}l=u=0}r.mode=28;case 28:if(r.wrap&&r.flags){for(;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u!==(4294967295&r.total)){e.msg="incorrect length check",r.mode=30;break}l=u=0}r.mode=29;case 29:x=1;break e;case 30:x=-3;break e;case 31:return-4;case 32:default:return U}return e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,(r.wsize||c!==e.avail_out&&r.mode<30&&(r.mode<27||4!==t))&&Z(e,e.output,e.next_out,c-e.avail_out)?(r.mode=31,-4):(f-=e.avail_in,c-=e.avail_out,e.total_in+=f,e.total_out+=c,r.total+=c,r.wrap&&c&&(e.adler=r.check=r.flags?B(r.check,i,c,e.next_out-c):O(r.check,i,c,e.next_out-c)),e.data_type=r.bits+(r.last?64:0)+(12===r.mode?128:0)+(20===r.mode||15===r.mode?256:0),(0==f&&0===c||4===t)&&x===N&&(x=-5),x)},r.inflateEnd=function(e){if(!e||!e.state)return U;var t=e.state;return t.window&&(t.window=null),e.state=null,N},r.inflateGetHeader=function(e,t){var r;return e&&e.state?0==(2&(r=e.state).wrap)?U:((r.head=t).done=!1,N):U},r.inflateSetDictionary=function(e,t){var r,n=t.length;return e&&e.state?0!==(r=e.state).wrap&&11!==r.mode?U:11===r.mode&&O(1,t,n,0)!==r.check?-3:Z(e,t,n,n)?(r.mode=31,-4):(r.havedict=1,N):U},r.inflateInfo="pako inflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./inffast":48,"./inftrees":50}],50:[function(e,t,r){"use strict";var D=e("../utils/common"),F=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],N=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],U=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],P=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];t.exports=function(e,t,r,n,i,s,a,o){var h,u,l,f,c,d,p,m,_,g=o.bits,b=0,v=0,y=0,w=0,k=0,x=0,S=0,z=0,C=0,E=0,A=null,I=0,O=new D.Buf16(16),B=new D.Buf16(16),R=null,T=0;for(b=0;b<=15;b++)O[b]=0;for(v=0;v<n;v++)O[t[r+v]]++;for(k=g,w=15;1<=w&&0===O[w];w--);if(w<k&&(k=w),0===w)return i[s++]=20971520,i[s++]=20971520,o.bits=1,0;for(y=1;y<w&&0===O[y];y++);for(k<y&&(k=y),b=z=1;b<=15;b++)if(z<<=1,(z-=O[b])<0)return-1;if(0<z&&(0===e||1!==w))return-1;for(B[1]=0,b=1;b<15;b++)B[b+1]=B[b]+O[b];for(v=0;v<n;v++)0!==t[r+v]&&(a[B[t[r+v]]++]=v);if(d=0===e?(A=R=a,19):1===e?(A=F,I-=257,R=N,T-=257,256):(A=U,R=P,-1),b=y,c=s,S=v=E=0,l=-1,f=(C=1<<(x=k))-1,1===e&&852<C||2===e&&592<C)return 1;for(;;){for(p=b-S,_=a[v]<d?(m=0,a[v]):a[v]>d?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<<b-S,y=u=1<<x;i[c+(E>>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<<b-1;E&h;)h>>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k<b&&(E&f)!==l){for(0===S&&(S=k),c+=y,z=1<<(x=b-S);x+S<w&&!((z-=O[x+S])<=0);)x++,z<<=1;if(C+=1<<x,1===e&&852<C||2===e&&592<C)return 1;i[l=E&f]=k<<24|x<<16|c-s|0}}return 0!==E&&(i[c+E]=b-S<<24|64<<16|0),o.bits=k,0}},{"../utils/common":41}],51:[function(e,t,r){"use strict";t.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],52:[function(e,t,r){"use strict";var i=e("../utils/common"),o=0,h=1;function n(e){for(var t=e.length;0<=--t;)e[t]=0}var s=0,a=29,u=256,l=u+1+a,f=30,c=19,_=2*l+1,g=15,d=16,p=7,m=256,b=16,v=17,y=18,w=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],k=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],x=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],S=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],z=new Array(2*(l+2));n(z);var C=new Array(2*f);n(C);var E=new Array(512);n(E);var A=new Array(256);n(A);var I=new Array(a);n(I);var O,B,R,T=new Array(f);function D(e,t,r,n,i){this.static_tree=e,this.extra_bits=t,this.extra_base=r,this.elems=n,this.max_length=i,this.has_stree=e&&e.length}function F(e,t){this.dyn_tree=e,this.max_code=0,this.stat_desc=t}function N(e){return e<256?E[e]:E[256+(e>>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<<e.bi_valid&65535,U(e,e.bi_buf),e.bi_buf=t>>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<<e.bi_valid&65535,e.bi_valid+=r)}function L(e,t,r){P(e,r[2*t],r[2*t+1])}function j(e,t){for(var r=0;r|=1&e,e>>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t<l;t++)e.dyn_ltree[2*t]=0;for(t=0;t<f;t++)e.dyn_dtree[2*t]=0;for(t=0;t<c;t++)e.bl_tree[2*t]=0;e.dyn_ltree[2*m]=1,e.opt_len=e.static_len=0,e.last_lit=e.matches=0}function M(e){8<e.bi_valid?U(e,e.bi_buf):0<e.bi_valid&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0}function H(e,t,r,n){var i=2*t,s=2*r;return e[i]<e[s]||e[i]===e[s]&&n[t]<=n[r]}function G(e,t,r){for(var n=e.heap[r],i=r<<1;i<=e.heap_len&&(i<e.heap_len&&H(t,e.heap[i+1],e.heap[i],e.depth)&&i++,!H(t,n,e.heap[i],e.depth));)e.heap[r]=e.heap[i],r=i,i<<=1;e.heap[r]=n}function K(e,t,r){var n,i,s,a,o=0;if(0!==e.last_lit)for(;n=e.pending_buf[e.d_buf+2*o]<<8|e.pending_buf[e.d_buf+2*o+1],i=e.pending_buf[e.l_buf+o],o++,0===n?L(e,i,t):(L(e,(s=A[i])+u+1,t),0!==(a=w[s])&&P(e,i-=I[s],a),L(e,s=N(--n),r),0!==(a=k[s])&&P(e,n-=T[s],a)),o<e.last_lit;);L(e,m,t)}function Y(e,t){var r,n,i,s=t.dyn_tree,a=t.stat_desc.static_tree,o=t.stat_desc.has_stree,h=t.stat_desc.elems,u=-1;for(e.heap_len=0,e.heap_max=_,r=0;r<h;r++)0!==s[2*r]?(e.heap[++e.heap_len]=u=r,e.depth[r]=0):s[2*r+1]=0;for(;e.heap_len<2;)s[2*(i=e.heap[++e.heap_len]=u<2?++u:0)]=1,e.depth[i]=0,e.opt_len--,o&&(e.static_len-=a[2*i+1]);for(t.max_code=u,r=e.heap_len>>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u<n||(e.bl_count[s]++,a=0,d<=n&&(a=c[n-d]),o=h[2*n],e.opt_len+=o*(s+a),f&&(e.static_len+=o*(l[2*n+1]+a)));if(0!==m){do{for(s=p-1;0===e.bl_count[s];)s--;e.bl_count[s]--,e.bl_count[s+1]+=2,e.bl_count[p]--,m-=2}while(0<m);for(s=p;0!==s;s--)for(n=e.bl_count[s];0!==n;)u<(i=e.heap[--r])||(h[2*i+1]!==s&&(e.opt_len+=(s-h[2*i+1])*h[2*i],h[2*i+1]=s),n--)}}(e,t),Z(s,u,e.bl_count)}function X(e,t,r){var n,i,s=-1,a=t[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),t[2*(r+1)+1]=65535,n=0;n<=r;n++)i=a,a=t[2*(n+1)+1],++o<h&&i===a||(o<u?e.bl_tree[2*i]+=o:0!==i?(i!==s&&e.bl_tree[2*i]++,e.bl_tree[2*b]++):o<=10?e.bl_tree[2*v]++:e.bl_tree[2*y]++,s=i,u=(o=0)===a?(h=138,3):i===a?(h=6,3):(h=7,4))}function V(e,t,r){var n,i,s=-1,a=t[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),n=0;n<=r;n++)if(i=a,a=t[2*(n+1)+1],!(++o<h&&i===a)){if(o<u)for(;L(e,i,e.bl_tree),0!=--o;);else 0!==i?(i!==s&&(L(e,i,e.bl_tree),o--),L(e,b,e.bl_tree),P(e,o-3,2)):o<=10?(L(e,v,e.bl_tree),P(e,o-3,3)):(L(e,y,e.bl_tree),P(e,o-11,7));s=i,u=(o=0)===a?(h=138,3):i===a?(h=6,3):(h=7,4)}}n(T);var q=!1;function J(e,t,r,n){P(e,(s<<1)+(n?1:0),3),function(e,t,r,n){M(e),n&&(U(e,r),U(e,~r)),i.arraySet(e.pending_buf,e.window,t,r,e.pending),e.pending+=r}(e,t,r,!0)}r._tr_init=function(e){q||(function(){var e,t,r,n,i,s=new Array(g+1);for(n=r=0;n<a-1;n++)for(I[n]=r,e=0;e<1<<w[n];e++)A[r++]=n;for(A[r-1]=n,n=i=0;n<16;n++)for(T[n]=i,e=0;e<1<<k[n];e++)E[i++]=n;for(i>>=7;n<f;n++)for(T[n]=i<<7,e=0;e<1<<k[n]-7;e++)E[256+i++]=n;for(t=0;t<=g;t++)s[t]=0;for(e=0;e<=143;)z[2*e+1]=8,e++,s[8]++;for(;e<=255;)z[2*e+1]=9,e++,s[9]++;for(;e<=279;)z[2*e+1]=7,e++,s[7]++;for(;e<=287;)z[2*e+1]=8,e++,s[8]++;for(Z(z,l+1,s),e=0;e<f;e++)C[2*e+1]=5,C[2*e]=j(e,5);O=new D(z,w,u+1,l,g),B=new D(C,k,0,f,g),R=new D(new Array(0),x,0,c,p)}(),q=!0),e.l_desc=new F(e.dyn_ltree,O),e.d_desc=new F(e.dyn_dtree,B),e.bl_desc=new F(e.bl_tree,R),e.bi_buf=0,e.bi_valid=0,W(e)},r._tr_stored_block=J,r._tr_flush_block=function(e,t,r,n){var i,s,a=0;0<e.level?(2===e.strm.data_type&&(e.strm.data_type=function(e){var t,r=4093624447;for(t=0;t<=31;t++,r>>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t<u;t++)if(0!==e.dyn_ltree[2*t])return h;return o}(e)),Y(e,e.l_desc),Y(e,e.d_desc),a=function(e){var t;for(X(e,e.dyn_ltree,e.l_desc.max_code),X(e,e.dyn_dtree,e.d_desc.max_code),Y(e,e.bl_desc),t=c-1;3<=t&&0===e.bl_tree[2*S[t]+1];t--);return e.opt_len+=3*(t+1)+5+5+4,t}(e),i=e.opt_len+3+7>>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i<n;i++)P(e,e.bl_tree[2*S[i]+1],3);V(e,e.dyn_ltree,t-1),V(e,e.dyn_dtree,r-1)}(e,e.l_desc.max_code+1,e.d_desc.max_code+1,a+1),K(e,e.dyn_ltree,e.dyn_dtree)),W(e),n&&M(e)},r._tr_tally=function(e,t,r){return e.pending_buf[e.d_buf+2*e.last_lit]=t>>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r<t.length;r++)t[r]=arguments[r+1];var n={callback:e,args:t};return h[o]=n,i(o),o++},e.clearImmediate=f}function f(e){delete h[e]}function c(e){if(u)setTimeout(c,0,e);else{var t=h[e];if(t){u=!0;try{!function(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}(t)}finally{f(e),u=!1}}}}function d(e){e.source===r&&"string"==typeof e.data&&0===e.data.indexOf(a)&&c(+e.data.slice(a.length))}}("undefined"==typeof self?void 0===e?this:e:self)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[10])(10)});
|
||
/*
|
||
* @license
|
||
* docx-preview <https://github.com/VolodymyrBaydalka/docxjs>
|
||
* Released under Apache License 2.0 <https://github.com/VolodymyrBaydalka/docxjs/blob/master/LICENSE>
|
||
* Copyright Volodymyr Baydalka
|
||
*/
|
||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("jszip")):"function"==typeof define&&define.amd?define(["exports","jszip"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).docx={},e.JSZip)}(this,function(e,t){"use strict";var r;function a(e){return/^[^"'].*\s.*[^"']$/.test(e)?`'${e}'`:e}function s(e){let t=e.lastIndexOf("/")+1;return[0==t?"":e.substring(0,t),0==t?e:e.substring(t)]}function n(e,t){try{const r="http://docx/";return new URL(e,r+t).toString().substring(r.length)}catch{return`${t}${e}`}}function l(e,t){return e.reduce((e,r)=>(e[t(r)]=r,e),{})}function o(e){return e&&"object"==typeof e&&!Array.isArray(e)}function i(e,...t){if(!t.length)return e;const r=t.shift();if(o(e)&&o(r))for(const t in r)if(o(r[t])){i(e[t]??(e[t]={}),r[t])}else e[t]=r[t];return i(e,...t)}function c(e){return Array.isArray(e)?e:[e]}!function(e){e.OfficeDocument="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",e.FontTable="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable",e.Image="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",e.Numbering="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",e.Styles="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",e.StylesWithEffects="http://schemas.microsoft.com/office/2007/relationships/stylesWithEffects",e.Theme="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",e.Settings="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings",e.WebSettings="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings",e.Hyperlink="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",e.Footnotes="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",e.Endnotes="http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes",e.Footer="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",e.Header="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",e.ExtendedProperties="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",e.CoreProperties="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",e.CustomProperties="http://schemas.openxmlformats.org/package/2006/relationships/metadata/custom-properties",e.Comments="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",e.CommentsExtended="http://schemas.microsoft.com/office/2011/relationships/commentsExtended",e.AltChunk="http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"}(r||(r={}));const h="http://schemas.openxmlformats.org/wordprocessingml/2006/main",m={mul:.05,unit:"pt"},p={mul:1/12700,unit:"pt"},u={mul:.5,unit:"pt"},d={mul:.125,unit:"pt",min:.25,max:12},f={mul:1,unit:"pt"},g={mul:.02,unit:"%"};function b(e,t=m){if(null==e||/.+(p[xt]|[%])$/.test(e))return e;var r=parseInt(e)*t.mul;return t.min&&t.max&&(r=function(e,t,r){return t>e?t:r<e?r:e}(r,t.min,t.max)),`${r.toFixed(2)}${t.unit}`}function y(e,t,r){if(e.namespaceURI!=h)return!1;switch(e.localName){case"color":t.color=r.attr(e,"val");break;case"sz":t.fontSize=r.lengthAttr(e,"val",u);break;default:return!1}return!0}class k{elements(e,t=null){const r=[];for(let a=0,s=e.childNodes.length;a<s;a++){let s=e.childNodes.item(a);s.nodeType!=Node.ELEMENT_NODE||null!=t&&s.localName!=t||r.push(s)}return r}element(e,t){for(let r=0,a=e.childNodes.length;r<a;r++){let a=e.childNodes.item(r);if(1==a.nodeType&&a.localName==t)return a}return null}elementAttr(e,t,r){var a=this.element(e,t);return a?this.attr(a,r):void 0}attrs(e){return Array.from(e.attributes)}attr(e,t){for(let r=0,a=e.attributes.length;r<a;r++){let a=e.attributes.item(r);if(a.localName==t)return a.value}return null}intAttr(e,t,r=null){var a=this.attr(e,t);return a?parseInt(a):r}hexAttr(e,t,r=null){var a=this.attr(e,t);return a?parseInt(a,16):r}floatAttr(e,t,r=null){var a=this.attr(e,t);return a?parseFloat(a):r}boolAttr(e,t,r=null){return function(e,t=!1){switch(e){case"1":case"on":case"true":return!0;case"0":case"off":case"false":return!1;default:return t}}(this.attr(e,t),r)}lengthAttr(e,t,r=m){return b(this.attr(e,t),r)}}const v=new k;class S{constructor(e,t){this._package=e,this.path=t}async load(){this.rels=await this._package.loadRelationships(this.path);const e=await this._package.load(this.path),t=this._package.parseXmlDocument(e);this._package.options.keepOrigin&&(this._xmlDocument=t),this.parseXml(t.firstElementChild)}save(){var e;this._package.update(this.path,(e=this._xmlDocument,(new XMLSerializer).serializeToString(e)))}parseXml(e){}}const P={embedRegular:"regular",embedBold:"bold",embedItalic:"italic",embedBoldItalic:"boldItalic"};function w(e,t){return t.elements(e).map(e=>function(e,t){let r={name:t.attr(e,"name"),embedFontRefs:[]};for(let a of t.elements(e))switch(a.localName){case"family":r.family=t.attr(a,"val");break;case"altName":r.altName=t.attr(a,"val");break;case"embedRegular":case"embedBold":case"embedItalic":case"embedBoldItalic":r.embedFontRefs.push(C(a,t))}return r}(e,t))}function C(e,t){return{id:t.attr(e,"id"),key:t.attr(e,"fontKey"),type:P[e.localName]}}class x extends S{parseXml(e){this.fonts=w(e,this._package.xmlParser)}}class N{constructor(e,t){this._zip=e,this.options=t,this.xmlParser=new k}get(e){const t=function(e){return e.startsWith("/")?e.substr(1):e}(e);return this._zip.files[t]??this._zip.files[t.replace(/\//g,"\\")]}update(e,t){this._zip.file(e,t)}static async load(e,r){const a=await t.loadAsync(e);return new N(a,r)}save(e="blob"){return this._zip.generateAsync({type:e})}load(e,t="string"){return this.get(e)?.async(t)??Promise.resolve(null)}async loadRelationships(e=null){let t="_rels/.rels";if(null!=e){const[r,a]=s(e);t=`${r}_rels/${a}.rels`}const r=await this.load(t);return r?(a=this.parseXmlDocument(r).firstElementChild,(n=this.xmlParser).elements(a).map(e=>({id:n.attr(e,"Id"),type:n.attr(e,"Type"),target:n.attr(e,"Target"),targetMode:n.attr(e,"TargetMode")}))):null;var a,n}parseXmlDocument(e){return function(e,t=!1){var r;t&&(e=e.replace(/<[?].*[?]>/,"")),e=65279===(r=e).charCodeAt(0)?r.substring(1):r;const a=(new DOMParser).parseFromString(e,"application/xml"),s=(n=a,n.getElementsByTagName("parsererror")[0]?.textContent);var n;if(s)throw new Error(s);return a}(e,this.options.trimXmlDeclaration)}}class M extends S{constructor(e,t,r){super(e,t),this._documentParser=r}parseXml(e){this.body=this._documentParser.parseDocumentFile(e)}}function A(e,t){return{type:t.attr(e,"val"),color:t.attr(e,"color"),size:t.lengthAttr(e,"sz",d),offset:t.lengthAttr(e,"space",f),frame:t.boolAttr(e,"frame"),shadow:t.boolAttr(e,"shadow")}}function E(e,t){var r={};for(let a of t.elements(e))switch(a.localName){case"left":r.left=A(a,t);break;case"top":r.top=A(a,t);break;case"right":r.right=A(a,t);break;case"bottom":r.bottom=A(a,t)}return r}var T,R;function B(e,t=v){var r={};for(let a of t.elements(e))switch(a.localName){case"pgSz":r.pageSize={width:t.lengthAttr(a,"w"),height:t.lengthAttr(a,"h"),orientation:t.attr(a,"orient")};break;case"type":r.type=t.attr(a,"val");break;case"pgMar":r.pageMargins={left:t.lengthAttr(a,"left"),right:t.lengthAttr(a,"right"),top:t.lengthAttr(a,"top"),bottom:t.lengthAttr(a,"bottom"),header:t.lengthAttr(a,"header"),footer:t.lengthAttr(a,"footer"),gutter:t.lengthAttr(a,"gutter")};break;case"cols":r.columns=D(a,t);break;case"headerReference":(r.headerRefs??(r.headerRefs=[])).push(F(a,t));break;case"footerReference":(r.footerRefs??(r.footerRefs=[])).push(F(a,t));break;case"titlePg":r.titlePage=t.boolAttr(a,"val",!0);break;case"pgBorders":r.pageBorders=E(a,t);break;case"pgNumType":r.pageNumber=$(a,t)}return r}function D(e,t){return{numberOfColumns:t.intAttr(e,"num"),space:t.lengthAttr(e,"space"),separator:t.boolAttr(e,"sep"),equalWidth:t.boolAttr(e,"equalWidth",!0),columns:t.elements(e,"col").map(e=>({width:t.lengthAttr(e,"w"),space:t.lengthAttr(e,"space")}))}}function $(e,t){return{chapSep:t.attr(e,"chapSep"),chapStyle:t.attr(e,"chapStyle"),format:t.attr(e,"fmt"),start:t.intAttr(e,"start")}}function F(e,t){return{id:t.attr(e,"id"),type:t.attr(e,"type")}}function L(e,t){let r={};for(let a of t.elements(e))I(a,r,t);return r}function I(e,t,r){return!!y(e,t,r)}function O(e,t){let r={};for(let a of t.elements(e))H(a,r,t);return r}function H(e,t,r){if(e.namespaceURI!=h)return!1;if(y(e,t,r))return!0;switch(e.localName){case"tabs":t.tabs=function(e,t){return t.elements(e,"tab").map(e=>({position:t.lengthAttr(e,"pos"),leader:t.attr(e,"leader"),style:t.attr(e,"val")}))}(e,r);break;case"sectPr":t.sectionProps=B(e,r);break;case"numPr":t.numbering=function(e,t){var r={};for(let a of t.elements(e))switch(a.localName){case"numId":r.id=t.attr(a,"val");break;case"ilvl":r.level=t.intAttr(a,"val")}return r}(e,r);break;case"spacing":return t.lineSpacing=function(e,t){return{before:t.lengthAttr(e,"before"),after:t.lengthAttr(e,"after"),line:t.intAttr(e,"line"),lineRule:t.attr(e,"lineRule")}}(e,r),!1;case"textAlignment":return t.textAlignment=r.attr(e,"val"),!1;case"keepLines":t.keepLines=r.boolAttr(e,"val",!0);break;case"keepNext":t.keepNext=r.boolAttr(e,"val",!0);break;case"pageBreakBefore":t.pageBreakBefore=r.boolAttr(e,"val",!0);break;case"outlineLvl":t.outlineLevel=r.intAttr(e,"val");break;case"pStyle":t.styleName=r.attr(e,"val");break;case"rPr":t.runProps=L(e,r);break;default:return!1}return!0}function _(e,t){let r={id:t.attr(e,"numId"),overrides:[]};for(let a of t.elements(e))switch(a.localName){case"abstractNumId":r.abstractId=t.attr(a,"val");break;case"lvlOverride":r.overrides.push(j(a,t))}return r}function z(e,t){let r={id:t.attr(e,"abstractNumId"),levels:[]};for(let a of t.elements(e))switch(a.localName){case"name":r.name=t.attr(a,"val");break;case"multiLevelType":r.multiLevelType=t.attr(a,"val");break;case"numStyleLink":r.numberingStyleLink=t.attr(a,"val");break;case"styleLink":r.styleLink=t.attr(a,"val");break;case"lvl":r.levels.push(V(a,t))}return r}function V(e,t){let r={level:t.intAttr(e,"ilvl")};for(let a of t.elements(e))switch(a.localName){case"start":r.start=t.attr(a,"val");break;case"lvlRestart":r.restart=t.intAttr(a,"val");break;case"numFmt":r.format=t.attr(a,"val");break;case"lvlText":r.text=t.attr(a,"val");break;case"lvlJc":r.justification=t.attr(a,"val");break;case"lvlPicBulletId":r.bulletPictureId=t.attr(a,"val");break;case"pStyle":r.paragraphStyle=t.attr(a,"val");break;case"pPr":r.paragraphProps=O(a,t);break;case"rPr":r.runProps=L(a,t)}return r}function j(e,t){let r={level:t.intAttr(e,"ilvl")};for(let a of t.elements(e))switch(a.localName){case"startOverride":r.start=t.intAttr(a,"val");break;case"lvl":r.numberingLevel=V(a,t)}return r}function W(e,t){var r=t.element(e,"pict"),a=r&&t.element(r,"shape"),s=a&&t.element(a,"imagedata");return s?{id:t.attr(e,"numPicBulletId"),referenceId:t.attr(s,"id"),style:t.attr(a,"style")}:null}!function(e){e.Continuous="continuous",e.NextPage="nextPage",e.NextColumn="nextColumn",e.EvenPage="evenPage",e.OddPage="oddPage"}(T||(T={}));class X extends S{constructor(e,t,r){super(e,t),this._documentParser=r}parseXml(e){Object.assign(this,function(e,t){let r={numberings:[],abstractNumberings:[],bulletPictures:[]};for(let a of t.elements(e))switch(a.localName){case"num":r.numberings.push(_(a,t));break;case"abstractNum":r.abstractNumberings.push(z(a,t));break;case"numPicBullet":r.bulletPictures.push(W(a,t))}return r}(e,this._package.xmlParser)),this.domNumberings=this._documentParser.parseNumberingFile(e)}}class G extends S{constructor(e,t,r){super(e,t),this._documentParser=r}parseXml(e){this.styles=this._documentParser.parseStylesFile(e)}}!function(e){e.Document="document",e.Paragraph="paragraph",e.Run="run",e.Break="break",e.NoBreakHyphen="noBreakHyphen",e.Table="table",e.Row="row",e.Cell="cell",e.Hyperlink="hyperlink",e.SmartTag="smartTag",e.Drawing="drawing",e.Image="image",e.Text="text",e.Tab="tab",e.Symbol="symbol",e.BookmarkStart="bookmarkStart",e.BookmarkEnd="bookmarkEnd",e.Footer="footer",e.Header="header",e.FootnoteReference="footnoteReference",e.EndnoteReference="endnoteReference",e.Footnote="footnote",e.Endnote="endnote",e.SimpleField="simpleField",e.ComplexField="complexField",e.Instruction="instruction",e.VmlPicture="vmlPicture",e.MmlMath="mmlMath",e.MmlMathParagraph="mmlMathParagraph",e.MmlFraction="mmlFraction",e.MmlFunction="mmlFunction",e.MmlFunctionName="mmlFunctionName",e.MmlNumerator="mmlNumerator",e.MmlDenominator="mmlDenominator",e.MmlRadical="mmlRadical",e.MmlBase="mmlBase",e.MmlDegree="mmlDegree",e.MmlSuperscript="mmlSuperscript",e.MmlSubscript="mmlSubscript",e.MmlPreSubSuper="mmlPreSubSuper",e.MmlSubArgument="mmlSubArgument",e.MmlSuperArgument="mmlSuperArgument",e.MmlNary="mmlNary",e.MmlDelimiter="mmlDelimiter",e.MmlRun="mmlRun",e.MmlEquationArray="mmlEquationArray",e.MmlLimit="mmlLimit",e.MmlLimitLower="mmlLimitLower",e.MmlMatrix="mmlMatrix",e.MmlMatrixRow="mmlMatrixRow",e.MmlBox="mmlBox",e.MmlBar="mmlBar",e.MmlGroupChar="mmlGroupChar",e.VmlElement="vmlElement",e.Inserted="inserted",e.Deleted="deleted",e.DeletedText="deletedText",e.Comment="comment",e.CommentReference="commentReference",e.CommentRangeStart="commentRangeStart",e.CommentRangeEnd="commentRangeEnd",e.AltChunk="altChunk"}(R||(R={}));class U{constructor(){this.children=[],this.cssStyle={}}}class q extends U{constructor(){super(...arguments),this.type=R.Header}}class J extends U{constructor(){super(...arguments),this.type=R.Footer}}class Z extends S{constructor(e,t,r){super(e,t),this._documentParser=r}parseXml(e){this.rootElement=this.createRootElement(),this.rootElement.children=this._documentParser.parseBodyElements(e)}}class K extends Z{createRootElement(){return new q}}class Y extends Z{createRootElement(){return new J}}function Q(e){if(void 0!==e)return parseInt(e)}class ee extends S{parseXml(e){this.props=function(e,t){const r={};for(let a of t.elements(e))switch(a.localName){case"Template":r.template=a.textContent;break;case"Pages":r.pages=Q(a.textContent);break;case"Words":r.words=Q(a.textContent);break;case"Characters":r.characters=Q(a.textContent);break;case"Application":r.application=a.textContent;break;case"Lines":r.lines=Q(a.textContent);break;case"Paragraphs":r.paragraphs=Q(a.textContent);break;case"Company":r.company=a.textContent;break;case"AppVersion":r.appVersion=a.textContent}return r}(e,this._package.xmlParser)}}class te extends S{parseXml(e){this.props=function(e,t){const r={};for(let a of t.elements(e))switch(a.localName){case"title":r.title=a.textContent;break;case"description":r.description=a.textContent;break;case"subject":r.subject=a.textContent;break;case"creator":r.creator=a.textContent;break;case"keywords":r.keywords=a.textContent;break;case"language":r.language=a.textContent;break;case"lastModifiedBy":r.lastModifiedBy=a.textContent;break;case"revision":a.textContent&&(r.revision=parseInt(a.textContent))}return r}(e,this._package.xmlParser)}}class re{}function ae(e,t){var r={name:t.attr(e,"name"),colors:{}};for(let n of t.elements(e)){var a=t.element(n,"srgbClr"),s=t.element(n,"sysClr");a?r.colors[n.localName]=t.attr(a,"val"):s&&(r.colors[n.localName]=t.attr(s,"lastClr"))}return r}function se(e,t){var r={name:t.attr(e,"name")};for(let a of t.elements(e))switch(a.localName){case"majorFont":r.majorFont=ne(a,t);break;case"minorFont":r.minorFont=ne(a,t)}return r}function ne(e,t){return{latinTypeface:t.elementAttr(e,"latin","typeface"),eaTypeface:t.elementAttr(e,"ea","typeface"),csTypeface:t.elementAttr(e,"cs","typeface")}}class le extends S{constructor(e,t){super(e,t)}parseXml(e){this.theme=function(e,t){var r=new re,a=t.element(e,"themeElements");for(let e of t.elements(a))switch(e.localName){case"clrScheme":r.colorScheme=ae(e,t);break;case"fontScheme":r.fontScheme=se(e,t)}return r}(e,this._package.xmlParser)}}class oe{}class ie extends oe{constructor(){super(...arguments),this.type=R.Footnote}}class ce extends oe{constructor(){super(...arguments),this.type=R.Endnote}}class he extends S{constructor(e,t,r){super(e,t),this._documentParser=r}}class me extends he{constructor(e,t,r){super(e,t,r)}parseXml(e){this.notes=this._documentParser.parseNotes(e,"footnote",ie)}}class pe extends he{constructor(e,t,r){super(e,t,r)}parseXml(e){this.notes=this._documentParser.parseNotes(e,"endnote",ce)}}function ue(e,t){var r={defaultNoteIds:[]};for(let a of t.elements(e))switch(a.localName){case"numFmt":r.nummeringFormat=t.attr(a,"val");break;case"footnote":case"endnote":r.defaultNoteIds.push(t.attr(a,"id"))}return r}class de extends S{constructor(e,t){super(e,t)}parseXml(e){this.settings=function(e,t){var r={};for(let a of t.elements(e))switch(a.localName){case"defaultTabStop":r.defaultTabStop=t.lengthAttr(a,"val");break;case"footnotePr":r.footnoteProps=ue(a,t);break;case"endnotePr":r.endnoteProps=ue(a,t);break;case"autoHyphenation":r.autoHyphenation=t.boolAttr(a,"val")}return r}(e,this._package.xmlParser)}}class fe extends S{parseXml(e){this.props=function(e,t){return t.elements(e,"property").map(e=>{const r=e.firstChild;return{formatId:t.attr(e,"fmtid"),name:t.attr(e,"name"),type:r.nodeName,value:r.textContent}})}(e,this._package.xmlParser)}}class ge extends S{constructor(e,t,r){super(e,t),this._documentParser=r}parseXml(e){this.comments=this._documentParser.parseComments(e),this.commentMap=l(this.comments,e=>e.id)}}class be extends S{constructor(e,t){super(e,t),this.comments=[]}parseXml(e){const t=this._package.xmlParser;for(let r of t.elements(e,"commentEx"))this.comments.push({paraId:t.attr(r,"paraId"),paraIdParent:t.attr(r,"paraIdParent"),done:t.boolAttr(r,"done")});this.commentMap=l(this.comments,e=>e.paraId)}}const ye=[{type:r.OfficeDocument,target:"word/document.xml"},{type:r.ExtendedProperties,target:"docProps/app.xml"},{type:r.CoreProperties,target:"docProps/core.xml"},{type:r.CustomProperties,target:"docProps/custom.xml"}];class ke{constructor(){this.parts=[],this.partsMap={}}static async load(e,t,r){var a=new ke;return a._options=r,a._parser=t,a._package=await N.load(e,r),a.rels=await a._package.loadRelationships(),await Promise.all(ye.map(e=>{const t=a.rels.find(t=>t.type===e.type)??e;return a.loadRelationshipPart(t.target,t.type)})),a}save(e="blob"){return this._package.save(e)}async loadRelationshipPart(e,t){if(this.partsMap[e])return this.partsMap[e];if(!this._package.get(e))return null;let a=null;switch(t){case r.OfficeDocument:this.documentPart=a=new M(this._package,e,this._parser);break;case r.FontTable:this.fontTablePart=a=new x(this._package,e);break;case r.Numbering:this.numberingPart=a=new X(this._package,e,this._parser);break;case r.Styles:this.stylesPart=a=new G(this._package,e,this._parser);break;case r.Theme:this.themePart=a=new le(this._package,e);break;case r.Footnotes:this.footnotesPart=a=new me(this._package,e,this._parser);break;case r.Endnotes:this.endnotesPart=a=new pe(this._package,e,this._parser);break;case r.Footer:a=new Y(this._package,e,this._parser);break;case r.Header:a=new K(this._package,e,this._parser);break;case r.CoreProperties:this.corePropsPart=a=new te(this._package,e);break;case r.ExtendedProperties:this.extendedPropsPart=a=new ee(this._package,e);break;case r.CustomProperties:a=new fe(this._package,e);break;case r.Settings:this.settingsPart=a=new de(this._package,e);break;case r.Comments:this.commentsPart=a=new ge(this._package,e,this._parser);break;case r.CommentsExtended:this.commentsExtendedPart=a=new be(this._package,e)}if(null==a)return Promise.resolve(null);if(this.partsMap[e]=a,this.parts.push(a),await a.load(),a.rels?.length>0){const[e]=s(a.path);await Promise.all(a.rels.map(t=>this.loadRelationshipPart(n(t.target,e),t.type)))}return a}async loadDocumentImage(e,t){const r=await this.loadResource(t??this.documentPart,e,"blob");return this.blobToURL(r)}async loadNumberingImage(e){const t=await this.loadResource(this.numberingPart,e,"blob");return this.blobToURL(t)}async loadFont(e,t){const r=await this.loadResource(this.fontTablePart,e,"uint8array");return r?this.blobToURL(new Blob([ve(r,t)])):r}async loadAltChunk(e,t){return await this.loadResource(t??this.documentPart,e,"string")}blobToURL(e){return e?this._options.useBase64URL?function(e){return new Promise((t,r)=>{const a=new FileReader;a.onloadend=()=>t(a.result),a.onerror=()=>r(),a.readAsDataURL(e)})}(e):URL.createObjectURL(e):null}findPartByRelId(e,t=null){var r=(t.rels??this.rels).find(t=>t.id==e);const a=t?s(t.path)[0]:"";return r?this.partsMap[n(r.target,a)]:null}getPathById(e,t){const r=e.rels.find(e=>e.id==t),[a]=s(e.path);return r?n(r.target,a):null}loadResource(e,t,r){const a=this.getPathById(e,t);return a?this._package.load(a,r):Promise.resolve(null)}}function ve(e,t){const r=t.replace(/{|}|-/g,""),a=new Array(16);for(let e=0;e<16;e++)a[16-e-1]=parseInt(r.substring(2*e,2*e+2),16);for(let t=0;t<32;t++)e[t]=e[t]^a[t%16];return e}function Se(e,t){return{type:R.BookmarkStart,id:t.attr(e,"id"),name:t.attr(e,"name"),colFirst:t.intAttr(e,"colFirst"),colLast:t.intAttr(e,"colLast")}}function Pe(e,t){return{type:R.BookmarkEnd,id:t.attr(e,"id")}}class we extends U{constructor(){super(...arguments),this.type=R.VmlElement,this.attrs={}}}function Ce(e,t){var r=new we;switch(e.localName){case"rect":r.tagName="rect",Object.assign(r.attrs,{width:"100%",height:"100%"});break;case"oval":r.tagName="ellipse",Object.assign(r.attrs,{cx:"50%",cy:"50%",rx:"50%",ry:"50%"});break;case"line":r.tagName="line";break;case"shape":r.tagName="g";break;case"textbox":r.tagName="foreignObject",Object.assign(r.attrs,{width:"100%",height:"100%"});break;default:return null}for(const t of v.attrs(e))switch(t.localName){case"style":r.cssStyleText=t.value;break;case"fillcolor":r.attrs.fill=t.value;break;case"from":const[e,a]=Me(t.value);Object.assign(r.attrs,{x1:e,y1:a});break;case"to":const[s,n]=Me(t.value);Object.assign(r.attrs,{x2:s,y2:n})}for(const a of v.elements(e))switch(a.localName){case"stroke":Object.assign(r.attrs,xe(a));break;case"fill":Object.assign(r.attrs,Ne());break;case"imagedata":r.tagName="image",Object.assign(r.attrs,{width:"100%",height:"100%"}),r.imageHref={id:v.attr(a,"id"),title:v.attr(a,"title")};break;case"txbxContent":r.children.push(...t.parseBodyElements(a));break;default:const e=Ce(a,t);e&&r.children.push(e)}return r}function xe(e){return{stroke:v.attr(e,"color"),"stroke-width":v.lengthAttr(e,"weight",p)??"1px"}}function Ne(e){return{}}function Me(e){return e.split(",")}class Ae extends U{constructor(){super(...arguments),this.type=R.Comment}}class Ee extends U{constructor(e){super(),this.id=e,this.type=R.CommentReference}}class Te extends U{constructor(e){super(),this.id=e,this.type=R.CommentRangeStart}}class Re extends U{constructor(e){super(),this.id=e,this.type=R.CommentRangeEnd}}var Be="inherit",De="black",$e="black",Fe="transparent";const Le=[],Ie={oMath:R.MmlMath,oMathPara:R.MmlMathParagraph,f:R.MmlFraction,func:R.MmlFunction,fName:R.MmlFunctionName,num:R.MmlNumerator,den:R.MmlDenominator,rad:R.MmlRadical,deg:R.MmlDegree,e:R.MmlBase,sSup:R.MmlSuperscript,sSub:R.MmlSubscript,sPre:R.MmlPreSubSuper,sup:R.MmlSuperArgument,sub:R.MmlSubArgument,d:R.MmlDelimiter,nary:R.MmlNary,eqArr:R.MmlEquationArray,lim:R.MmlLimit,limLow:R.MmlLimitLower,m:R.MmlMatrix,mr:R.MmlMatrixRow,box:R.MmlBox,bar:R.MmlBar,groupChr:R.MmlGroupChar};class Oe{constructor(e){this.options={ignoreWidth:!1,debug:!1,...e}}parseNotes(e,t,r){var a=[];for(let s of v.elements(e,t)){const e=new r;e.id=v.attr(s,"id"),e.noteType=v.attr(s,"type"),e.children=this.parseBodyElements(s),a.push(e)}return a}parseComments(e){var t=[];for(let r of v.elements(e,"comment")){const e=new Ae;e.id=v.attr(r,"id"),e.author=v.attr(r,"author"),e.initials=v.attr(r,"initials"),e.date=v.attr(r,"date"),e.children=this.parseBodyElements(r),t.push(e)}return t}parseDocumentFile(e){var t=v.element(e,"body"),r=v.element(e,"background"),a=v.element(t,"sectPr");return{type:R.Document,children:this.parseBodyElements(t),props:a?B(a,v):{},cssStyle:r?this.parseBackground(r):{}}}parseBackground(e){var t={},r=_e.colorAttr(e,"color");return r&&(t["background-color"]=r),t}parseBodyElements(e){var t=[];for(const r of v.elements(e))switch(r.localName){case"p":t.push(this.parseParagraph(r));break;case"altChunk":t.push(this.parseAltChunk(r));break;case"tbl":t.push(this.parseTable(r));break;case"sdt":t.push(...this.parseSdt(r,e=>this.parseBodyElements(e)))}return t}parseStylesFile(e){var t=[];for(const r of v.elements(e))switch(r.localName){case"style":t.push(this.parseStyle(r));break;case"docDefaults":t.push(this.parseDefaultStyles(r))}return t}parseDefaultStyles(e){var t={id:null,name:null,target:null,basedOn:null,styles:[]};for(const s of v.elements(e))switch(s.localName){case"rPrDefault":var r=v.element(s,"rPr");r&&t.styles.push({target:"span",values:this.parseDefaultProperties(r,{})});break;case"pPrDefault":var a=v.element(s,"pPr");a&&t.styles.push({target:"p",values:this.parseDefaultProperties(a,{})})}return t}parseStyle(e){var t={id:v.attr(e,"styleId"),isDefault:v.boolAttr(e,"default"),name:null,target:null,basedOn:null,styles:[],linked:null};switch(v.attr(e,"type")){case"paragraph":t.target="p";break;case"table":t.target="table";break;case"character":t.target="span"}for(const r of v.elements(e))switch(r.localName){case"basedOn":t.basedOn=v.attr(r,"val");break;case"name":t.name=v.attr(r,"val");break;case"link":t.linked=v.attr(r,"val");break;case"next":t.next=v.attr(r,"val");break;case"aliases":t.aliases=v.attr(r,"val").split(",");break;case"pPr":t.styles.push({target:"p",values:this.parseDefaultProperties(r,{})}),t.paragraphProps=O(r,v);break;case"rPr":t.styles.push({target:"span",values:this.parseDefaultProperties(r,{})}),t.runProps=L(r,v);break;case"tblPr":case"tcPr":t.styles.push({target:"td",values:this.parseDefaultProperties(r,{})});break;case"tblStylePr":for(let e of this.parseTableStyle(r))t.styles.push(e);break;case"rsid":case"qFormat":case"hidden":case"semiHidden":case"unhideWhenUsed":case"autoRedefine":case"uiPriority":break;default:this.options.debug&&console.warn(`DOCX: Unknown style element: ${r.localName}`)}return t}parseTableStyle(e){var t=[],r="",a="";switch(v.attr(e,"type")){case"firstRow":a=".first-row",r="tr.first-row td";break;case"lastRow":a=".last-row",r="tr.last-row td";break;case"firstCol":a=".first-col",r="td.first-col";break;case"lastCol":a=".last-col",r="td.last-col";break;case"band1Vert":a=":not(.no-vband)",r="td.odd-col";break;case"band2Vert":a=":not(.no-vband)",r="td.even-col";break;case"band1Horz":a=":not(.no-hband)",r="tr.odd-row";break;case"band2Horz":a=":not(.no-hband)",r="tr.even-row";break;default:return[]}for(const s of v.elements(e))switch(s.localName){case"pPr":t.push({target:`${r} p`,mod:a,values:this.parseDefaultProperties(s,{})});break;case"rPr":t.push({target:`${r} span`,mod:a,values:this.parseDefaultProperties(s,{})});break;case"tblPr":case"tcPr":t.push({target:r,mod:a,values:this.parseDefaultProperties(s,{})})}return t}parseNumberingFile(e){var t=[],r={},a=[];for(const l of v.elements(e))switch(l.localName){case"abstractNum":this.parseAbstractNumbering(l,a).forEach(e=>t.push(e));break;case"numPicBullet":a.push(this.parseNumberingPicBullet(l));break;case"num":var s=v.attr(l,"numId"),n=v.elementAttr(l,"abstractNumId","val");r[n]=s}return t.forEach(e=>e.id=r[e.id]),t}parseNumberingPicBullet(e){var t=v.element(e,"pict"),r=t&&v.element(t,"shape"),a=r&&v.element(r,"imagedata");return a?{id:v.intAttr(e,"numPicBulletId"),src:v.attr(a,"id"),style:v.attr(r,"style")}:null}parseAbstractNumbering(e,t){var r=[],a=v.attr(e,"abstractNumId");for(const s of v.elements(e))if("lvl"===s.localName)r.push(this.parseNumberingLevel(a,s,t));return r}parseNumberingLevel(e,t,r){var a={id:e,level:v.intAttr(t,"ilvl"),start:1,pStyleName:void 0,pStyle:{},rStyle:{},suff:"tab"};for(const e of v.elements(t))switch(e.localName){case"start":a.start=v.intAttr(e,"val");break;case"pPr":this.parseDefaultProperties(e,a.pStyle);break;case"rPr":this.parseDefaultProperties(e,a.rStyle);break;case"lvlPicBulletId":var s=v.intAttr(e,"val");a.bullet=r.find(e=>e?.id==s);break;case"lvlText":a.levelText=v.attr(e,"val");break;case"pStyle":a.pStyleName=v.attr(e,"val");break;case"numFmt":a.format=v.attr(e,"val");break;case"suff":a.suff=v.attr(e,"val")}return a}parseSdt(e,t){const r=v.element(e,"sdtContent");return r?t(r):[]}parseInserted(e,t){return{type:R.Inserted,children:t(e)?.children??[]}}parseDeleted(e,t){return{type:R.Deleted,children:t(e)?.children??[]}}parseAltChunk(e){return{type:R.AltChunk,children:[],id:v.attr(e,"id")}}parseParagraph(e){var t={type:R.Paragraph,children:[]};for(let r of v.elements(e))switch(r.localName){case"pPr":this.parseParagraphProperties(r,t);break;case"r":t.children.push(this.parseRun(r,t));break;case"hyperlink":t.children.push(this.parseHyperlink(r,t));break;case"smartTag":t.children.push(this.parseSmartTag(r,t));break;case"bookmarkStart":t.children.push(Se(r,v));break;case"bookmarkEnd":t.children.push(Pe(r,v));break;case"commentRangeStart":t.children.push(new Te(v.attr(r,"id")));break;case"commentRangeEnd":t.children.push(new Re(v.attr(r,"id")));break;case"oMath":case"oMathPara":t.children.push(this.parseMathElement(r));break;case"sdt":t.children.push(...this.parseSdt(r,e=>this.parseParagraph(e).children));break;case"ins":t.children.push(this.parseInserted(r,e=>this.parseParagraph(e)));break;case"del":t.children.push(this.parseDeleted(r,e=>this.parseParagraph(e)))}return t}parseParagraphProperties(e,t){this.parseDefaultProperties(e,t.cssStyle={},null,e=>{if(H(e,t,v))return!0;switch(e.localName){case"pStyle":t.styleName=v.attr(e,"val");break;case"cnfStyle":t.className=ze.classNameOfCnfStyle(e);break;case"framePr":this.parseFrame(e,t);break;case"rPr":break;default:return!1}return!0})}parseFrame(e,t){"drop"==v.attr(e,"dropCap")&&(t.cssStyle.float="left")}parseHyperlink(e,t){var r={type:R.Hyperlink,parent:t,children:[]};r.anchor=v.attr(e,"anchor"),r.id=v.attr(e,"id");for(const t of v.elements(e))if("r"===t.localName)r.children.push(this.parseRun(t,r));return r}parseSmartTag(e,t){var r={type:R.SmartTag,parent:t,children:[]},a=v.attr(e,"uri"),s=v.attr(e,"element");a&&(r.uri=a),s&&(r.element=s);for(const t of v.elements(e))if("r"===t.localName)r.children.push(this.parseRun(t,r));return r}parseRun(e,t){var r={type:R.Run,parent:t,children:[]};for(let t of v.elements(e))switch(t=this.checkAlternateContent(t),t.localName){case"t":r.children.push({type:R.Text,text:t.textContent});break;case"delText":r.children.push({type:R.DeletedText,text:t.textContent});break;case"commentReference":r.children.push(new Ee(v.attr(t,"id")));break;case"fldSimple":r.children.push({type:R.SimpleField,instruction:v.attr(t,"instr"),lock:v.boolAttr(t,"lock",!1),dirty:v.boolAttr(t,"dirty",!1)});break;case"instrText":r.fieldRun=!0,r.children.push({type:R.Instruction,text:t.textContent});break;case"fldChar":r.fieldRun=!0,r.children.push({type:R.ComplexField,charType:v.attr(t,"fldCharType"),lock:v.boolAttr(t,"lock",!1),dirty:v.boolAttr(t,"dirty",!1)});break;case"noBreakHyphen":r.children.push({type:R.NoBreakHyphen});break;case"br":r.children.push({type:R.Break,break:v.attr(t,"type")||"textWrapping"});break;case"lastRenderedPageBreak":r.children.push({type:R.Break,break:"lastRenderedPageBreak"});break;case"sym":r.children.push({type:R.Symbol,font:a(v.attr(t,"font")),char:v.attr(t,"char")});break;case"tab":r.children.push({type:R.Tab});break;case"footnoteReference":r.children.push({type:R.FootnoteReference,id:v.attr(t,"id")});break;case"endnoteReference":r.children.push({type:R.EndnoteReference,id:v.attr(t,"id")});break;case"drawing":let e=this.parseDrawing(t);e&&(r.children=[e]);break;case"pict":r.children.push(this.parseVmlPicture(t));break;case"rPr":this.parseRunProperties(t,r)}return r}parseMathElement(e){const t=`${e.localName}Pr`,r={type:Ie[e.localName],children:[]};for(const s of v.elements(e)){if(Ie[s.localName])r.children.push(this.parseMathElement(s));else if("r"==s.localName){var a=this.parseRun(s);a.type=R.MmlRun,r.children.push(a)}else s.localName==t&&(r.props=this.parseMathProperies(s))}return r}parseMathProperies(e){const t={};for(const r of v.elements(e))switch(r.localName){case"chr":t.char=v.attr(r,"val");break;case"vertJc":t.verticalJustification=v.attr(r,"val");break;case"pos":t.position=v.attr(r,"val");break;case"degHide":t.hideDegree=v.boolAttr(r,"val");break;case"begChr":t.beginChar=v.attr(r,"val");break;case"endChr":t.endChar=v.attr(r,"val")}return t}parseRunProperties(e,t){this.parseDefaultProperties(e,t.cssStyle={},null,e=>{switch(e.localName){case"rStyle":t.styleName=v.attr(e,"val");break;case"vertAlign":t.verticalAlign=ze.valueOfVertAlign(e,!0);break;default:return!1}return!0})}parseVmlPicture(e){const t={type:R.VmlPicture,children:[]};for(const r of v.elements(e)){const e=Ce(r,this);e&&t.children.push(e)}return t}checkAlternateContent(e){if("AlternateContent"!=e.localName)return e;var t=v.element(e,"Choice");if(t){var r=v.attr(t,"Requires"),a=e.lookupNamespaceURI(r);if(Le.includes(a))return t.firstElementChild}return v.element(e,"Fallback")?.firstElementChild}parseDrawing(e){for(var t of v.elements(e))switch(t.localName){case"inline":case"anchor":return this.parseDrawingWrapper(t)}}parseDrawingWrapper(e){var t={type:R.Drawing,children:[],cssStyle:{}},r="anchor"==e.localName;let a=null,s=v.boolAttr(e,"simplePos");v.boolAttr(e,"behindDoc");let n={relative:"page",align:"left",offset:"0"},l={relative:"page",align:"top",offset:"0"};for(var o of v.elements(e))switch(o.localName){case"simplePos":s&&(n.offset=v.lengthAttr(o,"x",p),l.offset=v.lengthAttr(o,"y",p));break;case"extent":t.cssStyle.width=v.lengthAttr(o,"cx",p),t.cssStyle.height=v.lengthAttr(o,"cy",p);break;case"positionH":case"positionV":if(!s){let e="positionH"==o.localName?n:l;var i=v.element(o,"align"),c=v.element(o,"posOffset");e.relative=v.attr(o,"relativeFrom")??e.relative,i&&(e.align=i.textContent),c&&(e.offset=b(c.textContent,p))}break;case"wrapTopAndBottom":a="wrapTopAndBottom";break;case"wrapNone":a="wrapNone";break;case"graphic":var h=this.parseGraphic(o);h&&t.children.push(h)}return"wrapTopAndBottom"==a?(t.cssStyle.display="block",n.align&&(t.cssStyle["text-align"]=n.align,t.cssStyle.width="100%")):"wrapNone"==a?(t.cssStyle.display="block",t.cssStyle.position="relative",t.cssStyle.width="0px",t.cssStyle.height="0px",n.offset&&(t.cssStyle.left=n.offset),l.offset&&(t.cssStyle.top=l.offset)):!r||"left"!=n.align&&"right"!=n.align||(t.cssStyle.float=n.align),t}parseGraphic(e){var t=v.element(e,"graphicData");for(let e of v.elements(t))if("pic"===e.localName)return this.parsePicture(e);return null}parsePicture(e){var t={type:R.Image,src:"",cssStyle:{}},r=v.element(e,"blipFill"),a=v.element(r,"blip"),s=v.element(r,"srcRect");t.src=v.attr(a,"embed"),s&&(t.srcRect=[v.intAttr(s,"l",0)/1e5,v.intAttr(s,"t",0)/1e5,v.intAttr(s,"r",0)/1e5,v.intAttr(s,"b",0)/1e5]);var n=v.element(e,"spPr"),l=v.element(n,"xfrm");if(t.cssStyle.position="relative",l)for(var o of(t.rotation=v.intAttr(l,"rot",0)/6e4,v.elements(l)))switch(o.localName){case"ext":t.cssStyle.width=v.lengthAttr(o,"cx",p),t.cssStyle.height=v.lengthAttr(o,"cy",p);break;case"off":t.cssStyle.left=v.lengthAttr(o,"x",p),t.cssStyle.top=v.lengthAttr(o,"y",p)}return t}parseTable(e){var t={type:R.Table,children:[]};for(const r of v.elements(e))switch(r.localName){case"tr":t.children.push(this.parseTableRow(r));break;case"tblGrid":t.columns=this.parseTableColumns(r);break;case"tblPr":this.parseTableProperties(r,t)}return t}parseTableColumns(e){var t=[];for(const r of v.elements(e))if("gridCol"===r.localName)t.push({width:v.lengthAttr(r,"w")});return t}parseTableProperties(e,t){switch(t.cssStyle={},t.cellStyle={},this.parseDefaultProperties(e,t.cssStyle,t.cellStyle,e=>{switch(e.localName){case"tblStyle":t.styleName=v.attr(e,"val");break;case"tblLook":t.className=ze.classNameOftblLook(e);break;case"tblpPr":this.parseTablePosition(e,t);break;case"tblStyleColBandSize":t.colBandSize=v.intAttr(e,"val");break;case"tblStyleRowBandSize":t.rowBandSize=v.intAttr(e,"val");break;case"hidden":t.cssStyle.display="none";break;default:return!1}return!0}),t.cssStyle["text-align"]){case"center":delete t.cssStyle["text-align"],t.cssStyle["margin-left"]="auto",t.cssStyle["margin-right"]="auto";break;case"right":delete t.cssStyle["text-align"],t.cssStyle["margin-left"]="auto"}}parseTablePosition(e,t){var r=v.lengthAttr(e,"topFromText"),a=v.lengthAttr(e,"bottomFromText"),s=v.lengthAttr(e,"rightFromText"),n=v.lengthAttr(e,"leftFromText");t.cssStyle.float="left",t.cssStyle["margin-bottom"]=ze.addSize(t.cssStyle["margin-bottom"],a),t.cssStyle["margin-left"]=ze.addSize(t.cssStyle["margin-left"],n),t.cssStyle["margin-right"]=ze.addSize(t.cssStyle["margin-right"],s),t.cssStyle["margin-top"]=ze.addSize(t.cssStyle["margin-top"],r)}parseTableRow(e){var t={type:R.Row,children:[]};for(const r of v.elements(e))switch(r.localName){case"tc":t.children.push(this.parseTableCell(r));break;case"trPr":case"tblPrEx":this.parseTableRowProperties(r,t)}return t}parseTableRowProperties(e,t){t.cssStyle=this.parseDefaultProperties(e,{},null,e=>{switch(e.localName){case"cnfStyle":t.className=ze.classNameOfCnfStyle(e);break;case"tblHeader":t.isHeader=v.boolAttr(e,"val");break;case"gridBefore":t.gridBefore=v.intAttr(e,"val");break;case"gridAfter":t.gridAfter=v.intAttr(e,"val");break;default:return!1}return!0})}parseTableCell(e){var t={type:R.Cell,children:[]};for(const r of v.elements(e))switch(r.localName){case"tbl":t.children.push(this.parseTable(r));break;case"p":t.children.push(this.parseParagraph(r));break;case"tcPr":this.parseTableCellProperties(r,t)}return t}parseTableCellProperties(e,t){t.cssStyle=this.parseDefaultProperties(e,{},null,e=>{switch(e.localName){case"gridSpan":t.span=v.intAttr(e,"val",null);break;case"vMerge":t.verticalMerge=v.attr(e,"val")??"continue";break;case"cnfStyle":t.className=ze.classNameOfCnfStyle(e);break;default:return!1}return!0}),this.parseTableCellVerticalText(e,t)}parseTableCellVerticalText(e,t){const r={btLr:{writingMode:"vertical-rl",transform:"rotate(180deg)"},lrTb:{writingMode:"vertical-lr",transform:"none"},tbRl:{writingMode:"vertical-rl",transform:"none"}};for(const a of v.elements(e))if("textDirection"===a.localName){const e=r[v.attr(a,"val")]||{writingMode:"horizontal-tb"};t.cssStyle["writing-mode"]=e.writingMode,t.cssStyle.transform=e.transform}}parseDefaultProperties(e,t=null,r=null,a=null){t=t||{};for(const s of v.elements(e))if(!a?.(s))switch(s.localName){case"jc":t["text-align"]=ze.valueOfJc(s);break;case"textAlignment":t["vertical-align"]=ze.valueOfTextAlignment(s);break;case"color":t.color=_e.colorAttr(s,"val",null,De);break;case"sz":t["font-size"]=t["min-height"]=v.lengthAttr(s,"val",u);break;case"shd":t["background-color"]=_e.colorAttr(s,"fill",null,Be);break;case"highlight":t["background-color"]=_e.colorAttr(s,"val",null,Fe);break;case"vertAlign":break;case"position":t.verticalAlign=v.lengthAttr(s,"val",u);break;case"tcW":if(this.options.ignoreWidth)break;case"tblW":t.width=ze.valueOfSize(s,"w");break;case"trHeight":this.parseTrHeight(s,t);break;case"strike":t["text-decoration"]=v.boolAttr(s,"val",!0)?"line-through":"none";break;case"b":t["font-weight"]=v.boolAttr(s,"val",!0)?"bold":"normal";break;case"i":t["font-style"]=v.boolAttr(s,"val",!0)?"italic":"normal";break;case"caps":t["text-transform"]=v.boolAttr(s,"val",!0)?"uppercase":"none";break;case"smallCaps":t["font-variant"]=v.boolAttr(s,"val",!0)?"small-caps":"none";break;case"u":this.parseUnderline(s,t);break;case"ind":case"tblInd":this.parseIndentation(s,t);break;case"rFonts":this.parseFont(s,t);break;case"tblBorders":this.parseBorderProperties(s,r||t);break;case"tblCellSpacing":t["border-spacing"]=ze.valueOfMargin(s),t["border-collapse"]="separate";break;case"pBdr":this.parseBorderProperties(s,t);break;case"bdr":t.border=ze.valueOfBorder(s);break;case"tcBorders":this.parseBorderProperties(s,t);break;case"vanish":v.boolAttr(s,"val",!0)&&(t.display="none");break;case"kern":case"noWrap":break;case"tblCellMar":case"tcMar":this.parseMarginProperties(s,r||t);break;case"tblLayout":t["table-layout"]=ze.valueOfTblLayout(s);break;case"vAlign":t["vertical-align"]=ze.valueOfTextAlignment(s);break;case"spacing":"pPr"==e.localName&&this.parseSpacing(s,t);break;case"wordWrap":v.boolAttr(s,"val")&&(t["overflow-wrap"]="break-word");break;case"suppressAutoHyphens":t.hyphens=v.boolAttr(s,"val",!0)?"none":"auto";break;case"lang":t.$lang=v.attr(s,"val");break;case"rtl":case"bidi":v.boolAttr(s,"val",!0)&&(t.direction="rtl");break;case"bCs":case"iCs":case"szCs":case"tabs":case"outlineLvl":case"contextualSpacing":case"tblStyleColBandSize":case"tblStyleRowBandSize":case"webHidden":case"pageBreakBefore":case"suppressLineNumbers":case"keepLines":case"keepNext":case"widowControl":case"bidi":case"rtl":case"noProof":break;default:this.options.debug&&console.warn(`DOCX: Unknown document element: ${e.localName}.${s.localName}`)}return t}parseUnderline(e,t){var r=v.attr(e,"val");if(null!=r){switch(r){case"dash":case"dashDotDotHeavy":case"dashDotHeavy":case"dashedHeavy":case"dashLong":case"dashLongHeavy":case"dotDash":case"dotDotDash":t["text-decoration"]="underline dashed";break;case"dotted":case"dottedHeavy":t["text-decoration"]="underline dotted";break;case"double":t["text-decoration"]="underline double";break;case"single":case"thick":case"words":t["text-decoration"]="underline";break;case"wave":case"wavyDouble":case"wavyHeavy":t["text-decoration"]="underline wavy";break;case"none":t["text-decoration"]="none"}var a=_e.colorAttr(e,"color");a&&(t["text-decoration-color"]=a)}}parseFont(e,t){var r=[v.attr(e,"ascii"),ze.themeValue(e,"asciiTheme"),v.attr(e,"eastAsia")].filter(e=>e).map(e=>a(e));r.length>0&&(t["font-family"]=[...new Set(r)].join(", "))}parseIndentation(e,t){var r=v.lengthAttr(e,"firstLine"),a=v.lengthAttr(e,"hanging"),s=v.lengthAttr(e,"left"),n=v.lengthAttr(e,"start"),l=v.lengthAttr(e,"right"),o=v.lengthAttr(e,"end");r&&(t["text-indent"]=r),a&&(t["text-indent"]=`-${a}`),(s||n)&&(t["margin-inline-start"]=s||n),(l||o)&&(t["margin-inline-end"]=l||o)}parseSpacing(e,t){var r=v.lengthAttr(e,"before"),a=v.lengthAttr(e,"after"),s=v.intAttr(e,"line",null),n=v.attr(e,"lineRule");if(r&&(t["margin-top"]=r),a&&(t["margin-bottom"]=a),null!==s)switch(n){case"auto":t["line-height"]=`${(s/240).toFixed(2)}`;break;case"atLeast":t["line-height"]=`calc(100% + ${s/20}pt)`;break;default:t["line-height"]=t["min-height"]=s/20+"pt"}}parseMarginProperties(e,t){for(const r of v.elements(e))switch(r.localName){case"left":t["padding-left"]=ze.valueOfMargin(r);break;case"right":t["padding-right"]=ze.valueOfMargin(r);break;case"top":t["padding-top"]=ze.valueOfMargin(r);break;case"bottom":t["padding-bottom"]=ze.valueOfMargin(r)}}parseTrHeight(e,t){v.attr(e,"hRule"),t.height=v.lengthAttr(e,"val")}parseBorderProperties(e,t){for(const r of v.elements(e))switch(r.localName){case"start":case"left":t["border-left"]=ze.valueOfBorder(r);break;case"end":case"right":t["border-right"]=ze.valueOfBorder(r);break;case"top":t["border-top"]=ze.valueOfBorder(r);break;case"bottom":t["border-bottom"]=ze.valueOfBorder(r)}}}const He=["black","blue","cyan","darkBlue","darkCyan","darkGray","darkGreen","darkMagenta","darkRed","darkYellow","green","lightGray","magenta","none","red","white","yellow"];class _e{static colorAttr(e,t,r=null,a="black"){var s=v.attr(e,t);if(s)return"auto"==s?a:He.includes(s)?s:`#${s}`;var n=v.attr(e,"themeColor");return n?`var(--docx-${n}-color)`:r}}class ze{static themeValue(e,t){var r=v.attr(e,t);return r?`var(--docx-${r}-font)`:null}static valueOfSize(e,t){var r=m;switch(v.attr(e,"type")){case"dxa":break;case"pct":r=g;break;case"auto":return"auto"}return v.lengthAttr(e,t,r)}static valueOfMargin(e){return v.lengthAttr(e,"w")}static valueOfBorder(e){var t=ze.parseBorderType(v.attr(e,"val"));if("none"==t)return"none";var r=_e.colorAttr(e,"color");return`${v.lengthAttr(e,"sz",d)} ${t} ${"auto"==r?$e:r}`}static parseBorderType(e){switch(e){case"single":case"dashDotStroked":case"thick":case"thickThinLargeGap":case"thickThinMediumGap":case"thickThinSmallGap":case"thinThickLargeGap":case"thinThickMediumGap":case"thinThickSmallGap":case"thinThickThinLargeGap":case"thinThickThinMediumGap":case"thinThickThinSmallGap":case"threeDEmboss":case"threeDEngrave":case"wave":return"solid";case"dashed":case"dashSmallGap":return"dashed";case"dotDash":case"dotDotDash":case"dotted":return"dotted";case"double":case"doubleWave":case"triple":return"double";case"inset":return"inset";case"nil":case"none":return"none";case"outset":return"outset"}return"solid"}static valueOfTblLayout(e){return"fixed"==v.attr(e,"val")?"fixed":"auto"}static classNameOfCnfStyle(e){const t=v.attr(e,"val");return["first-row","last-row","first-col","last-col","odd-col","even-col","odd-row","even-row","ne-cell","nw-cell","se-cell","sw-cell"].filter((e,r)=>"1"==t[r]).join(" ")}static valueOfJc(e){var t=v.attr(e,"val");switch(t){case"start":case"left":return"left";case"center":return"center";case"end":case"right":return"right";case"both":return"justify"}return t}static valueOfVertAlign(e,t=!1){var r=v.attr(e,"val");switch(r){case"subscript":return"sub";case"superscript":return t?"sup":"super"}return t?null:r}static valueOfTextAlignment(e){var t=v.attr(e,"val");switch(t){case"auto":case"baseline":return"baseline";case"top":return"top";case"center":return"middle";case"bottom":return"bottom"}return t}static addSize(e,t){return null==e?t:null==t?e:`calc(${e} + ${t})`}static classNameOftblLook(e){const t=v.hexAttr(e,"val",0);let r="";return(v.boolAttr(e,"firstRow")||32&t)&&(r+=" first-row"),(v.boolAttr(e,"lastRow")||64&t)&&(r+=" last-row"),(v.boolAttr(e,"firstColumn")||128&t)&&(r+=" first-col"),(v.boolAttr(e,"lastColumn")||256&t)&&(r+=" last-col"),(v.boolAttr(e,"noHBand")||512&t)&&(r+=" no-hband"),(v.boolAttr(e,"noVBand")||1024&t)&&(r+=" no-vband"),r.trim()}}const Ve={pos:0,leader:"none",style:"left"};function je(e,t,r,a=.75){const s=e.closest("p"),n=e.getBoundingClientRect(),l=s.getBoundingClientRect(),o=getComputedStyle(s),i=t?.length>0?t.map(e=>({pos:We(e.position),leader:e.leader,style:e.style})).sort((e,t)=>e.pos-t.pos):[Ve],c=i[i.length-1],h=l.width*a,m=We(r);let p=c.pos+m;if(p<h)for(;p<h&&i.length<50;p+=m)i.push({...Ve,pos:p});const u=parseFloat(o.marginLeft),d=l.left+u,f=(n.left-d)*a,g=i.find(e=>"clear"!=e.style&&e.pos>f);if(null==g)return;let b=1;if("right"==g.style||"center"==g.style){const t=Array.from(s.querySelectorAll(`.${e.className}`)),r=t.indexOf(e)+1,n=document.createRange();n.setStart(e,1),r<t.length?n.setEndBefore(t[r]):n.setEndAfter(s);const o="center"==g.style?.5:1,i=n.getBoundingClientRect(),c=i.left+o*i.width-(l.left-u);b=g.pos-c*a}else b=g.pos-f;switch(e.innerHTML=" ",e.style.textDecoration="inherit",e.style.wordSpacing=`${b.toFixed(0)}pt`,g.leader){case"dot":case"middleDot":e.style.textDecoration="underline",e.style.textDecorationStyle="dotted";break;case"hyphen":case"heavy":case"underscore":e.style.textDecoration="underline"}}function We(e){return parseFloat(e)}const Xe="http://www.w3.org/2000/svg",Ge="http://www.w3.org/1998/Math/MathML";class Ue{constructor(e){this.htmlDocument=e,this.className="docx",this.styleMap={},this.currentPart=null,this.tableVerticalMerges=[],this.currentVerticalMerge=null,this.tableCellPositions=[],this.currentCellPosition=null,this.footnoteMap={},this.endnoteMap={},this.currentEndnoteIds=[],this.usedHederFooterParts=[],this.currentTabs=[],this.commentMap={},this.tasks=[],this.postRenderTasks=[]}async render(e,t,r=null,a){this.document=e,this.options=a,this.className=a.className,this.rootSelector=a.inWrapper?`.${this.className}-wrapper`:":root",this.styleMap=null,this.tasks=[],this.options.renderComments&&globalThis.Highlight&&(this.commentHighlight=new Highlight),qe(r=r||t),qe(t),r.appendChild(this.createComment("docxjs library predefined styles")),r.appendChild(this.renderDefaultStyle()),e.themePart&&(r.appendChild(this.createComment("docxjs document theme values")),this.renderTheme(e.themePart,r)),null!=e.stylesPart&&(this.styleMap=this.processStyles(e.stylesPart.styles),r.appendChild(this.createComment("docxjs document styles")),r.appendChild(this.renderStyles(e.stylesPart.styles))),e.numberingPart&&(this.prodessNumberings(e.numberingPart.domNumberings),r.appendChild(this.createComment("docxjs document numbering styles")),r.appendChild(this.renderNumbering(e.numberingPart.domNumberings,r))),e.footnotesPart&&(this.footnoteMap=l(e.footnotesPart.notes,e=>e.id)),e.endnotesPart&&(this.endnoteMap=l(e.endnotesPart.notes,e=>e.id)),e.settingsPart&&(this.defaultTabSize=e.settingsPart.settings?.defaultTabStop),!a.ignoreFonts&&e.fontTablePart&&this.renderFontTable(e.fontTablePart,r);var s=this.renderSections(e.documentPart.body);this.options.inWrapper?t.appendChild(this.renderWrapper(s)):Je(t,s),this.commentHighlight&&a.renderComments&&CSS.highlights.set(`${this.className}-comments`,this.commentHighlight),this.postRenderTasks.forEach(e=>e()),await Promise.allSettled(this.tasks),this.refreshTabStops()}renderTheme(e,t){const r={},a=e.theme?.fontScheme;a&&(a.majorFont&&(r["--docx-majorHAnsi-font"]=a.majorFont.latinTypeface),a.minorFont&&(r["--docx-minorHAnsi-font"]=a.minorFont.latinTypeface));const s=e.theme?.colorScheme;if(s)for(let[e,t]of Object.entries(s.colors))r[`--docx-${e}-color`]=`#${t}`;const n=this.styleToString(`.${this.className}`,r);t.appendChild(this.createStyleElement(n))}renderFontTable(e,t){for(let r of e.fonts)for(let e of r.embedFontRefs)this.tasks.push(this.document.loadFont(e.id,e.key).then(s=>{const n={"font-family":a(r.name),src:`url(${s})`};"bold"!=e.type&&"boldItalic"!=e.type||(n["font-weight"]="bold"),"italic"!=e.type&&"boldItalic"!=e.type||(n["font-style"]="italic");const l=this.styleToString("@font-face",n);t.appendChild(this.createComment(`docxjs ${r.name} font`)),t.appendChild(this.createStyleElement(l))}))}processStyleName(e){return e?`${this.className}_${function(e){return e?.replace(/[ .]+/g,"-").replace(/[&]+/g,"and").toLowerCase()}(e)}`:this.className}processStyles(e){const t=l(e.filter(e=>null!=e.id),e=>e.id);for(const a of e.filter(e=>e.basedOn)){var r=t[a.basedOn];if(r){a.paragraphProps=i(a.paragraphProps,r.paragraphProps),a.runProps=i(a.runProps,r.runProps);for(const e of r.styles){const t=a.styles.find(t=>t.target==e.target);t?this.copyStyleProperties(e.values,t.values):a.styles.push({...e,values:{...e.values}})}}else this.options.debug&&console.warn(`Can't find base style ${a.basedOn}`)}for(let t of e)t.cssName=this.processStyleName(t.id);return t}prodessNumberings(e){for(let t of e.filter(e=>e.pStyleName)){const e=this.findStyle(t.pStyleName);e?.paragraphProps?.numbering&&(e.paragraphProps.numbering.level=t.level)}}processElement(e){if(e.children)for(var t of e.children)t.parent=e,t.type==R.Table?this.processTable(t):this.processElement(t)}processTable(e){for(var t of e.children)for(var r of t.children)r.cssStyle=this.copyStyleProperties(e.cellStyle,r.cssStyle,["border-left","border-right","border-top","border-bottom","padding-left","padding-right","padding-top","padding-bottom"]),this.processElement(r)}copyStyleProperties(e,t,r=null){if(!e)return t;for(var a of(null==t&&(t={}),null==r&&(r=Object.getOwnPropertyNames(e)),r))e.hasOwnProperty(a)&&!t.hasOwnProperty(a)&&(t[a]=e[a]);return t}createPageElement(e,t){var r=this.createElement("section",{className:e});return t&&(t.pageMargins&&(r.style.paddingLeft=t.pageMargins.left,r.style.paddingRight=t.pageMargins.right,r.style.paddingTop=t.pageMargins.top,r.style.paddingBottom=t.pageMargins.bottom),t.pageSize&&(this.options.ignoreWidth||(r.style.width=t.pageSize.width),this.options.ignoreHeight||(r.style.minHeight=t.pageSize.height))),r}createSectionContent(e){var t=this.createElement("article");return e.columns&&e.columns.numberOfColumns&&(t.style.columnCount=`${e.columns.numberOfColumns}`,t.style.columnGap=e.columns.space,e.columns.separator&&(t.style.columnRule="1px solid black")),t}renderSections(e){const t=[];this.processElement(e);const r=this.splitBySection(e.children,e.props),a=this.groupByPageBreaks(r);let s=null;for(let r=0,l=a.length;r<l;r++){this.currentFootnoteIds=[];let o=a[r][0].sectProps;const i=this.createPageElement(this.className,o);this.renderStyleValues(e.cssStyle,i),this.options.renderHeaders&&this.renderHeaderFooter(o.headerRefs,o,t.length,s!=o,i);for(const e of a[r]){var n=this.createSectionContent(e.sectProps);this.renderElements(e.elements,n),i.appendChild(n),o=e.sectProps}this.options.renderFootnotes&&this.renderNotes(this.currentFootnoteIds,this.footnoteMap,i),this.options.renderEndnotes&&r==l-1&&this.renderNotes(this.currentEndnoteIds,this.endnoteMap,i),this.options.renderFooters&&this.renderHeaderFooter(o.footerRefs,o,t.length,s!=o,i),t.push(i),s=o}return t}renderHeaderFooter(e,t,r,a,s){if(e){var n=(t.titlePage&&a?e.find(e=>"first"==e.type):null)??(r%2==1?e.find(e=>"even"==e.type):null)??e.find(e=>"default"==e.type),l=n&&this.document.findPartByRelId(n.id,this.document.documentPart);if(l){this.currentPart=l,this.usedHederFooterParts.includes(l.path)||(this.processElement(l.rootElement),this.usedHederFooterParts.push(l.path));const[e]=this.renderElements([l.rootElement],s);t?.pageMargins&&(l.rootElement.type===R.Header?(e.style.marginTop=`calc(${t.pageMargins.header} - ${t.pageMargins.top})`,e.style.minHeight=`calc(${t.pageMargins.top} - ${t.pageMargins.header})`):l.rootElement.type===R.Footer&&(e.style.marginBottom=`calc(${t.pageMargins.footer} - ${t.pageMargins.bottom})`,e.style.minHeight=`calc(${t.pageMargins.bottom} - ${t.pageMargins.footer})`)),this.currentPart=null}}}isPageBreakElement(e){return e.type==R.Break&&("lastRenderedPageBreak"==e.break?!this.options.ignoreLastRenderedPageBreak:"page"==e.break)}isPageBreakSection(e,t){return!!e&&(!!t&&(e.pageSize?.orientation!=t.pageSize?.orientation||e.pageSize?.width!=t.pageSize?.width||e.pageSize?.height!=t.pageSize?.height))}splitBySection(e,t){var r={sectProps:null,elements:[],pageBreak:!1},a=[r];for(let t of e){if(t.type==R.Paragraph){const e=this.findStyle(t.styleName);e?.paragraphProps?.pageBreakBefore&&(r.sectProps=s,r.pageBreak=!0,r={sectProps:null,elements:[],pageBreak:!1},a.push(r))}if(r.elements.push(t),t.type==R.Paragraph){const e=t;var s=e.sectionProps,n=-1,l=-1;if(this.options.breakPages&&e.children&&(n=e.children.findIndex(e=>-1!=(l=e.children?.findIndex(this.isPageBreakElement.bind(this))??-1))),(s||-1!=n)&&(r.sectProps=s,r.pageBreak=-1!=n,r={sectProps:null,elements:[],pageBreak:!1},a.push(r)),-1!=n){let a=e.children[n],s=l<a.children.length-1;if(n<e.children.length-1||s){var o=t.children,i={...t,children:o.slice(n)};if(t.children=o.slice(0,n),r.elements.push(i),s){let e=a.children,r={...a,children:e.slice(0,l)};t.children.push(r),a.children=e.slice(l)}}}}}let c=null;for(let e=a.length-1;e>=0;e--)null==a[e].sectProps?a[e].sectProps=c??t:c=a[e].sectProps;return a}groupByPageBreaks(e){let t,r=[];const a=[r];for(let s of e)r.push(s),(this.options.ignoreLastRenderedPageBreak||s.pageBreak||this.isPageBreakSection(t,s.sectProps))&&a.push(r=[]),t=s.sectProps;return a.filter(e=>e.length>0)}renderWrapper(e){return this.createElement("div",{className:`${this.className}-wrapper`},e)}renderDefaultStyle(){var e=this.className,t=`\n.${e}-wrapper { background: gray; padding: 30px; padding-bottom: 0px; display: flex; flex-flow: column; align-items: center; } \n.${e}-wrapper>section.${e} { background: white; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); margin-bottom: 30px; }`;this.options.hideWrapperOnPrint&&(t=`@media not print { ${t} }`);var r=`${t}\n.${e} { color: black; hyphens: auto; text-underline-position: from-font; }\nsection.${e} { box-sizing: border-box; display: flex; flex-flow: column nowrap; position: relative; overflow: hidden; }\nsection.${e}>article { margin-bottom: auto; z-index: 1; }\nsection.${e}>footer { z-index: 1; }\n.${e} table { border-collapse: collapse; }\n.${e} table td, .${e} table th { vertical-align: top; }\n.${e} p { margin: 0pt; min-height: 1em; }\n.${e} span { white-space: pre-wrap; overflow-wrap: break-word; }\n.${e} a { color: inherit; text-decoration: inherit; }\n.${e} svg { fill: transparent; }\n`;return this.options.renderComments&&(r+=`\n.${e}-comment-ref { cursor: default; }\n.${e}-comment-popover { display: none; z-index: 1000; padding: 0.5rem; background: white; position: absolute; box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.25); width: 30ch; }\n.${e}-comment-ref:hover~.${e}-comment-popover { display: block; }\n.${e}-comment-author,.${e}-comment-date { font-size: 0.875rem; color: #888; }\n`),this.createStyleElement(r)}renderNumbering(e,t){var r="",a=[];for(var s of e){var n=`p.${this.numberingClass(s.id,s.level)}`,l="none";if(s.bullet){let e=`--${this.className}-${s.bullet.src}`.toLowerCase();r+=this.styleToString(`${n}:before`,{content:"' '",display:"inline-block",background:`var(${e})`},s.bullet.style),this.tasks.push(this.document.loadNumberingImage(s.bullet.src).then(r=>{var a=`${this.rootSelector} { ${e}: url(${r}) }`;t.appendChild(this.createStyleElement(a))}))}else if(s.levelText){let e=this.numberingCounter(s.id,s.level);const t=e+" "+(s.start-1);s.level>0&&(r+=this.styleToString(`p.${this.numberingClass(s.id,s.level-1)}`,{"counter-set":t})),a.push(t),r+=this.styleToString(`${n}:before`,{content:this.levelTextToContent(s.levelText,s.suff,s.id,this.numFormatToCssValue(s.format)),"counter-increment":e,...s.rStyle})}else l=this.numFormatToCssValue(s.format);r+=this.styleToString(n,{display:"list-item","list-style-position":"inside","list-style-type":l,...s.pStyle})}return a.length>0&&(r+=this.styleToString(this.rootSelector,{"counter-reset":a.join(" ")})),this.createStyleElement(r)}renderStyles(e){var t="";const r=this.styleMap,a=l(e.filter(e=>e.isDefault),e=>e.target);for(const l of e){var s=l.styles;if(l.linked){var n=l.linked&&r[l.linked];n?s=s.concat(n.styles):this.options.debug&&console.warn(`Can't find linked style ${l.linked}`)}for(const e of s){var o=`${l.target??""}.${l.cssName}`;l.target!=e.target&&(o+=` ${e.target}`),a[l.target]==l&&(o=`.${this.className} ${l.target}, `+o),t+=this.styleToString(o,e.values)}}return this.createStyleElement(t)}renderNotes(e,t,r){var a=e.map(e=>t[e]).filter(e=>e);if(a.length>0){var s=this.createElement("ol",null,this.renderElements(a));r.appendChild(s)}}renderElement(e){switch(e.type){case R.Paragraph:return this.renderParagraph(e);case R.BookmarkStart:return this.renderBookmarkStart(e);case R.BookmarkEnd:return null;case R.Run:return this.renderRun(e);case R.Table:return this.renderTable(e);case R.Row:return this.renderTableRow(e);case R.Cell:return this.renderTableCell(e);case R.Hyperlink:return this.renderHyperlink(e);case R.SmartTag:return this.renderSmartTag(e);case R.Drawing:return this.renderDrawing(e);case R.Image:return this.renderImage(e);case R.Text:case R.Text:return this.renderText(e);case R.DeletedText:return this.renderDeletedText(e);case R.Tab:return this.renderTab(e);case R.Symbol:return this.renderSymbol(e);case R.Break:return this.renderBreak(e);case R.Footer:return this.renderContainer(e,"footer");case R.Header:return this.renderContainer(e,"header");case R.Footnote:case R.Endnote:return this.renderContainer(e,"li");case R.FootnoteReference:return this.renderFootnoteReference(e);case R.EndnoteReference:return this.renderEndnoteReference(e);case R.NoBreakHyphen:return this.createElement("wbr");case R.VmlPicture:return this.renderVmlPicture(e);case R.VmlElement:return this.renderVmlElement(e);case R.MmlMath:return this.renderContainerNS(e,Ge,"math",{xmlns:Ge});case R.MmlMathParagraph:return this.renderContainer(e,"span");case R.MmlFraction:return this.renderContainerNS(e,Ge,"mfrac");case R.MmlBase:return this.renderContainerNS(e,Ge,e.parent.type==R.MmlMatrixRow?"mtd":"mrow");case R.MmlNumerator:case R.MmlDenominator:case R.MmlFunction:case R.MmlLimit:case R.MmlBox:return this.renderContainerNS(e,Ge,"mrow");case R.MmlGroupChar:return this.renderMmlGroupChar(e);case R.MmlLimitLower:return this.renderContainerNS(e,Ge,"munder");case R.MmlMatrix:return this.renderContainerNS(e,Ge,"mtable");case R.MmlMatrixRow:return this.renderContainerNS(e,Ge,"mtr");case R.MmlRadical:return this.renderMmlRadical(e);case R.MmlSuperscript:return this.renderContainerNS(e,Ge,"msup");case R.MmlSubscript:return this.renderContainerNS(e,Ge,"msub");case R.MmlDegree:case R.MmlSuperArgument:case R.MmlSubArgument:return this.renderContainerNS(e,Ge,"mn");case R.MmlFunctionName:return this.renderContainerNS(e,Ge,"ms");case R.MmlDelimiter:return this.renderMmlDelimiter(e);case R.MmlRun:return this.renderMmlRun(e);case R.MmlNary:return this.renderMmlNary(e);case R.MmlPreSubSuper:return this.renderMmlPreSubSuper(e);case R.MmlBar:return this.renderMmlBar(e);case R.MmlEquationArray:return this.renderMllList(e);case R.Inserted:return this.renderInserted(e);case R.Deleted:return this.renderDeleted(e);case R.CommentRangeStart:return this.renderCommentRangeStart(e);case R.CommentRangeEnd:return this.renderCommentRangeEnd(e);case R.CommentReference:return this.renderCommentReference(e);case R.AltChunk:return this.renderAltChunk(e)}return null}renderElements(e,t){if(null==e)return null;var r=e.flatMap(e=>this.renderElement(e)).filter(e=>null!=e);return t&&Je(t,r),r}renderContainer(e,t,r){return this.createElement(t,r,this.renderElements(e.children))}renderContainerNS(e,t,r,a){return this.createElementNS(t,r,a,this.renderElements(e.children))}renderParagraph(e){var t=this.renderContainer(e,"p");const r=this.findStyle(e.styleName);e.tabs??(e.tabs=r?.paragraphProps?.tabs),this.renderClass(e,t),this.renderStyleValues(e.cssStyle,t),this.renderCommonProperties(t.style,e);const a=e.numbering??r?.paragraphProps?.numbering;return a&&t.classList.add(this.numberingClass(a.id,a.level)),t}renderRunProperties(e,t){this.renderCommonProperties(e,t)}renderCommonProperties(e,t){null!=t&&(t.color&&(e.color=t.color),t.fontSize&&(e["font-size"]=t.fontSize))}renderHyperlink(e){var t=this.renderContainer(e,"a");this.renderStyleValues(e.cssStyle,t);let r="";if(e.id){const t=this.document.documentPart.rels.find(t=>t.id==e.id&&"External"===t.targetMode);r=t?.target??r}return e.anchor&&(r+=`#${e.anchor}`),t.href=r,t}renderSmartTag(e){return this.renderContainer(e,"span")}renderCommentRangeStart(e){if(!this.options.renderComments)return null;const t=new Range;this.commentHighlight?.add(t);const r=this.createComment(`start of comment #${e.id}`);return this.later(()=>t.setStart(r,0)),this.commentMap[e.id]=t,r}renderCommentRangeEnd(e){if(!this.options.renderComments)return null;const t=this.commentMap[e.id],r=this.createComment(`end of comment #${e.id}`);return this.later(()=>t?.setEnd(r,0)),r}renderCommentReference(e){if(!this.options.renderComments)return null;var t=this.document.commentsPart?.commentMap[e.id];if(!t)return null;const r=new DocumentFragment,a=this.createElement("span",{className:`${this.className}-comment-ref`},["💬"]),s=this.createElement("div",{className:`${this.className}-comment-popover`});return this.renderCommentContent(t,s),r.appendChild(this.createComment(`comment #${t.id} by ${t.author} on ${t.date}`)),r.appendChild(a),r.appendChild(s),r}renderAltChunk(e){if(!this.options.renderAltChunks)return null;var t=this.createElement("iframe");return this.tasks.push(this.document.loadAltChunk(e.id,this.currentPart).then(e=>{t.srcdoc=e})),t}renderCommentContent(e,t){t.appendChild(this.createElement("div",{className:`${this.className}-comment-author`},[e.author])),t.appendChild(this.createElement("div",{className:`${this.className}-comment-date`},[new Date(e.date).toLocaleString()])),this.renderElements(e.children,t)}renderDrawing(e){var t=this.renderContainer(e,"div");return t.style.display="inline-block",t.style.position="relative",t.style.textIndent="0px",this.renderStyleValues(e.cssStyle,t),t}renderImage(e){let t=this.createElement("img"),r=e.cssStyle?.transform;if(this.renderStyleValues(e.cssStyle,t),e.srcRect&&e.srcRect.some(e=>0!=e)){var[a,s,n,l]=e.srcRect;r=`scale(${1/(1-a-n)}, ${1/(1-s-l)})`,t.style["clip-path"]=`rect(${(100*s).toFixed(2)}% ${(100*(1-n)).toFixed(2)}% ${(100*(1-l)).toFixed(2)}% ${(100*a).toFixed(2)}%)`}return e.rotation&&(r=`rotate(${e.rotation}deg) ${r??""}`),t.style.transform=r?.trim(),this.document&&this.tasks.push(this.document.loadDocumentImage(e.src,this.currentPart).then(e=>{t.src=e})),t}renderText(e){return this.htmlDocument.createTextNode(e.text)}renderDeletedText(e){return this.options.renderChanges?this.renderText(e):null}renderBreak(e){return"textWrapping"==e.break?this.createElement("br"):null}renderInserted(e){return this.options.renderChanges?this.renderContainer(e,"ins"):this.renderElements(e.children)}renderDeleted(e){return this.options.renderChanges?this.renderContainer(e,"del"):null}renderSymbol(e){var t=this.createElement("span");return t.style.fontFamily=e.font,t.innerHTML=`&#x${e.char};`,t}renderFootnoteReference(e){var t=this.createElement("sup");return this.currentFootnoteIds.push(e.id),t.textContent=`${this.currentFootnoteIds.length}`,t}renderEndnoteReference(e){var t=this.createElement("sup");return this.currentEndnoteIds.push(e.id),t.textContent=`${this.currentEndnoteIds.length}`,t}renderTab(e){var t=this.createElement("span");if(t.innerHTML=" ",this.options.experimental){t.className=this.tabStopClass();var r=function(e,t){var r=e.parent;for(;null!=r&&r.type!=t;)r=r.parent;return r}(e,R.Paragraph)?.tabs;this.currentTabs.push({stops:r,span:t})}return t}renderBookmarkStart(e){return this.createElement("span",{id:e.name})}renderRun(e){if(e.fieldRun)return null;const t=this.createElement("span");if(e.id&&(t.id=e.id),this.renderClass(e,t),this.renderStyleValues(e.cssStyle,t),e.verticalAlign){const r=this.createElement(e.verticalAlign);this.renderElements(e.children,r),t.appendChild(r)}else this.renderElements(e.children,t);return t}renderTable(e){let t=this.createElement("table");return this.tableCellPositions.push(this.currentCellPosition),this.tableVerticalMerges.push(this.currentVerticalMerge),this.currentVerticalMerge={},this.currentCellPosition={col:0,row:0},e.columns&&t.appendChild(this.renderTableColumns(e.columns)),this.renderClass(e,t),this.renderElements(e.children,t),this.renderStyleValues(e.cssStyle,t),this.currentVerticalMerge=this.tableVerticalMerges.pop(),this.currentCellPosition=this.tableCellPositions.pop(),t}renderTableColumns(e){let t=this.createElement("colgroup");for(let r of e){let e=this.createElement("col");r.width&&(e.style.width=r.width),t.appendChild(e)}return t}renderTableRow(e){let t=this.createElement("tr");return this.currentCellPosition.col=0,e.gridBefore&&t.appendChild(this.renderTableCellPlaceholder(e.gridBefore)),this.renderClass(e,t),this.renderElements(e.children,t),this.renderStyleValues(e.cssStyle,t),e.gridAfter&&t.appendChild(this.renderTableCellPlaceholder(e.gridAfter)),this.currentCellPosition.row++,t}renderTableCellPlaceholder(e){const t=this.createElement("td",{colSpan:e});return t.style.border="none",t}renderTableCell(e){let t=this.renderContainer(e,"td");const r=this.currentCellPosition.col;return e.verticalMerge?"restart"==e.verticalMerge?(this.currentVerticalMerge[r]=t,t.rowSpan=1):this.currentVerticalMerge[r]&&(this.currentVerticalMerge[r].rowSpan+=1,t.style.display="none"):this.currentVerticalMerge[r]=null,this.renderClass(e,t),this.renderStyleValues(e.cssStyle,t),e.span&&(t.colSpan=e.span),this.currentCellPosition.col+=t.colSpan,t}renderVmlPicture(e){return this.renderContainer(e,"div")}renderVmlElement(e){var t=this.createSvgElement("svg");t.setAttribute("style",e.cssStyleText);const r=this.renderVmlChildElement(e);return e.imageHref?.id&&this.tasks.push(this.document?.loadDocumentImage(e.imageHref.id,this.currentPart).then(e=>r.setAttribute("href",e))),t.appendChild(r),requestAnimationFrame(()=>{const e=t.firstElementChild.getBBox();t.setAttribute("width",`${Math.ceil(e.x+e.width)}`),t.setAttribute("height",`${Math.ceil(e.y+e.height)}`)}),t}renderVmlChildElement(e){const t=this.createSvgElement(e.tagName);Object.entries(e.attrs).forEach(([e,r])=>t.setAttribute(e,r));for(let r of e.children)r.type==R.VmlElement?t.appendChild(this.renderVmlChildElement(r)):t.appendChild(...c(this.renderElement(r)));return t}renderMmlRadical(e){const t=e.children.find(e=>e.type==R.MmlBase);if(e.props?.hideDegree)return this.createElementNS(Ge,"msqrt",null,this.renderElements([t]));const r=e.children.find(e=>e.type==R.MmlDegree);return this.createElementNS(Ge,"mroot",null,this.renderElements([t,r]))}renderMmlDelimiter(e){const t=[];return t.push(this.createElementNS(Ge,"mo",null,[e.props.beginChar??"("])),t.push(...this.renderElements(e.children)),t.push(this.createElementNS(Ge,"mo",null,[e.props.endChar??")"])),this.createElementNS(Ge,"mrow",null,t)}renderMmlNary(e){const t=[],r=l(e.children,e=>e.type),a=r[R.MmlSuperArgument],s=r[R.MmlSubArgument],n=a?this.createElementNS(Ge,"mo",null,c(this.renderElement(a))):null,o=s?this.createElementNS(Ge,"mo",null,c(this.renderElement(s))):null,i=this.createElementNS(Ge,"mo",null,[e.props?.char??"∫"]);return n||o?t.push(this.createElementNS(Ge,"munderover",null,[i,o,n])):n?t.push(this.createElementNS(Ge,"mover",null,[i,n])):o?t.push(this.createElementNS(Ge,"munder",null,[i,o])):t.push(i),t.push(...this.renderElements(r[R.MmlBase].children)),this.createElementNS(Ge,"mrow",null,t)}renderMmlPreSubSuper(e){const t=[],r=l(e.children,e=>e.type),a=r[R.MmlSuperArgument],s=r[R.MmlSubArgument],n=a?this.createElementNS(Ge,"mo",null,c(this.renderElement(a))):null,o=s?this.createElementNS(Ge,"mo",null,c(this.renderElement(s))):null,i=this.createElementNS(Ge,"mo",null);return t.push(this.createElementNS(Ge,"msubsup",null,[i,o,n])),t.push(...this.renderElements(r[R.MmlBase].children)),this.createElementNS(Ge,"mrow",null,t)}renderMmlGroupChar(e){const t="bot"===e.props.verticalJustification?"mover":"munder",r=this.renderContainerNS(e,Ge,t);return e.props.char&&r.appendChild(this.createElementNS(Ge,"mo",null,[e.props.char])),r}renderMmlBar(e){const t=this.renderContainerNS(e,Ge,"mrow");switch(e.props.position){case"top":t.style.textDecoration="overline";break;case"bottom":t.style.textDecoration="underline"}return t}renderMmlRun(e){const t=this.createElementNS(Ge,"ms",null,this.renderElements(e.children));return this.renderClass(e,t),this.renderStyleValues(e.cssStyle,t),t}renderMllList(e){const t=this.createElementNS(Ge,"mtable");this.renderClass(e,t),this.renderStyleValues(e.cssStyle,t);for(let r of this.renderElements(e.children))t.appendChild(this.createElementNS(Ge,"mtr",null,[this.createElementNS(Ge,"mtd",null,[r])]));return t}renderStyleValues(e,t){for(let r in e)r.startsWith("$")?t.setAttribute(r.slice(1),e[r]):t.style[r]=e[r]}renderClass(e,t){e.className&&(t.className=e.className),e.styleName&&t.classList.add(this.processStyleName(e.styleName))}findStyle(e){return e&&this.styleMap?.[e]}numberingClass(e,t){return`${this.className}-num-${e}-${t}`}tabStopClass(){return`${this.className}-tab-stop`}styleToString(e,t,r=null){let a=`${e} {\r\n`;for(const e in t)e.startsWith("$")||(a+=` ${e}: ${t[e]};\r\n`);return r&&(a+=r),a+"}\r\n"}numberingCounter(e,t){return`${this.className}-num-${e}-${t}`}levelTextToContent(e,t,r,a){return`"${e.replace(/%\d*/g,e=>{let t=parseInt(e.substring(1),10)-1;return`"counter(${this.numberingCounter(r,t)}, ${a})"`})}${{tab:"\\9",space:"\\a0"}[t]??""}"`}numFormatToCssValue(e){return{none:"none",bullet:"disc",decimal:"decimal",lowerLetter:"lower-alpha",upperLetter:"upper-alpha",lowerRoman:"lower-roman",upperRoman:"upper-roman",decimalZero:"decimal-leading-zero",aiueo:"katakana",aiueoFullWidth:"katakana",chineseCounting:"simp-chinese-informal",chineseCountingThousand:"simp-chinese-informal",chineseLegalSimplified:"simp-chinese-formal",chosung:"hangul-consonant",ideographDigital:"cjk-ideographic",ideographTraditional:"cjk-heavenly-stem",ideographLegalTraditional:"trad-chinese-formal",ideographZodiac:"cjk-earthly-branch",iroha:"katakana-iroha",irohaFullWidth:"katakana-iroha",japaneseCounting:"japanese-informal",japaneseDigitalTenThousand:"cjk-decimal",japaneseLegal:"japanese-formal",thaiNumbers:"thai",koreanCounting:"korean-hangul-formal",koreanDigital:"korean-hangul-formal",koreanDigital2:"korean-hanja-informal",hebrew1:"hebrew",hebrew2:"hebrew",hindiNumbers:"devanagari",ganada:"hangul",taiwaneseCounting:"cjk-ideographic",taiwaneseCountingThousand:"cjk-ideographic",taiwaneseDigital:"cjk-decimal"}[e]??e}refreshTabStops(){this.options.experimental&&setTimeout(()=>{const e=function(e=document.body){const t=document.createElement("div");t.style.width="100pt",e.appendChild(t);const r=100/t.offsetWidth;return e.removeChild(t),r}();for(let t of this.currentTabs)je(t.span,t.stops,this.defaultTabSize,e)},500)}createElementNS(e,t,r,a){var s=e?this.htmlDocument.createElementNS(e,t):this.htmlDocument.createElement(t);return Object.assign(s,r),a&&Je(s,a),s}createElement(e,t,r){return this.createElementNS(void 0,e,t,r)}createSvgElement(e,t,r){return this.createElementNS(Xe,e,t,r)}createStyleElement(e){return this.createElement("style",{innerHTML:e})}createComment(e){return this.htmlDocument.createComment(e)}later(e){this.postRenderTasks.push(e)}}function qe(e){e.innerHTML=""}function Je(e,t){t.forEach(t=>{return e.appendChild("string"==typeof(r=t)||r instanceof String?document.createTextNode(t):t);var r})}const Ze={ignoreHeight:!1,ignoreWidth:!1,ignoreFonts:!1,breakPages:!0,debug:!1,experimental:!1,className:"docx",inWrapper:!0,hideWrapperOnPrint:!1,trimXmlDeclaration:!0,ignoreLastRenderedPageBreak:!0,renderHeaders:!0,renderFooters:!0,renderFootnotes:!0,renderEndnotes:!0,useBase64URL:!1,renderChanges:!1,renderComments:!1,renderAltChunks:!0};function Ke(e,t){const r={...Ze,...t};return ke.load(e,new Oe(r),r)}async function Ye(e,t,r,a){const s={...Ze,...a},n=new Ue(window.document);return await n.render(e,t,r,s)}e.defaultOptions=Ze,e.parseAsync=Ke,e.renderAsync=async function(e,t,r,a){const s=await Ke(e,a);return await Ye(s,t,r,a),s},e.renderDocument=Ye});
|
||
//# sourceMappingURL=docx-preview.min.js.map
|
||
|
||
/**
|
||
* 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));
|
||
|
||
/**
|
||
* ZDDC — shared SHA-256 helpers
|
||
*
|
||
* Attaches to window.zddc.crypto. Must load AFTER shared/zddc.js (which creates
|
||
* the window.zddc object).
|
||
*
|
||
* Exports:
|
||
* zddc.crypto.sha256Hex(buffer) → Promise<string> hex digest of ArrayBuffer/Uint8Array
|
||
* zddc.crypto.sha256String(str) → Promise<string> hex digest of UTF-8 encoded string
|
||
* zddc.crypto.sha256File(file, onProgress?) → Promise<string>
|
||
* chunked streaming digest for File/Blob; for files >= 4 MB, streams 2 MB chunks
|
||
* and invokes onProgress(loaded, total) every ~8 MB.
|
||
* zddc.crypto.bytesToHex(buffer) → string (hex of ArrayBuffer/Uint8Array, no digest)
|
||
*
|
||
* Throws if Web Crypto SubtleCrypto is not available.
|
||
*/
|
||
(function (root) {
|
||
'use strict';
|
||
|
||
if (!root.zddc) {
|
||
throw new Error('shared/hash.js: window.zddc must be loaded first');
|
||
}
|
||
|
||
var HASH_CHUNK_SIZE = 2 * 1024 * 1024; // 2 MB
|
||
|
||
function requireSubtle() {
|
||
if (!root.crypto || !root.crypto.subtle || typeof root.crypto.subtle.digest !== 'function') {
|
||
throw new Error('Web Crypto SubtleCrypto is required');
|
||
}
|
||
}
|
||
|
||
function bytesToHex(buffer) {
|
||
return Array.from(new Uint8Array(buffer), function (byte) {
|
||
return byte.toString(16).padStart(2, '0');
|
||
}).join('');
|
||
}
|
||
|
||
async function sha256Hex(buffer) {
|
||
requireSubtle();
|
||
var input = (buffer instanceof Uint8Array) ? buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) : buffer;
|
||
var hash = await root.crypto.subtle.digest('SHA-256', input);
|
||
return bytesToHex(hash);
|
||
}
|
||
|
||
async function sha256String(str) {
|
||
requireSubtle();
|
||
var bytes = new TextEncoder().encode(str);
|
||
var hash = await root.crypto.subtle.digest('SHA-256', bytes);
|
||
return bytesToHex(hash);
|
||
}
|
||
|
||
async function sha256File(file, onProgress) {
|
||
requireSubtle();
|
||
// Single-shot for small files or environments without ReadableStream
|
||
if (file.size < HASH_CHUNK_SIZE * 2 || typeof file.stream !== 'function') {
|
||
if (onProgress) { onProgress(file.size, file.size); }
|
||
var buf = await file.arrayBuffer();
|
||
var hash = await root.crypto.subtle.digest('SHA-256', buf);
|
||
return bytesToHex(hash);
|
||
}
|
||
// Chunked streaming for large files
|
||
var reader = file.stream().getReader();
|
||
var loaded = 0;
|
||
var chunks = [];
|
||
var yieldCounter = 0;
|
||
while (true) {
|
||
var result = await reader.read();
|
||
if (result.done) { break; }
|
||
chunks.push(result.value);
|
||
loaded += result.value.byteLength;
|
||
yieldCounter++;
|
||
if (onProgress && yieldCounter % 4 === 0) {
|
||
onProgress(loaded, file.size);
|
||
await new Promise(function (r) { setTimeout(r, 0); });
|
||
}
|
||
}
|
||
var total = new Uint8Array(loaded);
|
||
var offset = 0;
|
||
for (var i = 0; i < chunks.length; i++) {
|
||
total.set(chunks[i], offset);
|
||
offset += chunks[i].byteLength;
|
||
}
|
||
var digest = await root.crypto.subtle.digest('SHA-256', total.buffer);
|
||
if (onProgress) { onProgress(file.size, file.size); }
|
||
return bytesToHex(digest);
|
||
}
|
||
|
||
root.zddc.crypto = {
|
||
sha256Hex: sha256Hex,
|
||
sha256String: sha256String,
|
||
sha256File: sha256File,
|
||
bytesToHex: bytesToHex,
|
||
};
|
||
})(typeof window !== 'undefined' ? window : globalThis);
|
||
|
||
/**
|
||
* 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 preview helpers
|
||
*
|
||
* Cross-tool helpers for previewing file types that need a decoder:
|
||
* - TIFF (UTIF.js) — multi-page, browser-PDF-viewer-style toolbar
|
||
* - ZIP listing (JSZip) — sortable file-list view
|
||
*
|
||
* Renderers operate on any document (parent window or popup window), so the
|
||
* same code works for tools whose preview opens in a popup (classifier,
|
||
* archive, transmittal) and tools that render inline (mdedit).
|
||
*
|
||
* Public API on window.zddc.preview:
|
||
* loadLibrary(url) → Promise<void>
|
||
* renderTiff(doc, container, arrayBuffer, opts) → Promise<void>
|
||
* renderZipListing(doc, container, arrayBuffer, opts) → Promise<void>
|
||
* TIFF_EXTENSIONS, IMAGE_EXTENSIONS, TEXT_EXTENSIONS, OFFICE_EXTENSIONS
|
||
* isTiff(ext), isImage(ext), isText(ext), isZip(ext), isOffice(ext)
|
||
*
|
||
* Each tool keeps its own dispatcher; this lib only owns the heavy renderers.
|
||
*/
|
||
|
||
(function (root) {
|
||
'use strict';
|
||
|
||
var TIFF_EXTENSIONS = ['tif', 'tiff'];
|
||
var IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico'];
|
||
var TEXT_EXTENSIONS = [
|
||
'txt', 'md', 'markdown', 'json', 'xml', 'csv', 'tsv', 'log',
|
||
'html', 'htm', 'css', 'js', 'mjs', 'ts', 'tsx', 'jsx',
|
||
'py', 'rb', 'sh', 'bash', 'zsh', 'bat', 'ps1',
|
||
'yaml', 'yml', 'ini', 'cfg', 'conf', 'toml',
|
||
'c', 'cc', 'cpp', 'h', 'hpp', 'go', 'rs', 'java', 'kt',
|
||
'sql', 'env'
|
||
];
|
||
var OFFICE_EXTENSIONS = ['docx', 'xlsx', 'xls'];
|
||
|
||
function lowerExt(ext) { return (ext || '').toLowerCase(); }
|
||
function isTiff(ext) { return TIFF_EXTENSIONS.indexOf(lowerExt(ext)) !== -1; }
|
||
function isImage(ext) { return IMAGE_EXTENSIONS.indexOf(lowerExt(ext)) !== -1; }
|
||
function isText(ext) { return TEXT_EXTENSIONS.indexOf(lowerExt(ext)) !== -1; }
|
||
function isZip(ext) { return lowerExt(ext) === 'zip'; }
|
||
function isOffice(ext) { return OFFICE_EXTENSIONS.indexOf(lowerExt(ext)) !== -1; }
|
||
|
||
// ── CDN library loader (parent window cache) ─────────────────────────────
|
||
|
||
var _libCache = new Map();
|
||
|
||
function loadLibrary(url) {
|
||
if (_libCache.has(url)) return _libCache.get(url);
|
||
var p = new Promise(function (resolve, reject) {
|
||
var s = document.createElement('script');
|
||
s.src = url;
|
||
s.onload = function () { resolve(); };
|
||
s.onerror = function () { reject(new Error('Failed to load: ' + url)); };
|
||
document.head.appendChild(s);
|
||
});
|
||
_libCache.set(url, p);
|
||
return p;
|
||
}
|
||
|
||
// ── Style injection (idempotent per-document) ────────────────────────────
|
||
|
||
function injectStyles(doc, id, css) {
|
||
if (doc.getElementById(id)) return;
|
||
var style = doc.createElement('style');
|
||
style.id = id;
|
||
style.textContent = css;
|
||
doc.head.appendChild(style);
|
||
}
|
||
|
||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||
|
||
function formatSize(bytes) {
|
||
if (bytes == null) return '';
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||
}
|
||
|
||
function formatDate(d) {
|
||
if (!d) return '';
|
||
var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
|
||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
||
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ── TIFF renderer ────────────────────────────────────────────────────────
|
||
|
||
var TIFF_CSS =
|
||
'.tiff-toolbar{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;' +
|
||
'background:#f5f5f5;border-bottom:1px solid #ddd;flex-wrap:wrap;font-size:.85rem;}' +
|
||
'.tiff-toolbar .tiff-btn{padding:.25rem .55rem;border:1px solid #ccc;border-radius:3px;' +
|
||
'background:#fff;cursor:pointer;font-size:.85rem;line-height:1;min-width:1.8rem;}' +
|
||
'.tiff-toolbar .tiff-btn:hover:not(:disabled){background:#e8e8e8;}' +
|
||
'.tiff-toolbar .tiff-btn:disabled{opacity:.4;cursor:default;}' +
|
||
'.tiff-toolbar .tiff-page-info{display:inline-flex;align-items:center;gap:.3rem;}' +
|
||
'.tiff-toolbar .tiff-page-input{width:3.2rem;padding:.2rem .3rem;border:1px solid #ccc;' +
|
||
'border-radius:3px;text-align:center;font-size:.85rem;}' +
|
||
'.tiff-toolbar .tiff-zoom-select{padding:.2rem .3rem;border:1px solid #ccc;border-radius:3px;' +
|
||
'background:#fff;font-size:.85rem;}' +
|
||
'.tiff-toolbar .tiff-spacer{flex:1;}' +
|
||
'.tiff-viewport{flex:1;overflow:auto;background:#525659;display:flex;align-items:flex-start;' +
|
||
'justify-content:center;padding:1rem;}' +
|
||
'.tiff-canvas{background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.4);display:block;' +
|
||
'image-rendering:auto;}' +
|
||
'.tiff-error{flex:1;display:flex;align-items:center;justify-content:center;color:#900;' +
|
||
'padding:2rem;text-align:center;}';
|
||
|
||
function renderTiff(doc, container, arrayBuffer, opts) {
|
||
opts = opts || {};
|
||
injectStyles(doc, 'zddc-tiff-styles', TIFF_CSS);
|
||
|
||
return loadLibrary('https://cdn.jsdelivr.net/npm/utif@3.1.0/UTIF.js').then(function () {
|
||
var ifds;
|
||
try {
|
||
ifds = window.UTIF.decode(arrayBuffer);
|
||
} catch (e) {
|
||
container.innerHTML = '<div class="tiff-error">Failed to parse TIFF: '
|
||
+ escapeHtml(e.message || e) + '</div>';
|
||
return;
|
||
}
|
||
if (!ifds || !ifds.length) {
|
||
container.innerHTML = '<div class="tiff-error">No images found in TIFF.</div>';
|
||
return;
|
||
}
|
||
|
||
// Reset container to a flex column
|
||
container.innerHTML = '';
|
||
container.style.display = 'flex';
|
||
container.style.flexDirection = 'column';
|
||
container.style.minHeight = '0';
|
||
container.style.height = '100%';
|
||
container.style.overflow = 'hidden';
|
||
|
||
// Toolbar
|
||
var toolbar = doc.createElement('div');
|
||
toolbar.className = 'tiff-toolbar';
|
||
|
||
var btnPrev = doc.createElement('button');
|
||
btnPrev.className = 'tiff-btn'; btnPrev.type = 'button';
|
||
btnPrev.title = 'Previous page'; btnPrev.textContent = '◀';
|
||
|
||
var pageInfo = doc.createElement('span');
|
||
pageInfo.className = 'tiff-page-info';
|
||
var pageInput = doc.createElement('input');
|
||
pageInput.type = 'number'; pageInput.min = '1'; pageInput.value = '1';
|
||
pageInput.className = 'tiff-page-input';
|
||
var pageOf = doc.createElement('span');
|
||
pageOf.textContent = ' of ' + ifds.length;
|
||
pageInfo.appendChild(doc.createTextNode('Page '));
|
||
pageInfo.appendChild(pageInput);
|
||
pageInfo.appendChild(pageOf);
|
||
|
||
var btnNext = doc.createElement('button');
|
||
btnNext.className = 'tiff-btn'; btnNext.type = 'button';
|
||
btnNext.title = 'Next page'; btnNext.textContent = '▶';
|
||
|
||
var spacer = doc.createElement('span');
|
||
spacer.className = 'tiff-spacer';
|
||
|
||
var btnZoomOut = doc.createElement('button');
|
||
btnZoomOut.className = 'tiff-btn'; btnZoomOut.type = 'button';
|
||
btnZoomOut.title = 'Zoom out'; btnZoomOut.textContent = '−';
|
||
|
||
var zoomSelect = doc.createElement('select');
|
||
zoomSelect.className = 'tiff-zoom-select';
|
||
var zoomOptions = [
|
||
['fit-width', 'Fit width'],
|
||
['fit-page', 'Fit page'],
|
||
['0.5', '50%'],
|
||
['0.75', '75%'],
|
||
['1', '100%'],
|
||
['1.25', '125%'],
|
||
['1.5', '150%'],
|
||
['2', '200%'],
|
||
['3', '300%'],
|
||
['4', '400%']
|
||
];
|
||
zoomOptions.forEach(function (z) {
|
||
var o = doc.createElement('option');
|
||
o.value = z[0]; o.textContent = z[1];
|
||
zoomSelect.appendChild(o);
|
||
});
|
||
zoomSelect.value = 'fit-width';
|
||
|
||
var btnZoomIn = doc.createElement('button');
|
||
btnZoomIn.className = 'tiff-btn'; btnZoomIn.type = 'button';
|
||
btnZoomIn.title = 'Zoom in'; btnZoomIn.textContent = '+';
|
||
|
||
toolbar.appendChild(btnPrev);
|
||
toolbar.appendChild(pageInfo);
|
||
toolbar.appendChild(btnNext);
|
||
toolbar.appendChild(spacer);
|
||
toolbar.appendChild(btnZoomOut);
|
||
toolbar.appendChild(zoomSelect);
|
||
toolbar.appendChild(btnZoomIn);
|
||
|
||
// Viewport with canvas
|
||
var viewport = doc.createElement('div');
|
||
viewport.className = 'tiff-viewport';
|
||
var canvas = doc.createElement('canvas');
|
||
canvas.className = 'tiff-canvas';
|
||
viewport.appendChild(canvas);
|
||
|
||
container.appendChild(toolbar);
|
||
container.appendChild(viewport);
|
||
|
||
// Render state
|
||
var currentPage = 0;
|
||
var zoom = 1;
|
||
var fitMode = 'width'; // 'width' | 'page' | null
|
||
var decoded = new Array(ifds.length);
|
||
|
||
function decodePage(i) {
|
||
if (decoded[i]) return decoded[i];
|
||
var ifd = ifds[i];
|
||
window.UTIF.decodeImage(arrayBuffer, ifd);
|
||
var rgba = window.UTIF.toRGBA8(ifd);
|
||
decoded[i] = { rgba: rgba, w: ifd.width, h: ifd.height };
|
||
return decoded[i];
|
||
}
|
||
|
||
function applyZoom() {
|
||
var page = decoded[currentPage];
|
||
if (!page) return;
|
||
var availW = viewport.clientWidth - 32; // padding
|
||
var availH = viewport.clientHeight - 32;
|
||
var scale;
|
||
if (fitMode === 'width') {
|
||
scale = availW / page.w;
|
||
} else if (fitMode === 'page') {
|
||
scale = Math.min(availW / page.w, availH / page.h);
|
||
} else {
|
||
scale = zoom;
|
||
}
|
||
if (!isFinite(scale) || scale <= 0) scale = 1;
|
||
canvas.style.width = (page.w * scale) + 'px';
|
||
canvas.style.height = (page.h * scale) + 'px';
|
||
}
|
||
|
||
function renderPage() {
|
||
var page;
|
||
try {
|
||
page = decodePage(currentPage);
|
||
} catch (e) {
|
||
container.innerHTML = '<div class="tiff-error">Failed to decode page '
|
||
+ (currentPage + 1) + ': ' + escapeHtml(e.message || e) + '</div>';
|
||
return;
|
||
}
|
||
canvas.width = page.w;
|
||
canvas.height = page.h;
|
||
var ctx = canvas.getContext('2d');
|
||
var imgData = ctx.createImageData(page.w, page.h);
|
||
imgData.data.set(page.rgba);
|
||
ctx.putImageData(imgData, 0, 0);
|
||
applyZoom();
|
||
pageInput.value = String(currentPage + 1);
|
||
btnPrev.disabled = currentPage <= 0;
|
||
btnNext.disabled = currentPage >= ifds.length - 1;
|
||
}
|
||
|
||
function setZoomFromSelect() {
|
||
var v = zoomSelect.value;
|
||
if (v === 'fit-width') { fitMode = 'width'; }
|
||
else if (v === 'fit-page') { fitMode = 'page'; }
|
||
else { fitMode = null; zoom = parseFloat(v) || 1; }
|
||
applyZoom();
|
||
}
|
||
|
||
function nudgeZoom(factor) {
|
||
if (fitMode) {
|
||
// capture current effective scale before leaving fit mode
|
||
var page = decoded[currentPage];
|
||
if (page) {
|
||
var availW = viewport.clientWidth - 32;
|
||
var availH = viewport.clientHeight - 32;
|
||
zoom = fitMode === 'width'
|
||
? availW / page.w
|
||
: Math.min(availW / page.w, availH / page.h);
|
||
} else {
|
||
zoom = 1;
|
||
}
|
||
fitMode = null;
|
||
}
|
||
zoom = Math.max(0.1, Math.min(8, zoom * factor));
|
||
// Match select option if any are close, else show as percent
|
||
var matched = false;
|
||
for (var i = 0; i < zoomSelect.options.length; i++) {
|
||
var ov = zoomSelect.options[i].value;
|
||
if (ov !== 'fit-width' && ov !== 'fit-page' && Math.abs(parseFloat(ov) - zoom) < 0.001) {
|
||
zoomSelect.value = ov; matched = true; break;
|
||
}
|
||
}
|
||
if (!matched) {
|
||
// Nearest standard step
|
||
var best = '1', bestDiff = Infinity;
|
||
for (var j = 0; j < zoomSelect.options.length; j++) {
|
||
var v2 = zoomSelect.options[j].value;
|
||
if (v2 === 'fit-width' || v2 === 'fit-page') continue;
|
||
var diff = Math.abs(parseFloat(v2) - zoom);
|
||
if (diff < bestDiff) { bestDiff = diff; best = v2; }
|
||
}
|
||
zoom = parseFloat(best);
|
||
zoomSelect.value = best;
|
||
}
|
||
applyZoom();
|
||
}
|
||
|
||
btnPrev.addEventListener('click', function () {
|
||
if (currentPage > 0) { currentPage--; renderPage(); }
|
||
});
|
||
btnNext.addEventListener('click', function () {
|
||
if (currentPage < ifds.length - 1) { currentPage++; renderPage(); }
|
||
});
|
||
pageInput.addEventListener('change', function () {
|
||
var n = parseInt(pageInput.value, 10);
|
||
if (!isNaN(n) && n >= 1 && n <= ifds.length) {
|
||
currentPage = n - 1;
|
||
renderPage();
|
||
} else {
|
||
pageInput.value = String(currentPage + 1);
|
||
}
|
||
});
|
||
zoomSelect.addEventListener('change', setZoomFromSelect);
|
||
btnZoomIn.addEventListener('click', function () { nudgeZoom(1.25); });
|
||
btnZoomOut.addEventListener('click', function () { nudgeZoom(1 / 1.25); });
|
||
|
||
// Keyboard nav (only when toolbar/viewport in focus path)
|
||
container.tabIndex = 0;
|
||
container.addEventListener('keydown', function (e) {
|
||
if (e.target === pageInput) return;
|
||
if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
|
||
if (currentPage > 0) { currentPage--; renderPage(); e.preventDefault(); }
|
||
} else if (e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') {
|
||
if (currentPage < ifds.length - 1) { currentPage++; renderPage(); e.preventDefault(); }
|
||
}
|
||
});
|
||
|
||
// Re-fit on viewport resize
|
||
if (typeof (doc.defaultView && doc.defaultView.ResizeObserver) === 'function') {
|
||
var ro = new doc.defaultView.ResizeObserver(function () { applyZoom(); });
|
||
ro.observe(viewport);
|
||
} else if (doc.defaultView) {
|
||
doc.defaultView.addEventListener('resize', function () { applyZoom(); });
|
||
}
|
||
|
||
renderPage();
|
||
});
|
||
}
|
||
|
||
// ── ZIP listing renderer ─────────────────────────────────────────────────
|
||
|
||
var ZIP_CSS =
|
||
'.zip-header{padding:.4rem .8rem;background:#f5f5f5;border-bottom:1px solid #ddd;' +
|
||
'font-size:.85rem;color:#444;}' +
|
||
'.zip-table-wrap{flex:1;overflow:auto;}' +
|
||
'.zip-table{width:100%;border-collapse:collapse;font-size:.85rem;font-family:' +
|
||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}' +
|
||
'.zip-table thead th{position:sticky;top:0;background:#f0f0f0;text-align:left;' +
|
||
'padding:.4rem .6rem;border-bottom:1px solid #ccc;cursor:pointer;user-select:none;' +
|
||
'font-weight:600;}' +
|
||
'.zip-table thead th:hover{background:#e6e6e6;}' +
|
||
'.zip-table thead th.zip-sort-asc::after{content:" ▲";font-size:.7rem;color:#888;}' +
|
||
'.zip-table thead th.zip-sort-desc::after{content:" ▼";font-size:.7rem;color:#888;}' +
|
||
'.zip-table tbody td{padding:.3rem .6rem;border-bottom:1px solid #eee;}' +
|
||
'.zip-table tbody tr:hover{background:#f6faff;}' +
|
||
'.zip-table .zip-folder{color:#888;}' +
|
||
'.zip-table .zip-name{color:#222;}' +
|
||
'.zip-table .zip-size,.zip-table .zip-date{font-variant-numeric:tabular-nums;' +
|
||
'white-space:nowrap;color:#555;}' +
|
||
'.zip-table .zip-col-size,.zip-table .zip-col-date{text-align:right;}' +
|
||
'.zip-empty{padding:2rem;text-align:center;color:#888;}';
|
||
|
||
function renderZipListing(doc, container, arrayBuffer, opts) {
|
||
opts = opts || {};
|
||
injectStyles(doc, 'zddc-zip-styles', ZIP_CSS);
|
||
|
||
return loadLibrary('https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js').then(function () {
|
||
return window.JSZip.loadAsync(arrayBuffer);
|
||
}).then(function (zip) {
|
||
var entries = [];
|
||
zip.forEach(function (relativePath, zipEntry) {
|
||
if (zipEntry.dir) return;
|
||
var size = (zipEntry._data && zipEntry._data.uncompressedSize) || 0;
|
||
entries.push({
|
||
path: relativePath,
|
||
name: relativePath.split('/').pop(),
|
||
size: size,
|
||
modified: zipEntry.date instanceof Date ? zipEntry.date : null
|
||
});
|
||
});
|
||
|
||
container.innerHTML = '';
|
||
container.style.display = 'flex';
|
||
container.style.flexDirection = 'column';
|
||
container.style.minHeight = '0';
|
||
container.style.height = '100%';
|
||
container.style.overflow = 'hidden';
|
||
|
||
var totalSize = entries.reduce(function (s, e) { return s + e.size; }, 0);
|
||
|
||
var header = doc.createElement('div');
|
||
header.className = 'zip-header';
|
||
header.textContent = entries.length + ' file' + (entries.length === 1 ? '' : 's')
|
||
+ (totalSize ? ' · ' + formatSize(totalSize) + ' uncompressed' : '');
|
||
container.appendChild(header);
|
||
|
||
if (!entries.length) {
|
||
var empty = doc.createElement('div');
|
||
empty.className = 'zip-empty';
|
||
empty.textContent = '(empty archive)';
|
||
container.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
var wrap = doc.createElement('div');
|
||
wrap.className = 'zip-table-wrap';
|
||
|
||
var table = doc.createElement('table');
|
||
table.className = 'zip-table';
|
||
var thead = doc.createElement('thead');
|
||
var trh = doc.createElement('tr');
|
||
var cols = [
|
||
{ key: 'path', label: 'Name', cls: 'zip-col-name' },
|
||
{ key: 'size', label: 'Size', cls: 'zip-col-size' },
|
||
{ key: 'modified', label: 'Modified', cls: 'zip-col-date' }
|
||
];
|
||
cols.forEach(function (c) {
|
||
var th = doc.createElement('th');
|
||
th.className = c.cls;
|
||
th.dataset.key = c.key;
|
||
th.textContent = c.label;
|
||
trh.appendChild(th);
|
||
});
|
||
thead.appendChild(trh);
|
||
table.appendChild(thead);
|
||
|
||
var tbody = doc.createElement('tbody');
|
||
table.appendChild(tbody);
|
||
|
||
wrap.appendChild(table);
|
||
container.appendChild(wrap);
|
||
|
||
var sortKey = 'path';
|
||
var sortDir = 1;
|
||
|
||
function render() {
|
||
var sorted = entries.slice().sort(function (a, b) {
|
||
var av, bv;
|
||
if (sortKey === 'size') { av = a.size; bv = b.size; }
|
||
else if (sortKey === 'modified') {
|
||
av = a.modified ? a.modified.getTime() : 0;
|
||
bv = b.modified ? b.modified.getTime() : 0;
|
||
} else {
|
||
av = a.path.toLowerCase(); bv = b.path.toLowerCase();
|
||
}
|
||
if (av < bv) return -1 * sortDir;
|
||
if (av > bv) return 1 * sortDir;
|
||
return 0;
|
||
});
|
||
|
||
tbody.innerHTML = '';
|
||
sorted.forEach(function (e) {
|
||
var tr = doc.createElement('tr');
|
||
var td1 = doc.createElement('td');
|
||
var slash = e.path.lastIndexOf('/');
|
||
if (slash >= 0) {
|
||
var folder = doc.createElement('span');
|
||
folder.className = 'zip-folder';
|
||
folder.textContent = e.path.substring(0, slash + 1);
|
||
td1.appendChild(folder);
|
||
}
|
||
var name = doc.createElement('span');
|
||
name.className = 'zip-name';
|
||
name.textContent = e.name;
|
||
td1.appendChild(name);
|
||
|
||
var td2 = doc.createElement('td');
|
||
td2.className = 'zip-size';
|
||
td2.textContent = formatSize(e.size);
|
||
|
||
var td3 = doc.createElement('td');
|
||
td3.className = 'zip-date';
|
||
td3.textContent = formatDate(e.modified);
|
||
|
||
tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3);
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
// Update sort arrows
|
||
var ths = thead.querySelectorAll('th');
|
||
for (var i = 0; i < ths.length; i++) {
|
||
ths[i].classList.remove('zip-sort-asc', 'zip-sort-desc');
|
||
if (ths[i].dataset.key === sortKey) {
|
||
ths[i].classList.add(sortDir > 0 ? 'zip-sort-asc' : 'zip-sort-desc');
|
||
}
|
||
}
|
||
}
|
||
|
||
thead.querySelectorAll('th').forEach(function (th) {
|
||
th.addEventListener('click', function () {
|
||
var k = th.dataset.key;
|
||
if (sortKey === k) sortDir = -sortDir;
|
||
else { sortKey = k; sortDir = 1; }
|
||
render();
|
||
});
|
||
});
|
||
|
||
render();
|
||
}).catch(function (err) {
|
||
container.innerHTML = '<div class="zip-empty">Failed to read ZIP: '
|
||
+ escapeHtml(err.message || err) + '</div>';
|
||
});
|
||
}
|
||
|
||
// ── Public API ───────────────────────────────────────────────────────────
|
||
|
||
if (!root.zddc) root.zddc = {};
|
||
root.zddc.preview = {
|
||
TIFF_EXTENSIONS: TIFF_EXTENSIONS,
|
||
IMAGE_EXTENSIONS: IMAGE_EXTENSIONS,
|
||
TEXT_EXTENSIONS: TEXT_EXTENSIONS,
|
||
OFFICE_EXTENSIONS: OFFICE_EXTENSIONS,
|
||
isTiff: isTiff,
|
||
isImage: isImage,
|
||
isText: isText,
|
||
isZip: isZip,
|
||
isOffice: isOffice,
|
||
loadLibrary: loadLibrary,
|
||
renderTiff: renderTiff,
|
||
renderZipListing: renderZipListing,
|
||
formatSize: formatSize,
|
||
formatDate: formatDate
|
||
};
|
||
})(typeof window !== 'undefined' ? window : this);
|
||
|
||
(function (global) {
|
||
'use strict';
|
||
|
||
if (global.transmittalApp) {
|
||
return;
|
||
}
|
||
|
||
const app = {
|
||
state: {
|
||
mode: 'edit',
|
||
published: false,
|
||
dirty: false
|
||
},
|
||
data: {
|
||
files: [],
|
||
selectedDirHandle: null,
|
||
selectedDirName: ''
|
||
},
|
||
constants: {
|
||
viewableExts: ['pdf', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'txt', 'html', 'htm', 'md'],
|
||
digestAlgorithm: 'SHA-256',
|
||
signatureAlgorithm: 'ECDSA-P256-SHA256',
|
||
SELF_HASH: 'self:payload-digest'
|
||
},
|
||
modules: {},
|
||
initCallbacks: [],
|
||
registerInit(fn) {
|
||
if (typeof fn === 'function') {
|
||
app.initCallbacks.push(fn);
|
||
}
|
||
},
|
||
dirtyListeners: [],
|
||
onDirty(fn) {
|
||
if (typeof fn === 'function') {
|
||
app.dirtyListeners.push(fn);
|
||
}
|
||
},
|
||
markDirty() {
|
||
app.state.dirty = true;
|
||
app.dirtyListeners.forEach(function (fn) {
|
||
try { fn(); } catch (err) { console.error('[transmittal] dirtyListener error', err); }
|
||
});
|
||
},
|
||
ready(fn) {
|
||
if (typeof fn !== 'function') {
|
||
return;
|
||
}
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', fn, { once: true });
|
||
} else {
|
||
fn();
|
||
}
|
||
},
|
||
start() {
|
||
if (app._started) {
|
||
return;
|
||
}
|
||
app._started = true;
|
||
const run = function () {
|
||
app.initCallbacks.forEach(function (fn) {
|
||
try {
|
||
fn();
|
||
} catch (err) {
|
||
console.error('[transmittal] init error', err);
|
||
}
|
||
});
|
||
};
|
||
app.ready(run);
|
||
}
|
||
};
|
||
|
||
global.transmittalApp = app;
|
||
})(window);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
/**
|
||
* Creates a reactive state object using Proxy.
|
||
* When properties change, all registered subscribers are notified.
|
||
*
|
||
* @param {Object} initialState - Initial state values
|
||
* @returns {Object} Reactive state proxy with subscribe/unsubscribe methods
|
||
*/
|
||
function createReactiveState(initialState) {
|
||
const subscribers = new Set();
|
||
const state = { ...initialState };
|
||
|
||
const handler = {
|
||
get(target, property) {
|
||
// Expose subscription methods
|
||
if (property === 'subscribe') {
|
||
return function(callback) {
|
||
if (typeof callback === 'function') {
|
||
subscribers.add(callback);
|
||
}
|
||
return function unsubscribe() {
|
||
subscribers.delete(callback);
|
||
};
|
||
};
|
||
}
|
||
|
||
if (property === 'unsubscribeAll') {
|
||
return function() {
|
||
subscribers.clear();
|
||
};
|
||
}
|
||
|
||
// Return the actual property value
|
||
return target[property];
|
||
},
|
||
|
||
set(target, property, value) {
|
||
const oldValue = target[property];
|
||
|
||
// Only notify if value actually changed
|
||
if (oldValue !== value) {
|
||
target[property] = value;
|
||
|
||
// Notify all subscribers
|
||
subscribers.forEach(function(callback) {
|
||
try {
|
||
callback(property, value, oldValue);
|
||
} catch (err) {
|
||
console.error('[reactive] Subscriber error:', err);
|
||
}
|
||
});
|
||
}
|
||
|
||
return true;
|
||
}
|
||
};
|
||
|
||
return new Proxy(state, handler);
|
||
}
|
||
|
||
app.createReactiveState = createReactiveState;
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom = app.dom || {};
|
||
|
||
dom.qs = function (selector) {
|
||
return document.querySelector(selector);
|
||
};
|
||
|
||
dom.qsa = function (selector) {
|
||
return Array.from(document.querySelectorAll(selector));
|
||
};
|
||
|
||
dom.show = function (element, shouldShow) {
|
||
if (!element) {
|
||
return;
|
||
}
|
||
element.hidden = !shouldShow;
|
||
};
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const util = app.util = app.util || {};
|
||
|
||
util.hasCrypto = function hasCrypto() {
|
||
return !!(window.crypto && window.crypto.subtle && typeof window.crypto.subtle.digest === 'function');
|
||
};
|
||
|
||
util.canonicalStringify = function canonicalStringify(input) {
|
||
if (input === null) {
|
||
return 'null';
|
||
}
|
||
const type = typeof input;
|
||
if (type === 'number' || type === 'boolean' || type === 'string') {
|
||
return JSON.stringify(input);
|
||
}
|
||
if (Array.isArray(input)) {
|
||
return '[' + input.map(util.canonicalStringify).join(',') + ']';
|
||
}
|
||
const keys = Object.keys(input).sort();
|
||
return '{' + keys.map(function (key) {
|
||
return JSON.stringify(key) + ':' + util.canonicalStringify(input[key]);
|
||
}).join(',') + '}';
|
||
};
|
||
|
||
util.hashString = function hashString(str) {
|
||
return zddc.crypto.sha256String(str);
|
||
};
|
||
|
||
util.arrayBufferToHex = function arrayBufferToHex(buffer) {
|
||
return zddc.crypto.bytesToHex(buffer);
|
||
};
|
||
|
||
util.base64ToArrayBuffer = function base64ToArrayBuffer(base64Value) {
|
||
if (!base64Value) {
|
||
return new ArrayBuffer(0);
|
||
}
|
||
const binaryString = atob(base64Value);
|
||
const length = binaryString.length;
|
||
const bytes = new Uint8Array(length);
|
||
for (let index = 0; index < length; index += 1) {
|
||
bytes[index] = binaryString.charCodeAt(index);
|
||
}
|
||
return bytes.buffer;
|
||
};
|
||
|
||
util.arrayBufferToBase64 = function arrayBufferToBase64(buffer) {
|
||
const bytes = new Uint8Array(buffer);
|
||
let output = '';
|
||
for (let index = 0; index < bytes.length; index += 1) {
|
||
output += String.fromCharCode(bytes[index]);
|
||
}
|
||
return btoa(output);
|
||
};
|
||
|
||
// Revision comparison delegates to shared zddc library
|
||
util.compareRevisionPriority = function compareRevisionPriority(aRevision, bRevision) {
|
||
return zddc.compareRevisions(aRevision, bRevision);
|
||
};
|
||
|
||
util.compareFilesByTrackingRevision = function compareFilesByTrackingRevision(a, b) {
|
||
const trackingA = (a && a.trackingNumber ? String(a.trackingNumber) : '').toLowerCase();
|
||
const trackingB = (b && b.trackingNumber ? String(b.trackingNumber) : '').toLowerCase();
|
||
if (trackingA < trackingB) {
|
||
return -1;
|
||
}
|
||
if (trackingA > trackingB) {
|
||
return 1;
|
||
}
|
||
|
||
const revisionCompare = util.compareRevisionPriority(a && a.revision, b && b.revision);
|
||
if (revisionCompare !== 0) {
|
||
return revisionCompare;
|
||
}
|
||
|
||
const extA = (a && a.extension ? String(a.extension) : '').toLowerCase();
|
||
const extB = (b && b.extension ? String(b.extension) : '').toLowerCase();
|
||
if (extA < extB) {
|
||
return -1;
|
||
}
|
||
if (extA > extB) {
|
||
return 1;
|
||
}
|
||
|
||
return 0;
|
||
};
|
||
|
||
util.canonicalizePublicJwk = function canonicalizePublicJwk(pk) {
|
||
if (!pk) {
|
||
return { kty: 'EC', crv: 'P-256', x: '', y: '' };
|
||
}
|
||
return {
|
||
kty: pk.kty || 'EC',
|
||
crv: pk.crv || 'P-256',
|
||
x: pk.x || '',
|
||
y: pk.y || ''
|
||
};
|
||
};
|
||
|
||
util.publicKeyFingerprint = async function publicKeyFingerprint(pk) {
|
||
try {
|
||
if (!pk) {
|
||
return '';
|
||
}
|
||
if (!util.hasCrypto()) {
|
||
return null;
|
||
}
|
||
const canonical = util.canonicalizePublicJwk(pk);
|
||
const canonicalStr = util.canonicalStringify(canonical);
|
||
const hash = await util.hashString(canonicalStr);
|
||
return (hash || '').slice(0, 12);
|
||
} catch (err) {
|
||
console.error('[transmittal] publicKeyFingerprint error', err);
|
||
return '';
|
||
}
|
||
};
|
||
|
||
util.hashFile = function hashFile(file, onProgress) {
|
||
return zddc.crypto.sha256File(file, onProgress);
|
||
};
|
||
|
||
util.escapeHtml = function escapeHtml(value) {
|
||
return String(value || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
};
|
||
|
||
util.escapeHtmlAttribute = function escapeHtmlAttribute(value) {
|
||
return String(value || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/"/g, '"');
|
||
};
|
||
|
||
util.downloadBlob = function downloadBlob(filename, contents, mime) {
|
||
var blob = (contents instanceof Blob)
|
||
? contents
|
||
: new Blob([contents], { type: mime || 'application/octet-stream' });
|
||
var anchor = document.createElement('a');
|
||
anchor.href = URL.createObjectURL(blob);
|
||
anchor.download = filename;
|
||
document.body.appendChild(anchor);
|
||
anchor.click();
|
||
setTimeout(function () {
|
||
URL.revokeObjectURL(anchor.href);
|
||
anchor.remove();
|
||
}, 0);
|
||
};
|
||
|
||
util.createEmptyData = function createEmptyData(date) {
|
||
return {
|
||
envelope: {
|
||
version: 1,
|
||
digestAlgorithm: app.constants.digestAlgorithm,
|
||
digest: '',
|
||
digestedAt: '',
|
||
signatureAlgorithm: app.constants.signatureAlgorithm,
|
||
signatures: []
|
||
},
|
||
payload: {
|
||
version: 1,
|
||
type: 'Transmittal',
|
||
title: '',
|
||
client: '',
|
||
project: '',
|
||
projectNumber: '',
|
||
date: date || '',
|
||
trackingNumber: '',
|
||
from: '',
|
||
to: '',
|
||
purpose: '',
|
||
responseDue: '',
|
||
subject: '',
|
||
remarks: '',
|
||
files: []
|
||
},
|
||
presentation: {
|
||
leftLogo: '',
|
||
rightLogo: '',
|
||
theme: 'default',
|
||
customCss: ''
|
||
}
|
||
};
|
||
};
|
||
|
||
util.fetchTrustedTime = function fetchTrustedTime() {
|
||
return new Date().toISOString();
|
||
};
|
||
|
||
util.formatISOWithTZ = function formatISOWithTZ(isoStr) {
|
||
if (!isoStr) { return 'Unknown'; }
|
||
var d = new Date(isoStr);
|
||
if (isNaN(d.getTime())) { return isoStr; }
|
||
try {
|
||
return d.toLocaleDateString('en-US', {
|
||
weekday: 'long',
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
hour: 'numeric',
|
||
minute: '2-digit',
|
||
timeZoneName: 'short'
|
||
});
|
||
} catch (_) {
|
||
return d.toLocaleString();
|
||
}
|
||
};
|
||
|
||
util.signEnvelope = async function signEnvelope(envelope, jwk) {
|
||
if (!jwk || jwk.kty !== 'EC') {
|
||
throw new Error('A valid EC private key (JWK) is required to sign.');
|
||
}
|
||
var envelopeToSign = {
|
||
version: envelope.version || 1,
|
||
digestAlgorithm: envelope.digestAlgorithm || app.constants.digestAlgorithm,
|
||
digest: envelope.digest,
|
||
digestedAt: envelope.digestedAt,
|
||
signatureAlgorithm: envelope.signatureAlgorithm || app.constants.signatureAlgorithm
|
||
};
|
||
var envelopeStr = util.canonicalStringify(envelopeToSign);
|
||
var key = await window.crypto.subtle.importKey(
|
||
'jwk',
|
||
jwk,
|
||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||
false,
|
||
['sign']
|
||
);
|
||
var raw = await window.crypto.subtle.sign(
|
||
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
||
key,
|
||
new TextEncoder().encode(envelopeStr)
|
||
);
|
||
return {
|
||
signature: util.arrayBufferToBase64(raw),
|
||
signedAt: util.fetchTrustedTime()
|
||
};
|
||
};
|
||
|
||
|
||
util.formatShortHash = function formatShortHash(hex) {
|
||
const normalized = (hex || '').trim();
|
||
if (!normalized) {
|
||
return '';
|
||
}
|
||
if (normalized.length <= 20) {
|
||
return normalized;
|
||
}
|
||
return normalized.slice(0, 12) + '\u2026' + normalized.slice(-8);
|
||
};
|
||
|
||
util.formatShortFileHash = function formatShortFileHash(hex) {
|
||
const normalized = (hex || '').trim();
|
||
if (!normalized) {
|
||
return '';
|
||
}
|
||
if (normalized.length <= 12) {
|
||
return normalized;
|
||
}
|
||
return normalized.slice(0, 6) + '\u2026' + normalized.slice(-5);
|
||
};
|
||
|
||
util.formatFileSize = function formatFileSize(bytes) {
|
||
const num = Number(bytes) || 0;
|
||
if (num === 0) {
|
||
return '0 B';
|
||
}
|
||
if (num < 1024) {
|
||
return num + ' B';
|
||
}
|
||
if (num < 1024 * 1024) {
|
||
return (num / 1024).toFixed(1) + ' KB';
|
||
}
|
||
if (num < 1024 * 1024 * 1024) {
|
||
return (num / (1024 * 1024)).toFixed(1) + ' MB';
|
||
}
|
||
return (num / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||
};
|
||
|
||
util.fileToImage = function fileToImage(file) {
|
||
return new Promise(function (resolve, reject) {
|
||
const url = URL.createObjectURL(file);
|
||
const img = new Image();
|
||
img.onload = function () {
|
||
URL.revokeObjectURL(url);
|
||
resolve(img);
|
||
};
|
||
img.onerror = function (err) {
|
||
URL.revokeObjectURL(url);
|
||
reject(err);
|
||
};
|
||
img.src = url;
|
||
});
|
||
};
|
||
|
||
util.imageFileToPngDataUrl = async function imageFileToPngDataUrl(file, maxWidth, maxHeight) {
|
||
const img = await util.fileToImage(file);
|
||
const scale = Math.min(1, maxWidth / img.width, maxHeight / img.height);
|
||
const w = Math.round(img.width * scale);
|
||
const h = Math.round(img.height * scale);
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = w;
|
||
canvas.height = h;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0, w, h);
|
||
return canvas.toDataURL('image/png');
|
||
};
|
||
|
||
// ── ZDDC Signing Key utilities ─────────────────────────
|
||
var KEY_FORMAT = 'zddc-signing-key-v1';
|
||
var KDF_ITERATIONS = 100000;
|
||
|
||
function deriveWrappingKey(password, salt) {
|
||
var enc = new TextEncoder();
|
||
return window.crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'])
|
||
.then(function (baseKey) {
|
||
return window.crypto.subtle.deriveKey(
|
||
{ name: 'PBKDF2', salt: salt, iterations: KDF_ITERATIONS, hash: 'SHA-256' },
|
||
baseKey,
|
||
{ name: 'AES-GCM', length: 256 },
|
||
false,
|
||
['encrypt', 'decrypt']
|
||
);
|
||
});
|
||
}
|
||
|
||
util.encryptPrivateKey = async function encryptPrivateKey(jwk, password, publicFingerprint) {
|
||
var plaintext = new TextEncoder().encode(JSON.stringify(jwk));
|
||
var salt = window.crypto.getRandomValues(new Uint8Array(16));
|
||
var iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||
var wrappingKey = await deriveWrappingKey(password, salt);
|
||
var ciphertext = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, wrappingKey, plaintext);
|
||
return {
|
||
format: KEY_FORMAT,
|
||
publicFingerprint: publicFingerprint || '',
|
||
encrypted: true,
|
||
kdf: 'PBKDF2-SHA256',
|
||
iterations: KDF_ITERATIONS,
|
||
salt: util.arrayBufferToBase64(salt),
|
||
iv: util.arrayBufferToBase64(iv),
|
||
ciphertext: util.arrayBufferToBase64(ciphertext)
|
||
};
|
||
};
|
||
|
||
util.decryptPrivateKey = async function decryptPrivateKey(keyData, password) {
|
||
var salt = util.base64ToArrayBuffer(keyData.salt);
|
||
var iv = util.base64ToArrayBuffer(keyData.iv);
|
||
var ciphertext = util.base64ToArrayBuffer(keyData.ciphertext);
|
||
var wrappingKey = await deriveWrappingKey(password, salt);
|
||
var plaintext = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, wrappingKey, ciphertext);
|
||
return JSON.parse(new TextDecoder().decode(plaintext));
|
||
};
|
||
|
||
util.wrapKeyFile = function wrapKeyFile(jwk, publicFingerprint) {
|
||
return {
|
||
format: KEY_FORMAT,
|
||
publicFingerprint: publicFingerprint || '',
|
||
encrypted: false,
|
||
key: jwk
|
||
};
|
||
};
|
||
|
||
util.loadKeyFile = async function loadKeyFile(text, promptForPassword) {
|
||
var data = JSON.parse(text);
|
||
// ZDDC key format
|
||
if (data.format === KEY_FORMAT) {
|
||
if (data.encrypted) {
|
||
if (typeof promptForPassword !== 'function') {
|
||
throw new Error('Password required but no prompt available.');
|
||
}
|
||
var password = await promptForPassword(data.publicFingerprint);
|
||
if (!password && password !== '') { return null; } // user cancelled
|
||
return util.decryptPrivateKey(data, password);
|
||
}
|
||
return data.key;
|
||
}
|
||
throw new Error('Unrecognized key file format. Expected a .zddc-key file.');
|
||
};
|
||
|
||
util.cloneDocumentHtml = function cloneDocumentHtml() {
|
||
// Temporarily remove inline scripts from the LIVE DOM so that
|
||
// outerHTML produces a clean string with zero script bodies.
|
||
// We never create a clone via innerHTML because the HTML parser
|
||
// would corrupt the JSON block if base64 data contains a close-script tag.
|
||
var scripts = document.querySelectorAll('script:not([src])');
|
||
var saved = [];
|
||
for (var i = 0; i < scripts.length; i++) {
|
||
var el = scripts[i];
|
||
saved.push({
|
||
el: el,
|
||
parent: el.parentNode,
|
||
next: el.nextSibling,
|
||
body: el.textContent || '',
|
||
attrs: ''
|
||
});
|
||
for (var a = 0; a < el.attributes.length; a++) {
|
||
var attr = el.attributes[a];
|
||
saved[i].attrs += ' ' + attr.name + '="' +
|
||
attr.value.replace(/&/g, '&').replace(/"/g, '"') + '"';
|
||
}
|
||
el.parentNode.removeChild(el);
|
||
}
|
||
|
||
var html = document.documentElement.outerHTML;
|
||
|
||
// Restore every script element to the live DOM immediately
|
||
for (var r = 0; r < saved.length; r++) {
|
||
saved[r].parent.insertBefore(saved[r].el, saved[r].next);
|
||
}
|
||
|
||
// Build each script tag as a safe string and insert before </body>
|
||
var scriptStrings = '';
|
||
for (var j = 0; j < saved.length; j++) {
|
||
var s = saved[j];
|
||
var safeBody = s.body;
|
||
var scriptType = (s.el.getAttribute('type') || '').toLowerCase();
|
||
if (scriptType === 'application/json') {
|
||
// Re-serialize from parsed data so we control the output.
|
||
// \u003c is valid JSON; JSON.parse converts it back to <
|
||
var jsonData = app.json.parse();
|
||
safeBody = JSON.stringify(jsonData, null, 2).replace(/</g, '\\u003c');
|
||
}
|
||
scriptStrings += '\n<script' + s.attrs + '>' + safeBody +
|
||
'</' + 'script>';
|
||
}
|
||
|
||
// Insert scripts before closing </body>
|
||
var bodyClose = html.lastIndexOf('</' + 'body>');
|
||
if (bodyClose !== -1) {
|
||
html = html.substring(0, bodyClose) + scriptStrings + '\n' +
|
||
html.substring(bodyClose);
|
||
} else {
|
||
html += scriptStrings;
|
||
}
|
||
|
||
return '<!DOCTYPE html>\n' + html;
|
||
};
|
||
|
||
/**
|
||
* Fetch the current page's own source HTML and replace only the
|
||
* transmittal-data JSON block with the supplied data object.
|
||
*
|
||
* This is the preferred save mechanism for drafts because it produces
|
||
* an exact copy of the source file with only the data changed, rather
|
||
* than a DOM snapshot that may contain stale or mutated content.
|
||
*
|
||
* @param {object} jsonData - The data object to embed as JSON.
|
||
* @returns {Promise<string>} The patched HTML string.
|
||
* @throws {Error} If the fetch fails or the JSON block is not found.
|
||
*/
|
||
util.fetchAndPatchHtml = async function fetchAndPatchHtml(jsonData) {
|
||
var response = await fetch(location.href, { cache: 'no-cache' });
|
||
if (!response.ok) {
|
||
throw new Error('fetch failed with status ' + response.status);
|
||
}
|
||
var html = await response.text();
|
||
// \u003c is valid JSON; JSON.parse converts it back to <
|
||
var jsonStr = JSON.stringify(jsonData, null, 2).replace(/</g, '\\u003c');
|
||
// Replace the transmittal-data script body. The non-greedy [\s\S]*? stops
|
||
// at the first close-script tag, which is safe because < is escaped above.
|
||
var patched = html.replace(
|
||
new RegExp(
|
||
'(<script\\b[^>]*\\bid\\s*=\\s*["\']transmittal-data["\'][^>]*>)[\\s\\S]*?(<\\/' + 'script>)',
|
||
'i'
|
||
),
|
||
'$1\n' + jsonStr + '\n$2'
|
||
);
|
||
if (patched === html) {
|
||
throw new Error('transmittal-data script block not found in fetched HTML');
|
||
}
|
||
return patched;
|
||
};
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const json = app.json = app.json || {};
|
||
|
||
const SCRIPT_ID = 'transmittal-data';
|
||
|
||
json.getScriptElement = function getScriptElement() {
|
||
return document.getElementById(SCRIPT_ID);
|
||
};
|
||
|
||
json.getRawText = function getRawText() {
|
||
const el = json.getScriptElement();
|
||
return el && typeof el.textContent === 'string' ? el.textContent : '';
|
||
};
|
||
|
||
json.setData = function setData(obj) {
|
||
const el = json.getScriptElement();
|
||
if (!el) {
|
||
return;
|
||
}
|
||
try {
|
||
el.textContent = JSON.stringify(obj, null, 2);
|
||
} catch (err) {
|
||
console.error('[transmittal] failed to serialize JSON', err);
|
||
}
|
||
};
|
||
|
||
json.parse = function parse() {
|
||
try {
|
||
const raw = json.getRawText();
|
||
if (raw && raw.trim().length) {
|
||
return JSON.parse(raw);
|
||
}
|
||
} catch (err) {
|
||
console.error('[transmittal] failed to parse JSON store', err);
|
||
}
|
||
return app.util.createEmptyData('');
|
||
};
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
const json = app.json;
|
||
const util = app.util;
|
||
|
||
/**
|
||
* Hydrates static HTML content with data from JSON.
|
||
* Called on page load to hide static placeholders and show dynamic content.
|
||
*/
|
||
function hydrate() {
|
||
// Hide static "Not Validated" warning (will be replaced by dynamic validation)
|
||
const staticWarning = dom.qs('#signature-status-static');
|
||
if (staticWarning) {
|
||
staticWarning.hidden = true;
|
||
}
|
||
|
||
// Digest will be populated by security.renderSignaturesList()
|
||
// which is called after signature verification
|
||
}
|
||
|
||
/**
|
||
* Populates static HTML before saving/publishing.
|
||
* This ensures the page displays content even without JavaScript.
|
||
*/
|
||
async function populateStatic() {
|
||
const data = json.parse();
|
||
const envelope = data.envelope || {};
|
||
const payload = data.payload || {};
|
||
const signatures = Array.isArray(envelope.signatures) ? envelope.signatures : [];
|
||
const files = Array.isArray(payload.files) ? payload.files : [];
|
||
|
||
// Populate all form fields with actual values
|
||
const fields = {
|
||
'#type': payload.type || 'Transmittal',
|
||
'#title': payload.title || '',
|
||
'#owner-name': payload.client || '',
|
||
'#project-name': payload.project || '',
|
||
'#project-number': payload.projectNumber || '',
|
||
'#tracking-number': payload.trackingNumber || '',
|
||
'#date': payload.date || '',
|
||
'#from': payload.from || '',
|
||
'#to': payload.to || '',
|
||
'#purpose': payload.purpose || '',
|
||
'#response-due': payload.responseDue || '',
|
||
'#subject': payload.subject || '',
|
||
'#remarks': payload.remarks || ''
|
||
};
|
||
|
||
Object.keys(fields).forEach(function(selector) {
|
||
const el = dom.qs(selector);
|
||
if (el) {
|
||
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
||
el.value = fields[selector];
|
||
el.setAttribute('value', fields[selector]);
|
||
} else {
|
||
el.textContent = fields[selector];
|
||
}
|
||
}
|
||
});
|
||
var typeDisp = dom.qs('#type-display');
|
||
if (typeDisp) { typeDisp.textContent = fields['#type']; }
|
||
|
||
// Render markdown for static display
|
||
if (app.modules.markdown && payload.remarks) {
|
||
const remarksRender = dom.qs('#remarks-render');
|
||
if (remarksRender) {
|
||
remarksRender.innerHTML = app.modules.markdown.render(payload.remarks);
|
||
}
|
||
}
|
||
|
||
// Populate table with file data
|
||
const SELF_HASH = app.constants.SELF_HASH;
|
||
const tbody = dom.qs('.table-wrapper tbody');
|
||
if (tbody) {
|
||
let html = '';
|
||
let rowNum = 0;
|
||
// Separate self-entry from regular files
|
||
var selfFile = null;
|
||
var regularFiles = [];
|
||
files.forEach(function(file) {
|
||
if (file.sha256 === SELF_HASH) {
|
||
selfFile = file;
|
||
} else {
|
||
regularFiles.push(file);
|
||
}
|
||
});
|
||
// If no explicit self-entry in JSON, synthesize one from payload header
|
||
if (!selfFile) {
|
||
selfFile = {
|
||
trackingNumber: payload.trackingNumber || '',
|
||
title: payload.subject || payload.title || '',
|
||
revision: payload.date || '',
|
||
status: payload.purpose || '',
|
||
extension: 'html',
|
||
fileSize: 0,
|
||
sha256: SELF_HASH
|
||
};
|
||
}
|
||
// Build self-link filename from payload fields
|
||
var selfFilename = '';
|
||
var dataModule = app.modules.data;
|
||
if (dataModule && dataModule.buildFileName) {
|
||
selfFilename = dataModule.buildFileName(payload, { extension: 'html' });
|
||
}
|
||
var selfHref = selfFilename ? encodeURI('./' + selfFilename) : '';
|
||
var selfTrackingHtml = selfHref
|
||
? '<a href="' + selfHref + '" class="text-gray-500 hover:underline" target="_blank" rel="noopener">' + util.escapeHtml(selfFile.trackingNumber || '') + '</a>'
|
||
: util.escapeHtml(selfFile.trackingNumber || '');
|
||
// Row 0: self-entry
|
||
html += '<tr class="self-entry">' +
|
||
'<td class="px-2 py-1 text-center text-gray-400">0</td>' +
|
||
'<td class="px-2 py-1 text-gray-500 font-mono">' + selfTrackingHtml + '</td>' +
|
||
'<td class="px-2 py-1 text-gray-500">' + util.escapeHtml(selfFile.title || '') + '</td>' +
|
||
'<td class="px-2 py-1 text-center text-gray-500 font-mono">' + util.escapeHtml(selfFile.revision || '') + '</td>' +
|
||
'<td class="px-2 py-1 text-center text-gray-500">' + util.escapeHtml(selfFile.status || '') + '</td>' +
|
||
'<td class="px-2 py-1 text-center text-gray-500">html</td>' +
|
||
'<td class="px-2 py-1 text-right text-gray-400">\u2014</td>' +
|
||
'<td class="px-2 py-1 font-mono text-[9px] text-gray-400 italic">see above</td>' +
|
||
'</tr>';
|
||
// Remaining files
|
||
regularFiles.forEach(function(file) {
|
||
rowNum++;
|
||
var isUnmatched = !file.sha256 && !file.fileSize;
|
||
var formattedSize = isUnmatched ? '\u2014' : (file.fileSize ? util.formatFileSize(file.fileSize) : '');
|
||
var sizeClass = isUnmatched ? 'px-2 py-1 text-right text-gray-400' : 'px-2 py-1 text-right';
|
||
var hashContent = isUnmatched ? '<span class="italic text-gray-400">pending</span>' : (file.sha256 ? util.formatShortFileHash(file.sha256) : '');
|
||
html += '<tr>' +
|
||
'<td class="px-2 py-1 text-center text-gray-400">' + rowNum + '</td>' +
|
||
'<td class="px-2 py-1">' + (file.trackingNumber || '') + '</td>' +
|
||
'<td class="px-2 py-1">' + (file.title || '') + '</td>' +
|
||
'<td class="px-2 py-1 text-center">' + (file.revision || '') + '</td>' +
|
||
'<td class="px-2 py-1 text-center">' + (file.status || '') + '</td>' +
|
||
'<td class="px-2 py-1 text-center">' + (file.extension || '') + '</td>' +
|
||
'<td class="' + sizeClass + '">' + formattedSize + '</td>' +
|
||
'<td class="px-2 py-1 font-mono text-[9px]">' + hashContent + '</td>' +
|
||
'</tr>';
|
||
});
|
||
tbody.innerHTML = html || '<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
|
||
}
|
||
|
||
// Populate digest card (static fallback using verify-card format)
|
||
const digestDisplay = dom.qs('#digest-display');
|
||
if (digestDisplay && envelope.digest) {
|
||
const digestedAt = envelope.digestedAt ? new Date(envelope.digestedAt).toLocaleString() : 'Unknown';
|
||
digestDisplay.innerHTML =
|
||
'<div class="verify-card verify-card--info">' +
|
||
'<div class="verify-card__status verify-card__status--info">Digest (SHA-256)</div>' +
|
||
'<div class="verify-card__detail"><code>' + envelope.digest + '</code></div>' +
|
||
'<div class="verify-card__detail">' + digestedAt + '</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
// Populate digest in Advanced section
|
||
const digestEl = dom.qs('#digest-info');
|
||
if (digestEl && envelope.digest) {
|
||
const digestedAt = envelope.digestedAt ? new Date(envelope.digestedAt).toLocaleString() : 'Unknown';
|
||
digestEl.innerHTML = '<div class="flex flex-col gap-0.5">' +
|
||
'<div><strong>Digest (SHA-256):</strong></div>' +
|
||
'<code class="text-[9px] break-all bg-gray-100 px-1 py-0.5 rounded">' + envelope.digest + '</code>' +
|
||
'<div class="text-gray-500">Created: ' + digestedAt + '</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
// Populate static signature cards
|
||
const sigList = dom.qs('#signatures-list');
|
||
if (sigList && signatures.length > 0) {
|
||
let html = '';
|
||
for (let i = 0; i < signatures.length; i++) {
|
||
const sig = signatures[i];
|
||
const fingerprint = await util.publicKeyFingerprint(sig.publicKeyJwk);
|
||
const fpDisplay = fingerprint === null ? 'unavailable' : (fingerprint || 'Unknown');
|
||
const signedAt = sig.signedAt ? new Date(sig.signedAt).toLocaleString() : 'Unknown';
|
||
html += '<div class="verify-card verify-card--info">' +
|
||
'<div class="verify-card__status verify-card__status--info">Signature ' + (i + 1) + '</div>' +
|
||
'<div class="verify-card__detail">Key: <code>' + fpDisplay + '</code></div>' +
|
||
'<div class="verify-card__detail">' + signedAt + '</div>' +
|
||
'</div>';
|
||
}
|
||
sigList.innerHTML = html;
|
||
}
|
||
|
||
// Show static warning
|
||
const staticWarning = dom.qs('#signature-status-static');
|
||
if (staticWarning) {
|
||
staticWarning.hidden = !(envelope.digest || signatures.length > 0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Populates form fields from current JSON data at runtime.
|
||
* Used by verification mode to hydrate the form with loaded data.
|
||
*/
|
||
function hydrateForm() {
|
||
const data = json.parse();
|
||
const payload = data.payload || {};
|
||
const files = Array.isArray(payload.files) ? payload.files : [];
|
||
|
||
var fields = {
|
||
'#type': payload.type || 'Transmittal',
|
||
'#title': payload.title || '',
|
||
'#owner-name': payload.client || '',
|
||
'#project-name': payload.project || '',
|
||
'#project-number': payload.projectNumber || '',
|
||
'#tracking-number': payload.trackingNumber || '',
|
||
'#date': payload.date || '',
|
||
'#from': payload.from || '',
|
||
'#to': payload.to || '',
|
||
'#purpose': payload.purpose || '',
|
||
'#response-due': payload.responseDue || '',
|
||
'#subject': payload.subject || '',
|
||
'#remarks': payload.remarks || ''
|
||
};
|
||
|
||
Object.keys(fields).forEach(function (selector) {
|
||
var el = dom.qs(selector);
|
||
if (el) {
|
||
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
||
el.value = fields[selector];
|
||
} else {
|
||
el.textContent = fields[selector];
|
||
}
|
||
}
|
||
});
|
||
var typeDisp2 = dom.qs('#type-display');
|
||
if (typeDisp2) { typeDisp2.textContent = fields['#type']; }
|
||
|
||
// Render markdown
|
||
if (app.modules.markdown && payload.remarks) {
|
||
var remarksRender = dom.qs('#remarks-render');
|
||
if (remarksRender) {
|
||
remarksRender.innerHTML = app.modules.markdown.render(payload.remarks);
|
||
}
|
||
}
|
||
|
||
// Render email fields
|
||
if (app.modules.emailTags) {
|
||
if (app.modules.emailTags.render) { app.modules.emailTags.render(); }
|
||
if (app.modules.emailTags.renderFrom) { app.modules.emailTags.renderFrom(); }
|
||
}
|
||
|
||
// Visibility
|
||
if (app.modules.visibility && app.modules.visibility.applyFieldVisibility) {
|
||
app.modules.visibility.applyFieldVisibility();
|
||
}
|
||
}
|
||
|
||
app.modules.hydrate = {
|
||
hydrate: hydrate,
|
||
hydrateForm: hydrateForm,
|
||
populateStatic: populateStatic
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
hydrate();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
|
||
// Convert existing state to reactive state
|
||
if (app.createReactiveState) {
|
||
const oldState = app.state;
|
||
app.state = app.createReactiveState({
|
||
mode: oldState.mode || 'edit',
|
||
published: oldState.published || false,
|
||
dirty: oldState.dirty || false
|
||
});
|
||
|
||
// Subscribe to state changes to automatically update UI
|
||
app.state.subscribe(function(property, newValue, oldValue) {
|
||
// Auto-apply state changes
|
||
if (property === 'mode' || property === 'published') {
|
||
state.updateHiddenFields();
|
||
state.apply();
|
||
// Sync preview checkbox to current mode
|
||
if (app.modules.files && app.modules.files.syncPreviewCheckbox) {
|
||
app.modules.files.syncPreviewCheckbox();
|
||
}
|
||
}
|
||
|
||
if (property === 'dirty') {
|
||
// Could update UI indicator here
|
||
}
|
||
});
|
||
}
|
||
|
||
const state = app.state;
|
||
|
||
state.updateHiddenFields = function updateHiddenFields() {
|
||
const modeInput = dom.qs('#mode');
|
||
const publishedInput = dom.qs('#published');
|
||
if (modeInput) {
|
||
modeInput.value = state.mode;
|
||
}
|
||
if (publishedInput) {
|
||
publishedInput.value = state.published ? 'true' : 'false';
|
||
}
|
||
};
|
||
|
||
function toggleEditOnlyElements() {
|
||
const isEdit = state.mode === 'edit';
|
||
dom.qsa('[data-edit-only]').forEach(function (element) {
|
||
const value = element.getAttribute('data-edit-only');
|
||
if (value === 'true') {
|
||
dom.show(element, isEdit);
|
||
} else if (value === 'false') {
|
||
dom.show(element, !isEdit);
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateDirectoryDependentControls() {
|
||
const isEdit = state.mode === 'edit';
|
||
const hasDirectory = !!app.data.selectedDirHandle;
|
||
dom.qsa('[data-requires-directory="true"]').forEach(function (element) {
|
||
const shouldDisable = !(isEdit && hasDirectory);
|
||
if ('disabled' in element) {
|
||
element.disabled = shouldDisable;
|
||
} else {
|
||
if (shouldDisable) {
|
||
element.setAttribute('aria-disabled', 'true');
|
||
} else {
|
||
element.removeAttribute('aria-disabled');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateResponseDueVisibility() {
|
||
var typeDisplay = dom.qs('#type-display');
|
||
var typeHidden = dom.qs('#type');
|
||
var wrapper = dom.qs('#response-due-wrapper');
|
||
if (!wrapper) { return; }
|
||
var typeVal = '';
|
||
if (typeDisplay) {
|
||
typeVal = (typeDisplay.textContent || '').trim();
|
||
} else if (typeHidden) {
|
||
typeVal = (typeHidden.value || '').trim();
|
||
}
|
||
var isSubmittal = typeVal.toLowerCase() === 'submittal';
|
||
if (isSubmittal) {
|
||
wrapper.classList.remove('hidden');
|
||
} else {
|
||
wrapper.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
state.apply = function applyState() {
|
||
updateResponseDueVisibility();
|
||
const inputs = document.querySelectorAll('input, select, textarea');
|
||
inputs.forEach(function (element) {
|
||
const noDisable = element.hasAttribute('data-no-disable') || (!!element.closest('thead') && !!element.closest('.filter-row'));
|
||
element.disabled = (state.mode !== 'edit') && !noDisable;
|
||
});
|
||
|
||
['#owner-name', '#project-name', '#project-number', '#type-display'].forEach(function (selector) {
|
||
const element = dom.qs(selector);
|
||
if (element) {
|
||
element.contentEditable = (state.mode === 'edit') ? 'true' : 'false';
|
||
}
|
||
});
|
||
|
||
const remarksTextarea = dom.qs('#remarks');
|
||
const remarksContainer = dom.qs('#remarks-render-container');
|
||
var mdEditor = app.modules.markdownEditor;
|
||
const isEdit = (state.mode === 'edit') && !state.published;
|
||
|
||
if (remarksTextarea) { remarksTextarea.hidden = true; }
|
||
|
||
if (mdEditor) {
|
||
if (isEdit) {
|
||
// Show rendered preview with click-to-edit; editor loads on first click
|
||
mdEditor.bindRenderClick();
|
||
mdEditor.setRenderClickable(true);
|
||
mdEditor.showRendered();
|
||
} else {
|
||
// View mode: destroy editor, show static rendered HTML
|
||
mdEditor.destroy();
|
||
mdEditor.setRenderClickable(false);
|
||
mdEditor.refreshRender();
|
||
if (remarksContainer) { remarksContainer.hidden = false; }
|
||
}
|
||
} else if (remarksContainer) {
|
||
remarksContainer.hidden = false;
|
||
}
|
||
|
||
const titleInput = dom.qs('#title');
|
||
if (titleInput) {
|
||
const empty = !(titleInput.value || '').trim();
|
||
titleInput.hidden = (state.mode === 'view' && empty);
|
||
}
|
||
|
||
// From field: input in edit mode, rendered mailto link in view mode
|
||
const fromInput = dom.qs('#from');
|
||
const fromRender = dom.qs('#from-render');
|
||
if (fromInput && fromRender) {
|
||
if (isEdit) {
|
||
fromInput.hidden = false;
|
||
fromRender.classList.add('hidden');
|
||
fromRender.hidden = true;
|
||
} else {
|
||
fromInput.hidden = true;
|
||
fromRender.classList.remove('hidden');
|
||
fromRender.hidden = false;
|
||
if (app.modules.emailTags && app.modules.emailTags.renderFrom) {
|
||
app.modules.emailTags.renderFrom();
|
||
}
|
||
}
|
||
}
|
||
|
||
// To field: input in edit mode, rendered mailto links in view mode
|
||
const toInput = dom.qs('#to');
|
||
const toRender = dom.qs('#to-render');
|
||
if (toInput && toRender) {
|
||
if (isEdit) {
|
||
toInput.hidden = false;
|
||
toRender.classList.add('hidden');
|
||
toRender.hidden = true;
|
||
} else {
|
||
toInput.hidden = true;
|
||
toRender.classList.remove('hidden');
|
||
toRender.hidden = false;
|
||
if (app.modules.emailTags && app.modules.emailTags.render) {
|
||
app.modules.emailTags.render();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Logo placeholders: show in edit mode when no logo loaded
|
||
document.querySelectorAll('.logo-cell').forEach(function (cell) {
|
||
var img = cell.querySelector('.logo-img');
|
||
var placeholder = cell.querySelector('.logo-placeholder');
|
||
if (!placeholder) { return; }
|
||
var hasLogo = img && img.getAttribute('src');
|
||
if (hasLogo) {
|
||
cell.classList.add('has-logo');
|
||
} else {
|
||
cell.classList.remove('has-logo');
|
||
}
|
||
if (state.mode === 'edit' && !hasLogo) {
|
||
placeholder.classList.remove('hidden');
|
||
} else {
|
||
placeholder.classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
toggleEditOnlyElements();
|
||
updateDirectoryDependentControls();
|
||
};
|
||
|
||
state.detectState = function detectState() {
|
||
var data = app.json.parse();
|
||
var envelope = (data && data.envelope) || {};
|
||
var payload = (data && data.payload) || {};
|
||
var presentation = (data && data.presentation) || {};
|
||
|
||
if (envelope.digest) { return 'published'; }
|
||
|
||
var hasDate = !!(payload.date && payload.date.trim());
|
||
if (hasDate) { return 'draft'; }
|
||
|
||
var hasHeader = !!(payload.client || payload.project || payload.projectNumber ||
|
||
presentation.leftLogo || presentation.rightLogo);
|
||
if (hasHeader) { return 'template'; }
|
||
|
||
return 'clean';
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
state.updateHiddenFields();
|
||
toggleEditOnlyElements();
|
||
updateDirectoryDependentControls();
|
||
var typeInput = dom.qs('#type');
|
||
if (typeInput) {
|
||
typeInput.addEventListener('input', updateResponseDueVisibility);
|
||
}
|
||
updateResponseDueVisibility();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
const json = app.json;
|
||
const state = app.state;
|
||
|
||
function computePublishedState() {
|
||
const data = json.parse();
|
||
const digest = (data.envelope && data.envelope.digest) || '';
|
||
state.published = !!digest;
|
||
return state.published;
|
||
}
|
||
|
||
function setMode(mode) {
|
||
if (mode !== 'edit' && mode !== 'view') {
|
||
return;
|
||
}
|
||
if (state.published && mode === 'edit') {
|
||
mode = 'view';
|
||
}
|
||
state.mode = mode;
|
||
}
|
||
|
||
var _previousMode = state.mode;
|
||
|
||
function refresh() {
|
||
const wasPublished = state.published;
|
||
const nowPublished = computePublishedState();
|
||
if (nowPublished && !wasPublished) {
|
||
state.mode = 'view';
|
||
}
|
||
// Re-render file table only when mode actually changes (edit↔view)
|
||
if (state.mode !== _previousMode) {
|
||
_previousMode = state.mode;
|
||
if (app.modules.files && typeof app.modules.files.render === 'function') {
|
||
app.modules.files.render();
|
||
}
|
||
}
|
||
if (app.modules.files && app.modules.files.updateToolbars) {
|
||
app.modules.files.updateToolbars();
|
||
}
|
||
}
|
||
|
||
app.modules.mode = {
|
||
setMode,
|
||
refresh
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
computePublishedState();
|
||
if (state.published) {
|
||
state.mode = 'view';
|
||
} else {
|
||
state.mode = 'edit';
|
||
}
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
|
||
const visibility = app.modules.visibility = {};
|
||
|
||
// Field visibility rules based on document type
|
||
const FIELD_RULES = {
|
||
'from': ['Transmittal', 'Submittal'],
|
||
'to': ['Transmittal', 'Submittal']
|
||
};
|
||
|
||
function getDocumentType() {
|
||
const typeInput = dom.qs('#type');
|
||
return typeInput ? (typeInput.value || 'Transmittal').trim() : 'Transmittal';
|
||
}
|
||
|
||
function isFieldVisible(fieldId, docType) {
|
||
const rules = FIELD_RULES[fieldId];
|
||
if (!rules) {
|
||
return true; // No rules = always visible
|
||
}
|
||
return rules.includes(docType);
|
||
}
|
||
|
||
function getFieldContainer(fieldId) {
|
||
const input = dom.qs('#' + fieldId);
|
||
if (!input) {
|
||
return null;
|
||
}
|
||
// Find the parent container (the div with col-span-6 or col-span-12)
|
||
let container = input.parentElement;
|
||
while (container && !container.className.includes('col-span-')) {
|
||
container = container.parentElement;
|
||
if (!container || container.tagName === 'FORM') {
|
||
break;
|
||
}
|
||
}
|
||
return container || input.parentElement;
|
||
}
|
||
|
||
visibility.applyFieldVisibility = function applyFieldVisibility() {
|
||
const docType = getDocumentType();
|
||
Object.keys(FIELD_RULES).forEach(function(fieldId) {
|
||
const container = getFieldContainer(fieldId);
|
||
if (!container) { return; }
|
||
container.hidden = !isFieldVisible(fieldId, docType);
|
||
});
|
||
};
|
||
|
||
visibility.bindTypeInput = function bindTypeInput() {
|
||
const typeInput = dom.qs('#type');
|
||
if (!typeInput) {
|
||
return;
|
||
}
|
||
|
||
// Apply visibility on input change
|
||
typeInput.addEventListener('input', function() {
|
||
visibility.applyFieldVisibility();
|
||
if (app.markDirty) {
|
||
app.markDirty();
|
||
}
|
||
});
|
||
|
||
typeInput.addEventListener('change', function() {
|
||
visibility.applyFieldVisibility();
|
||
});
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
visibility.bindTypeInput();
|
||
visibility.applyFieldVisibility();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
const util = app.util;
|
||
const json = app.json;
|
||
|
||
let debounceTimer = null;
|
||
const DEBOUNCE_MS = 300; // 300ms debounce
|
||
|
||
async function updateLiveDigest() {
|
||
var digestDisplay = dom.qs('#digest-display');
|
||
if (!digestDisplay) { return; }
|
||
|
||
// Published — let renderSignaturesList handle the display
|
||
var data = json.parse();
|
||
var envelope = (data && data.envelope) || {};
|
||
if (envelope.digest) { return; }
|
||
|
||
// Only compute live digest in edit mode
|
||
if (app.state && app.state.mode !== 'edit') { return; }
|
||
|
||
try {
|
||
// Sync form-field values to JSON only after the app has fully
|
||
// initialised (i.e. loadFromJson has run and app.data.files is
|
||
// populated). Calling syncUiToJson before that would overwrite
|
||
// the saved-draft JSON with the empty in-memory state.
|
||
if (app._initialized && app.modules.files && app.modules.files.syncUiToJson) {
|
||
app.modules.files.syncUiToJson();
|
||
// Re-read after sync so the digest reflects the updated payload.
|
||
data = json.parse();
|
||
}
|
||
var payload = (data && data.payload) || {};
|
||
var payloadStr = util.canonicalStringify(payload);
|
||
var digest = await util.hashString(payloadStr);
|
||
var now = new Date().toLocaleString();
|
||
|
||
// Render draft verify-card
|
||
digestDisplay.innerHTML = '';
|
||
var card = document.createElement('div');
|
||
card.className = 'verify-card verify-card--draft';
|
||
|
||
var status = document.createElement('div');
|
||
status.className = 'verify-card__status verify-card__status--draft';
|
||
status.textContent = 'DRAFT';
|
||
card.appendChild(status);
|
||
|
||
var detail = document.createElement('div');
|
||
detail.className = 'verify-card__detail';
|
||
detail.innerHTML = 'Digest (SHA-256): <code>' + digest + '</code>';
|
||
card.appendChild(detail);
|
||
|
||
var time = document.createElement('div');
|
||
time.className = 'verify-card__detail';
|
||
time.textContent = 'Live \u2014 ' + now;
|
||
card.appendChild(time);
|
||
|
||
digestDisplay.appendChild(card);
|
||
} catch (err) {
|
||
console.error('[live-digest] Failed to calculate digest:', err);
|
||
}
|
||
}
|
||
|
||
function scheduleDigestUpdate() {
|
||
if (debounceTimer) {
|
||
clearTimeout(debounceTimer);
|
||
}
|
||
debounceTimer = setTimeout(updateLiveDigest, DEBOUNCE_MS);
|
||
}
|
||
|
||
function bindFormChanges() {
|
||
const form = dom.qs('#transmittal-form');
|
||
if (!form) {
|
||
return;
|
||
}
|
||
|
||
// Listen to all input changes
|
||
form.addEventListener('input', scheduleDigestUpdate);
|
||
form.addEventListener('change', scheduleDigestUpdate);
|
||
}
|
||
|
||
function subscribeToStateChanges() {
|
||
// Subscribe to reactive state changes
|
||
if (app.state && app.state.subscribe) {
|
||
app.state.subscribe(function(property, newValue, oldValue) {
|
||
scheduleDigestUpdate();
|
||
});
|
||
}
|
||
}
|
||
|
||
app.onDirty(function () {
|
||
scheduleDigestUpdate();
|
||
});
|
||
|
||
app.modules.liveDigest = {
|
||
update: updateLiveDigest,
|
||
schedule: scheduleDigestUpdate
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
bindFormChanges();
|
||
subscribeToStateChanges();
|
||
// Calculate initial digest
|
||
updateLiveDigest();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const DEBUG = false; // Set to true to enable verbose logging
|
||
|
||
const dom = app.dom;
|
||
const json = app.json;
|
||
const util = app.util;
|
||
|
||
const filesModule = app.modules.files = {};
|
||
|
||
var hasFileSystemAccess = typeof window.showDirectoryPicker === 'function';
|
||
|
||
function requireFileSystemAccess() {
|
||
if (hasFileSystemAccess) { return true; }
|
||
alert('This feature requires the File System Access API.\n\nPlease use a Chromium-based browser on desktop.');
|
||
return false;
|
||
}
|
||
|
||
function hasFiles() {
|
||
return Array.isArray(app.data.files) && app.data.files.length > 0;
|
||
}
|
||
|
||
// Three primary-button states: 'scan', 'verify', 'publish'
|
||
var _primaryIntent = null; // null = auto-detect, or explicit 'scan'|'verify'|'publish'
|
||
var _primaryHandler = null;
|
||
|
||
function setPrimary(intent) {
|
||
_primaryIntent = intent;
|
||
updateToolbars();
|
||
}
|
||
|
||
function setScanningState(active) {
|
||
var wrapper = document.querySelector('.table-wrapper');
|
||
if (!wrapper) { return; }
|
||
if (active) {
|
||
wrapper.classList.add('scanning');
|
||
} else {
|
||
wrapper.classList.remove('scanning');
|
||
}
|
||
}
|
||
|
||
function nowMs() {
|
||
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
||
return performance.now();
|
||
}
|
||
return Date.now();
|
||
}
|
||
|
||
function formatDuration(ms) {
|
||
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
||
const minutes = Math.floor(totalSeconds / 60);
|
||
const seconds = totalSeconds % 60;
|
||
if (minutes > 0) {
|
||
return minutes + 'm ' + seconds.toString().padStart(2, '0') + 's';
|
||
}
|
||
return seconds + 's';
|
||
}
|
||
|
||
// ── File handle permission and refresh helpers ─────────────────────
|
||
|
||
async function ensureDirHandlePermission(dirHandle) {
|
||
if (!dirHandle || typeof dirHandle.requestPermission !== 'function') {
|
||
return;
|
||
}
|
||
const permissionDescriptor = { mode: 'readwrite' };
|
||
const current = await dirHandle.queryPermission(permissionDescriptor);
|
||
if (current === 'granted') {
|
||
return;
|
||
}
|
||
// Request permission (requires user gesture in most browsers)
|
||
const result = await dirHandle.requestPermission(permissionDescriptor);
|
||
if (result !== 'granted') {
|
||
throw new Error('Read/write permission is required for the selected directory');
|
||
}
|
||
}
|
||
|
||
async function getFreshFile(fileHandle, fallbackData) {
|
||
if (!fileHandle || typeof fileHandle.getFile !== 'function') {
|
||
throw new Error('No valid file handle');
|
||
}
|
||
|
||
// Check permission state before attempting to get file
|
||
try {
|
||
const current = await fileHandle.queryPermission({ mode: 'read' });
|
||
if (current !== 'granted') {
|
||
// Try to request permission (will fail without user gesture)
|
||
try {
|
||
await fileHandle.requestPermission({ mode: 'read' });
|
||
} catch (permErr) {
|
||
// Permission request failed (likely no user gesture), skip with warning
|
||
console.warn('[transmittal] Permission request failed for file:', fileHandle.name || 'unknown', permErr);
|
||
}
|
||
// Check permission again after potential request
|
||
const newPerm = await fileHandle.queryPermission({ mode: 'read' });
|
||
if (newPerm !== 'granted') {
|
||
if (fallbackData) {
|
||
// Use fallback data instead of throwing
|
||
console.log('[transmittal] Permission not granted, using fallback for ' + (fallbackData.path || fallbackData.name || 'unknown file'));
|
||
return fallbackData;
|
||
}
|
||
throw new Error('Permission denied for file access');
|
||
}
|
||
}
|
||
} catch (err) {
|
||
// queryPermission not supported or error
|
||
console.warn('[transmittal] Permission check error:', err);
|
||
}
|
||
|
||
try {
|
||
return await fileHandle.getFile();
|
||
} catch (err) {
|
||
if (err && err.name === 'NotReadableError') {
|
||
if (fallbackData) {
|
||
console.log('[transmittal] NotReadableError, using fallback for ' + (fallbackData.path || fallbackData.name || 'unknown file'));
|
||
return fallbackData;
|
||
}
|
||
throw err;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
function updateDirectoryIndicator(name) {
|
||
var indicator = dom.qs('#selected-directory');
|
||
if (indicator) {
|
||
indicator.textContent = name ? name : '';
|
||
}
|
||
updateToolbars();
|
||
}
|
||
|
||
function sortFilesInPlace(list) {
|
||
if (!Array.isArray(list)) {
|
||
return;
|
||
}
|
||
list.sort(util.compareFilesByTrackingRevision);
|
||
}
|
||
|
||
function buildFileHandleMap(files) {
|
||
var map = {};
|
||
(files || []).forEach(function (f) {
|
||
if (f.fileHandle) {
|
||
var key = (f.path || f.name || '').toLowerCase();
|
||
if (key) { map[key] = f.fileHandle; }
|
||
}
|
||
});
|
||
return map;
|
||
}
|
||
|
||
function restoreFileHandles(files, handleMap) {
|
||
(files || []).forEach(function (f) {
|
||
var key = (f.path || f.name || '').toLowerCase();
|
||
if (key && handleMap[key]) {
|
||
f.fileHandle = handleMap[key];
|
||
}
|
||
});
|
||
}
|
||
|
||
function selfEntryDate() {
|
||
var raw = (dom.qs('#date') || {}).value || '';
|
||
var trimmed = raw.trim();
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return trimmed; }
|
||
var d = new Date(trimmed);
|
||
if (!isNaN(d.getTime())) { return d.toISOString().slice(0, 10); }
|
||
return trimmed;
|
||
}
|
||
|
||
function buildSelfEntry() {
|
||
var tracking = (dom.qs('#tracking-number') || {}).value || '';
|
||
var subject = (dom.qs('#subject') || {}).value || '';
|
||
var title = (dom.qs('#title') || {}).value || '';
|
||
var date = selfEntryDate();
|
||
var purpose = (dom.qs('#purpose') || {}).value || '';
|
||
var d = app.modules.data;
|
||
var filename = (d && d.buildFileName)
|
||
? d.buildFileName({ trackingNumber: tracking, title: title, date: date, purpose: purpose }, { extension: 'html' })
|
||
: '';
|
||
return {
|
||
_isSelf: true,
|
||
path: filename ? ('./' + filename) : '',
|
||
name: filename || '',
|
||
trackingNumber: tracking,
|
||
title: subject || title,
|
||
revision: date,
|
||
status: purpose,
|
||
extension: 'html',
|
||
size: 0,
|
||
fileSize: 0,
|
||
sha256: app.constants.SELF_HASH
|
||
};
|
||
}
|
||
|
||
filesModule.buildSelfEntry = buildSelfEntry;
|
||
|
||
function canonicalFilePayload(entry) {
|
||
const relativePath = entry.path || entry.name || '';
|
||
const filename = relativePath.split('/').pop() || entry.name || '';
|
||
const pathOnly = relativePath.substring(0, relativePath.lastIndexOf('/')) || '';
|
||
return {
|
||
trackingNumber: entry.trackingNumber || '',
|
||
revision: entry.revision || '',
|
||
status: entry.status || '',
|
||
title: entry.title || '',
|
||
path: pathOnly,
|
||
filename: filename,
|
||
extension: entry.extension || '',
|
||
sha256: entry.sha256 || '',
|
||
fileSize: Number(entry.fileSize || entry.size || 0)
|
||
};
|
||
}
|
||
|
||
function updateFilesInJson(files) {
|
||
const data = json.parse();
|
||
const envelope = { ...(data.envelope || {}) };
|
||
const payload = { ...(data.payload || {}) };
|
||
const presentation = { ...(data.presentation || {}) };
|
||
|
||
const sorted = (Array.isArray(files) ? files : []).slice().sort(util.compareFilesByTrackingRevision);
|
||
// Prepend self-entry, then regular files
|
||
var selfEntry = buildSelfEntry();
|
||
payload.files = [canonicalFilePayload(selfEntry)].concat(sorted.map(canonicalFilePayload));
|
||
|
||
// Clear digest when files change (invalidates signatures)
|
||
envelope.digest = '';
|
||
envelope.digestedAt = '';
|
||
envelope.signatures = [];
|
||
|
||
json.setData({ envelope: envelope, payload: payload, presentation: presentation });
|
||
}
|
||
|
||
var ARCHIVE_DIR_NAME = '.archive';
|
||
var MAX_DIRECTORY_DEPTH = 48;
|
||
|
||
function isHiddenName(name) {
|
||
return !!name && name.startsWith('.');
|
||
}
|
||
|
||
function shouldSkipDirectorySegment(name) {
|
||
if (!name) {
|
||
return false;
|
||
}
|
||
if (name === ARCHIVE_DIR_NAME) {
|
||
return true;
|
||
}
|
||
return isHiddenName(name);
|
||
}
|
||
|
||
async function collectFilesRecursive(handle, relPath, out, depth) {
|
||
const currentDepth = depth || 0;
|
||
if (DEBUG) console.log('[transmittal] collectFilesRecursive:', relPath, 'depth:', currentDepth, 'kind:', handle.kind);
|
||
|
||
if (currentDepth > MAX_DIRECTORY_DEPTH) {
|
||
throw new Error('Directory nesting exceeds supported depth');
|
||
}
|
||
if (handle.kind === 'file') {
|
||
if (isHiddenName(handle.name)) {
|
||
if (DEBUG) console.log('[transmittal] Skipping hidden file:', relPath);
|
||
return;
|
||
}
|
||
if (DEBUG) console.log('[transmittal] Adding file to collection:', relPath);
|
||
out.push({ handle, path: relPath, name: handle.name });
|
||
return;
|
||
}
|
||
if (handle.kind !== 'directory') {
|
||
if (DEBUG) console.log('[transmittal] Unknown handle kind:', handle.kind, 'for', relPath);
|
||
return;
|
||
}
|
||
if (shouldSkipDirectorySegment(handle.name) && relPath) {
|
||
if (DEBUG) console.log('[transmittal] Skipping directory segment:', relPath);
|
||
return;
|
||
}
|
||
try {
|
||
if (DEBUG) console.log('[transmittal] Iterating directory:', relPath || 'root');
|
||
let childCount = 0;
|
||
for await (const child of handle.values()) {
|
||
childCount++;
|
||
if (DEBUG) console.log('[transmittal] Found child #' + childCount + ':', child.name, 'kind:', child.kind);
|
||
if (shouldSkipDirectorySegment(child.name)) {
|
||
continue;
|
||
}
|
||
const childPath = relPath ? (relPath + '/' + child.name) : child.name;
|
||
if (DEBUG) console.log('[transmittal] Recursing into:', childPath);
|
||
await collectFilesRecursive(child, childPath, out, currentDepth + 1);
|
||
}
|
||
if (DEBUG) console.log('[transmittal] Finished iterating directory:', relPath || 'root', 'found', childCount, 'children');
|
||
} catch (err) {
|
||
if (err && err.name === 'NotFoundError') {
|
||
console.error('[transmittal] NotFoundError during directory traversal:', relPath || handle.name || '', {
|
||
error: err,
|
||
message: err.message,
|
||
stack: err.stack,
|
||
handleName: handle.name,
|
||
handleKind: handle.kind
|
||
});
|
||
return;
|
||
}
|
||
console.error('[transmittal] Unexpected error during directory traversal:', relPath, err);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
async function collectDirectoryEntries(rootHandle) {
|
||
const entries = [];
|
||
|
||
if (!rootHandle || rootHandle.kind !== 'directory') {
|
||
throw new Error('Invalid directory handle provided');
|
||
}
|
||
|
||
try {
|
||
// Start directory scan
|
||
if (DEBUG) console.log('[transmittal] Starting directory scan for:', rootHandle.name);
|
||
|
||
for await (const child of rootHandle.values()) {
|
||
if (shouldSkipDirectorySegment(child.name)) {
|
||
continue;
|
||
}
|
||
const childPath = child.name;
|
||
try {
|
||
await collectFilesRecursive(child, childPath, entries, 1);
|
||
} catch (err) {
|
||
if (err && err.name === 'NotFoundError') {
|
||
console.warn('[transmittal] directory entry missing', childPath, err);
|
||
continue;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
if (err && err.name === 'NotFoundError') {
|
||
console.error('[transmittal] NotFoundError during directory scan', {
|
||
error: err,
|
||
message: err.message,
|
||
stack: err.stack,
|
||
handle: rootHandle,
|
||
handleName: rootHandle.name
|
||
});
|
||
// Return empty entries instead of throwing
|
||
return entries;
|
||
}
|
||
console.error('[transmittal] Unexpected error during directory scan', err);
|
||
throw err;
|
||
}
|
||
return entries;
|
||
}
|
||
|
||
// Promote shared helpers to filesModule for use by sub-modules
|
||
filesModule.nowMs = nowMs;
|
||
filesModule.formatDuration = formatDuration;
|
||
filesModule.updateDirectoryIndicator = updateDirectoryIndicator;
|
||
filesModule.sortFilesInPlace = sortFilesInPlace;
|
||
filesModule.updateFilesInJson = updateFilesInJson;
|
||
filesModule.collectDirectoryEntries = collectDirectoryEntries;
|
||
|
||
async function ensureDirHandle() {
|
||
if (typeof window.showDirectoryPicker !== 'function') {
|
||
throw new Error('File System Access API showDirectoryPicker is required');
|
||
}
|
||
const handle = await window.showDirectoryPicker();
|
||
|
||
// Log the handle details for debugging
|
||
if (DEBUG) {
|
||
console.log('[transmittal] Directory selected:', {
|
||
name: handle.name,
|
||
kind: handle.kind,
|
||
hasQueryPermission: typeof handle.queryPermission === 'function',
|
||
hasRequestPermission: typeof handle.requestPermission === 'function'
|
||
});
|
||
}
|
||
|
||
async function ensureRwPermission(dirHandle) {
|
||
if (!dirHandle) {
|
||
throw new Error('Directory handle is undefined');
|
||
}
|
||
if (typeof dirHandle.requestPermission !== 'function') {
|
||
return;
|
||
}
|
||
const permissionDescriptor = { mode: 'readwrite' };
|
||
if (typeof dirHandle.queryPermission === 'function') {
|
||
const current = await dirHandle.queryPermission(permissionDescriptor);
|
||
if (current === 'granted') {
|
||
return;
|
||
}
|
||
}
|
||
const result = await dirHandle.requestPermission(permissionDescriptor);
|
||
if (result !== 'granted') {
|
||
throw new Error('Read/write permission is required for the selected directory');
|
||
}
|
||
}
|
||
|
||
await ensureRwPermission(handle);
|
||
|
||
// Verify the handle is still valid after permission check
|
||
if (!handle || handle.kind !== 'directory') {
|
||
throw new Error('Directory handle became invalid after permission check');
|
||
}
|
||
|
||
app.data.selectedDirHandle = handle;
|
||
updateDirectoryIndicator(handle.name);
|
||
app.state.apply();
|
||
|
||
if (DEBUG) console.log('[transmittal] Directory handle ready:', handle.name);
|
||
return handle;
|
||
}
|
||
|
||
// Parse folder name: YYYY-MM-DD_TRACKING (STATUS) - TITLE
|
||
// Wraps zddc.parseFolder, preserving this module's null-on-invalid contract.
|
||
function parseFolderName(name) {
|
||
var parsed = zddc.parseFolder(name);
|
||
if (!parsed || !parsed.valid) { return null; }
|
||
return {
|
||
date: parsed.date,
|
||
trackingNumber: parsed.trackingNumber,
|
||
status: parsed.status,
|
||
title: parsed.title
|
||
};
|
||
}
|
||
|
||
function populateFields(parsed) {
|
||
if (!parsed) { return; }
|
||
var map = {
|
||
'date': parsed.date,
|
||
'tracking-number': parsed.trackingNumber,
|
||
'purpose': parsed.status,
|
||
'subject': parsed.title
|
||
};
|
||
Object.keys(map).forEach(function (id) {
|
||
var el = dom.qs('#' + id);
|
||
if (el && map[id]) {
|
||
el.value = map[id];
|
||
}
|
||
});
|
||
}
|
||
|
||
// Shared scan pipeline: collect → populate rows → hash with progress
|
||
// onHash(item, hash) is called per file after hashing; return value sets cell content.
|
||
async function scanEntries(dirHandle, onHash) {
|
||
var entries = await collectDirectoryEntries(dirHandle);
|
||
entries.sort(function (a, b) { return a.path.localeCompare(b.path); });
|
||
|
||
// Phase 0: Ensure directory handle has permission
|
||
await ensureDirHandlePermission(dirHandle);
|
||
|
||
// Phase 1: merge with existing files
|
||
var existingIndex = filesModule.buildExistingIndex(app.data.files || []);
|
||
var existingPasteKeys = Object.keys(existingIndex);
|
||
setScanningState(true);
|
||
var hashCells = [];
|
||
for (var i = 0; i < entries.length; i++) {
|
||
var entry = entries[i];
|
||
try {
|
||
// Check and refresh handle permission before getFile
|
||
var file = await getFreshFile(entry.handle);
|
||
var parsed = (zddc.parseFilename(file.name) || {});
|
||
var fileData = {
|
||
path: entry.path,
|
||
name: file.name,
|
||
size: file.size,
|
||
fileSize: file.size,
|
||
sha256: '',
|
||
trackingNumber: parsed.trackingNumber,
|
||
title: parsed.title,
|
||
revision: parsed.revision,
|
||
status: parsed.status,
|
||
extension: parsed.extension || zddc.splitExtension(file.name).extension,
|
||
fileHandle: entry.handle
|
||
};
|
||
var pasteKey = (fileData.trackingNumber || '').toLowerCase() + '|' + (fileData.revision || '').toLowerCase();
|
||
if (existingPasteKeys.indexOf(pasteKey) === -1) {
|
||
app.data.files.push(fileData);
|
||
var hashCell = filesModule.renderSingleRow(fileData, app.data.files.length - 1);
|
||
hashCells.push({ cell: hashCell, fileData: fileData, handle: entry.handle });
|
||
}
|
||
} catch (err) {
|
||
console.error('[transmittal] Error reading file:', entry.path, err);
|
||
}
|
||
if (i % 20 === 0) {
|
||
await new Promise(function (r) { setTimeout(r, 0); });
|
||
}
|
||
}
|
||
|
||
// Phase 2: hash each file with progress bar
|
||
setScanningState(false);
|
||
for (var j = 0; j < hashCells.length; j++) {
|
||
var item = hashCells[j];
|
||
try {
|
||
var fill = item.cell ? item.cell.querySelector('.hash-progress-fill') : null;
|
||
var onProgress = fill ? function (f) {
|
||
return function (loaded, total) {
|
||
var pct = total > 0 ? Math.round((loaded / total) * 100) : 0;
|
||
f.style.width = pct + '%';
|
||
};
|
||
}(fill) : null;
|
||
var file = await getFreshFile(item.handle);
|
||
var hash = await util.hashFile(file, onProgress);
|
||
item.fileData.sha256 = hash;
|
||
if (onHash) {
|
||
onHash(item, hash);
|
||
} else if (item.cell) {
|
||
item.cell.textContent = util.formatShortFileHash(hash);
|
||
}
|
||
} catch (err) {
|
||
console.error('[transmittal] Error hashing file:', item.fileData.path, err);
|
||
if (item.cell) { item.cell.textContent = 'error'; }
|
||
}
|
||
}
|
||
return hashCells;
|
||
}
|
||
|
||
function finalizeAfterScan() {
|
||
var handleMap = buildFileHandleMap(app.data.files);
|
||
updateFilesInJson(app.data.files);
|
||
filesModule.render();
|
||
filesModule.loadFromJson({ filesOnly: true });
|
||
restoreFileHandles(app.data.files, handleMap);
|
||
app.state.apply();
|
||
}
|
||
|
||
async function selectDirectory(event) {
|
||
if (DEBUG) console.log('[transmittal] ========== SELECT DIRECTORY STARTED ==========');
|
||
const trigger = event && event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
||
if (trigger) {
|
||
trigger.disabled = true;
|
||
trigger.classList.add('opacity-60');
|
||
}
|
||
try {
|
||
var dirHandle = app.data.selectedDirHandle;
|
||
if (!dirHandle) {
|
||
dirHandle = await ensureDirHandle();
|
||
}
|
||
populateFields(parseFolderName(dirHandle.name));
|
||
await scanEntries(dirHandle);
|
||
finalizeAfterScan();
|
||
app.markDirty();
|
||
setPrimary('publish');
|
||
} catch (err) {
|
||
if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) {
|
||
if (DEBUG) console.log('[transmittal] User cancelled directory selection');
|
||
} else {
|
||
console.error('[transmittal] selectDirectory failed', err);
|
||
}
|
||
} finally {
|
||
setScanningState(false);
|
||
if (trigger) {
|
||
trigger.disabled = false;
|
||
trigger.classList.remove('opacity-60');
|
||
}
|
||
}
|
||
}
|
||
|
||
async function writeFileToSelectedDir(filename, contents, mime) {
|
||
if (!app.data.selectedDirHandle) {
|
||
throw new Error('No directory selected');
|
||
}
|
||
const fileHandle = await app.data.selectedDirHandle.getFileHandle(filename, { create: true });
|
||
const writable = await fileHandle.createWritable();
|
||
await writable.write(new Blob([contents], { type: mime || 'text/plain' }));
|
||
await writable.close();
|
||
}
|
||
|
||
filesModule.writeFileToSelectedDir = writeFileToSelectedDir;
|
||
|
||
// ── Paste helpers ─────────────────────────────────────
|
||
// Expected formats (tab-separated, 3-5 adjacent columns):
|
||
// 3 cols: Tracking \t Title \t Revision+Status ("A (IFR)")
|
||
// 4 cols: Tracking \t Title \t Revision \t Status
|
||
// 5 cols: Tracking \t Title \t Revision \t Status \t Extension
|
||
var MAX_PASTE_COLS = 5;
|
||
var HEADER_RE = /^(#|tracking|number|title|revision|status|ext)/i;
|
||
|
||
function isHeaderLine(cols) {
|
||
if (!cols || !cols.length) { return false; }
|
||
return HEADER_RE.test((cols[0] || '').trim());
|
||
}
|
||
|
||
function columnsToFileRow(cols) {
|
||
var tracking = (cols[0] || '').trim();
|
||
if (!tracking) { return null; }
|
||
var revision = (cols[2] || '').trim();
|
||
var status = (cols[3] || '').trim();
|
||
var extension = (cols[4] || '').trim().toLowerCase();
|
||
|
||
// 3-column paste: split "A (IFR)" into revision "A" and status "IFR"
|
||
if (cols.length <= 3 && revision) {
|
||
var spaceIdx = revision.indexOf(' ');
|
||
if (spaceIdx > 0) {
|
||
status = revision.substring(spaceIdx + 1).trim().replace(/^\(+/, '').replace(/\)+$/, '');
|
||
revision = revision.substring(0, spaceIdx).trim();
|
||
}
|
||
}
|
||
return {
|
||
trackingNumber: tracking,
|
||
title: (cols[1] || '').trim(),
|
||
revision: revision,
|
||
status: status,
|
||
extension: extension,
|
||
path: '', name: '', size: 0, fileSize: 0, sha256: ''
|
||
};
|
||
}
|
||
|
||
// Parse plain-text tab-separated clipboard data.
|
||
// Returns { rows: [...], tooWide: false } or { rows: [], tooWide: true }.
|
||
function parseClipboardText(text) {
|
||
var lines = (text || '').split(/\r?\n/);
|
||
var rows = [];
|
||
for (var i = 0; i < lines.length; i++) {
|
||
var line = lines[i].trim();
|
||
if (!line) { continue; }
|
||
var cols = line.split('\t');
|
||
if (isHeaderLine(cols)) { continue; }
|
||
if (cols.length > MAX_PASTE_COLS) {
|
||
return { rows: [], tooWide: true, colCount: cols.length };
|
||
}
|
||
var row = columnsToFileRow(cols);
|
||
if (row) { rows.push(row); }
|
||
}
|
||
return { rows: rows, tooWide: false };
|
||
}
|
||
|
||
function pasteFileKey(trackingNumber, revision) {
|
||
return (trackingNumber || '').toLowerCase() + '|' + (revision || '').toLowerCase();
|
||
}
|
||
|
||
function buildExistingIndex(files) {
|
||
var index = {};
|
||
for (var i = 0; i < files.length; i++) {
|
||
index[pasteFileKey(files[i].trackingNumber, files[i].revision)] = i;
|
||
}
|
||
return index;
|
||
}
|
||
filesModule.buildExistingIndex = buildExistingIndex;
|
||
|
||
async function handlePasteFiles(mode, clipText) {
|
||
var setStatus = app.modules.data && app.modules.data.setStatus;
|
||
try {
|
||
var text = clipText || await navigator.clipboard.readText();
|
||
var result = parseClipboardText(text);
|
||
if (result.tooWide) {
|
||
if (setStatus) {
|
||
setStatus('Paste has ' + result.colCount + ' columns (max ' + MAX_PASTE_COLS + '). ' +
|
||
'Copy only: Tracking, Title, Revision, [Status], [Extension]', 'error');
|
||
}
|
||
return;
|
||
}
|
||
var rows = result.rows;
|
||
if (!rows.length) {
|
||
if (setStatus) { setStatus('No valid rows found. Expected tab-separated: Tracking, Title, Revision, [Status], [Ext]', 'error'); }
|
||
return;
|
||
}
|
||
|
||
var added = 0, updated = 0;
|
||
if (mode === 'new') {
|
||
app.data.files = [];
|
||
for (var i = 0; i < rows.length; i++) {
|
||
app.data.files.push(rows[i]);
|
||
added++;
|
||
}
|
||
} else {
|
||
var index = buildExistingIndex(app.data.files || []);
|
||
for (var j = 0; j < rows.length; j++) {
|
||
var row = rows[j];
|
||
var key = pasteFileKey(row.trackingNumber, row.revision);
|
||
if (index.hasOwnProperty(key)) {
|
||
var existing = app.data.files[index[key]];
|
||
existing.title = row.title;
|
||
existing.status = row.status;
|
||
if (row.extension) { existing.extension = row.extension; }
|
||
updated++;
|
||
} else {
|
||
app.data.files.push(row);
|
||
added++;
|
||
}
|
||
}
|
||
}
|
||
|
||
filesModule.sortFilesInPlace(app.data.files);
|
||
updateFilesInJson(app.data.files);
|
||
filesModule.render();
|
||
app.state.apply();
|
||
app.markDirty();
|
||
setPrimary('verify');
|
||
|
||
var msg = mode === 'new'
|
||
? 'Replaced file list: ' + added + ' rows'
|
||
: added + ' added, ' + updated + ' updated';
|
||
if (setStatus) { setStatus(msg, 'success'); }
|
||
} catch (err) {
|
||
console.error('[transmittal] paste failed', err);
|
||
if (setStatus) { setStatus('Paste failed: ' + (err && err.message ? err.message : err), 'error'); }
|
||
}
|
||
}
|
||
|
||
// ── Toolbar visibility per state ────────────────────
|
||
function updateToolbars() {
|
||
var docState = app.state.detectState();
|
||
buildBottomBar(docState);
|
||
}
|
||
|
||
function createMenuItem(label, handler, options) {
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'dropdown-item' + ((options && options.danger) ? ' text-red-600' : '');
|
||
btn.setAttribute('role', 'menuitem');
|
||
btn.textContent = label;
|
||
btn.addEventListener('click', handler);
|
||
return btn;
|
||
}
|
||
|
||
function createSeparator(label) {
|
||
var el = document.createElement('div');
|
||
el.className = 'dropdown-separator';
|
||
if (label) { el.textContent = label; }
|
||
return el;
|
||
}
|
||
|
||
function applyPrimaryButton(btn, intent) {
|
||
if (_primaryHandler) { btn.removeEventListener('click', _primaryHandler); }
|
||
btn.disabled = false;
|
||
btn.classList.remove('opacity-60');
|
||
if (intent === 'publish') {
|
||
btn.textContent = 'Publish';
|
||
_primaryHandler = function () {
|
||
document.dispatchEvent(new CustomEvent('transmittal:open-publish'));
|
||
};
|
||
} else if (intent === 'verify') {
|
||
btn.textContent = 'Verify Directory';
|
||
_primaryHandler = function () {
|
||
if (!requireFileSystemAccess()) { return; }
|
||
verifyFiles();
|
||
};
|
||
} else {
|
||
btn.textContent = 'Scan Directory';
|
||
_primaryHandler = function () {
|
||
if (!requireFileSystemAccess()) { return; }
|
||
selectDirectory({ currentTarget: null });
|
||
};
|
||
}
|
||
btn.addEventListener('click', _primaryHandler);
|
||
}
|
||
|
||
function buildBottomBar(docState) {
|
||
var primaryBtn = dom.qs('#bottom-primary');
|
||
var dropdown = dom.qs('#bottom-dropdown');
|
||
if (!primaryBtn || !dropdown) { return; }
|
||
|
||
var isPublished = docState === 'published';
|
||
var d = app.modules.data || {};
|
||
|
||
dropdown.innerHTML = '';
|
||
dropdown.classList.add('hidden');
|
||
|
||
if (isPublished) {
|
||
applyPrimaryButton(primaryBtn, 'verify');
|
||
|
||
dropdown.appendChild(createMenuItem('Copy Table', function () {
|
||
if (d.handleCopyTable) { d.handleCopyTable(); }
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Copy JSON', function () {
|
||
if (d.handleCopyJson) { d.handleCopyJson(); }
|
||
}));
|
||
dropdown.appendChild(createSeparator());
|
||
dropdown.appendChild(createMenuItem('Add Signature', function () {
|
||
if (app.modules.security && app.modules.security.addSignature) {
|
||
app.modules.security.addSignature();
|
||
}
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Acknowledge Receipt', function () {
|
||
if (app.modules.security && app.modules.security.addSignature) {
|
||
app.modules.security.addSignature({ label: 'Received By' });
|
||
}
|
||
}));
|
||
dropdown.appendChild(createSeparator());
|
||
dropdown.appendChild(createMenuItem('Revise', function () {
|
||
if (d.handleRevise) { d.handleRevise(); }
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Import HTML', function () {
|
||
if (d.handleImportHtml) { d.handleImportHtml(); }
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Reset', function () {
|
||
if (app.modules.reset && app.modules.reset.handleReset) {
|
||
app.modules.reset.handleReset();
|
||
}
|
||
}, { danger: true }));
|
||
dropdown.appendChild(createSeparator());
|
||
dropdown.appendChild(createMenuItem('Create Index', function () {
|
||
if (!requireFileSystemAccess()) { return; }
|
||
if (filesModule.generateArchiveRedirects) {
|
||
filesModule.generateArchiveRedirects().catch(function (err) {
|
||
console.error('[transmittal] create-index failed', err);
|
||
});
|
||
}
|
||
}));
|
||
} else {
|
||
// Edit: respect explicit intent, or auto-detect
|
||
var intent = _primaryIntent;
|
||
if (!intent) {
|
||
intent = hasFiles() ? 'verify' : 'scan';
|
||
}
|
||
applyPrimaryButton(primaryBtn, intent);
|
||
|
||
dropdown.appendChild(createMenuItem('Scan Directory', function () {
|
||
if (!requireFileSystemAccess()) { return; }
|
||
selectDirectory({ currentTarget: null });
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Verify Directory', function () {
|
||
if (!requireFileSystemAccess()) { return; }
|
||
verifyFiles();
|
||
}));
|
||
dropdown.appendChild(createSeparator());
|
||
dropdown.appendChild(createMenuItem('Publish', function () {
|
||
document.dispatchEvent(new CustomEvent('transmittal:open-publish'));
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Save Draft', function () {
|
||
if (d.handleSaveHtmlDraft) { d.handleSaveHtmlDraft(); }
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Create Folder', async function () {
|
||
var setStatus = d.setStatus;
|
||
try {
|
||
// Sync UI so payload reflects current form values
|
||
filesModule.syncUiToJson();
|
||
var data = json.parse();
|
||
var payload = (data && data.payload) || {};
|
||
if (!payload.trackingNumber) {
|
||
if (setStatus) { setStatus('Enter a tracking number first', 'error'); }
|
||
return;
|
||
}
|
||
var folderName = d.buildFolderName(payload);
|
||
// Sanitize for filesystem
|
||
folderName = folderName.replace(/[<>:"/\\|?*]+/g, '_').replace(/_+/g, '_');
|
||
|
||
// Prompt for staging directory
|
||
var stagingHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||
var newFolderHandle = await stagingHandle.getDirectoryHandle(folderName, { create: true });
|
||
|
||
// Set as selected directory for subsequent file operations
|
||
app.data.selectedDirHandle = newFolderHandle;
|
||
app.data.selectedDirName = folderName;
|
||
if (filesModule.updateDirectoryIndicator) {
|
||
filesModule.updateDirectoryIndicator();
|
||
}
|
||
|
||
// Save a draft into the new folder
|
||
var pub = app.modules.publish;
|
||
if (pub && typeof pub.syncUiToJson === 'function' && typeof pub.buildHtmlString === 'function') {
|
||
await pub.syncUiToJson({ sign: false, computeDigest: false });
|
||
var html = await pub.buildHtmlString();
|
||
var draftName = d.buildFileName(
|
||
((json.parse() || {}).payload || {}),
|
||
{ extension: 'html', draft: true }
|
||
);
|
||
await writeFileToSelectedDir(draftName, html, 'text/html');
|
||
// Verify both the folder and draft file exist
|
||
var warnings = [];
|
||
try {
|
||
await stagingHandle.getDirectoryHandle(folderName);
|
||
} catch (_) {
|
||
warnings.push('folder \"' + folderName + '\" could not be verified');
|
||
}
|
||
try {
|
||
await newFolderHandle.getFileHandle(draftName);
|
||
} catch (_) {
|
||
warnings.push('draft file \"' + draftName + '\" could not be verified');
|
||
}
|
||
if (warnings.length) {
|
||
if (setStatus) { setStatus('Warning: ' + warnings.join('; ') + '. Path may be too long for Windows.', 'error'); }
|
||
} else if (setStatus) {
|
||
setStatus('Draft saved to ' + folderName + '. Close this file and open ' + draftName + ' from the new folder.', 'success');
|
||
}
|
||
} else {
|
||
if (setStatus) { setStatus('Created folder: ' + folderName, 'success'); }
|
||
}
|
||
} catch (err) {
|
||
if (err && err.name === 'AbortError') { return; }
|
||
console.error('[transmittal] create-folder failed', err);
|
||
if (setStatus) { setStatus('Create folder failed: ' + (err.message || err), 'error'); }
|
||
}
|
||
}));
|
||
dropdown.appendChild(createSeparator());
|
||
dropdown.appendChild(createMenuItem('Paste New Rows', async function () {
|
||
var text;
|
||
try { text = await navigator.clipboard.readText(); } catch (e) {
|
||
if (d.setStatus) { d.setStatus('Clipboard access denied', 'error'); }
|
||
return;
|
||
}
|
||
var result = parseClipboardText(text);
|
||
if (result.tooWide) {
|
||
if (d.setStatus) {
|
||
d.setStatus('Paste has ' + result.colCount + ' columns (max ' + MAX_PASTE_COLS + '). ' +
|
||
'Copy only: Tracking, Title, Revision, [Status], [Extension]', 'error');
|
||
}
|
||
return;
|
||
}
|
||
if (!result.rows.length) {
|
||
if (d.setStatus) { d.setStatus('No valid rows on clipboard', 'error'); }
|
||
return;
|
||
}
|
||
if (confirm('Replace file list with ' + result.rows.length + ' rows from clipboard?')) {
|
||
handlePasteFiles('new', text);
|
||
}
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Paste Append Rows', function () {
|
||
handlePasteFiles('append');
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Copy Table', function () {
|
||
if (d.handleCopyTable) { d.handleCopyTable(); }
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Remove Files', function () {
|
||
if (!app.data.files.length) {
|
||
if (d.setStatus) { d.setStatus('File list is already empty', 'error'); }
|
||
return;
|
||
}
|
||
if (confirm('Remove all ' + app.data.files.length + ' files from the list? Header info and remarks will be kept.')) {
|
||
app.data.files = [];
|
||
updateFilesInJson([]);
|
||
filesModule.render();
|
||
app.state.apply();
|
||
app.markDirty();
|
||
if (d.setStatus) { d.setStatus('File list cleared', 'success'); }
|
||
}
|
||
}));
|
||
dropdown.appendChild(createSeparator());
|
||
dropdown.appendChild(createMenuItem('Import HTML', function () {
|
||
if (d.handleImportHtml) { d.handleImportHtml(); }
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Copy JSON', function () {
|
||
if (d.handleCopyJson) { d.handleCopyJson(); }
|
||
}));
|
||
dropdown.appendChild(createMenuItem('Paste JSON', function () {
|
||
if (d.handleLoadFromClipboard) { d.handleLoadFromClipboard(); }
|
||
}));
|
||
dropdown.appendChild(createSeparator());
|
||
dropdown.appendChild(createMenuItem('Reset', function () {
|
||
if (app.modules.reset && app.modules.reset.handleReset) {
|
||
app.modules.reset.handleReset();
|
||
}
|
||
}, { danger: true }));
|
||
dropdown.appendChild(createSeparator());
|
||
dropdown.appendChild(createMenuItem('Create Index', function () {
|
||
if (!requireFileSystemAccess()) { return; }
|
||
if (filesModule.generateArchiveRedirects) {
|
||
filesModule.generateArchiveRedirects().catch(function (err) {
|
||
console.error('[transmittal] create-index failed', err);
|
||
});
|
||
}
|
||
}));
|
||
}
|
||
}
|
||
|
||
// ── Bottom bar dropdown toggle ──────────────────────
|
||
function initBottomBarToggle() {
|
||
var toggle = dom.qs('#bottom-toggle');
|
||
var menu = dom.qs('#bottom-dropdown');
|
||
if (!toggle || !menu) { return; }
|
||
|
||
toggle.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
var open = !menu.classList.contains('hidden');
|
||
menu.classList.toggle('hidden', open);
|
||
toggle.setAttribute('aria-expanded', String(!open));
|
||
});
|
||
|
||
menu.addEventListener('click', function () {
|
||
menu.classList.add('hidden');
|
||
toggle.setAttribute('aria-expanded', 'false');
|
||
});
|
||
|
||
document.addEventListener('click', function (e) {
|
||
var container = dom.qs('#bottom-menu');
|
||
if (container && !container.contains(e.target)) {
|
||
menu.classList.add('hidden');
|
||
toggle.setAttribute('aria-expanded', 'false');
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
async function refreshDirectory() {
|
||
var dirHandle = app.data.selectedDirHandle;
|
||
if (!dirHandle) { return; }
|
||
try {
|
||
// Ensure directory handle has permission before scanning
|
||
await ensureDirHandlePermission(dirHandle);
|
||
|
||
// Build expected-hash lookup from current JSON
|
||
var expectedHashes = {};
|
||
var currentData = json.parse();
|
||
var loadedFiles = (currentData && currentData.payload && Array.isArray(currentData.payload.files)) ? currentData.payload.files : [];
|
||
loadedFiles.forEach(function (f) {
|
||
var key = (f.path ? f.path + '/' : '') + f.filename;
|
||
if (f.sha256) { expectedHashes[key.toLowerCase()] = f.sha256.toLowerCase(); }
|
||
});
|
||
var hasExpected = Object.keys(expectedHashes).length > 0;
|
||
var matchCount = 0;
|
||
var mismatchCount = 0;
|
||
|
||
var hashCells = await scanEntries(dirHandle, hasExpected ? function (item, hash) {
|
||
if (!item.cell) { return; }
|
||
var display = util.formatShortFileHash(hash);
|
||
var fileKey = (item.fileData.path || item.fileData.name).toLowerCase();
|
||
var expected = expectedHashes[fileKey];
|
||
if (expected && expected === hash.toLowerCase()) {
|
||
item.cell.innerHTML = '<span class="hash-match" title="Hash matches expected value">\u2713</span> ' + display;
|
||
matchCount++;
|
||
} else if (expected) {
|
||
item.cell.innerHTML = '<span class="hash-mismatch" title="Hash does NOT match expected value">\u2717</span> ' + display;
|
||
mismatchCount++;
|
||
} else {
|
||
item.cell.textContent = display;
|
||
}
|
||
} : null);
|
||
|
||
if (hasExpected && app.modules.data && app.modules.data.setStatus) {
|
||
if (mismatchCount === 0 && matchCount > 0) {
|
||
app.modules.data.setStatus(matchCount + '/' + hashCells.length + ' files verified', 'success');
|
||
} else if (mismatchCount > 0) {
|
||
app.modules.data.setStatus(mismatchCount + ' hash mismatch(es) \u2014 ' + matchCount + ' matched', 'error');
|
||
}
|
||
}
|
||
|
||
finalizeAfterScan();
|
||
if (!hasExpected) { app.markDirty(); }
|
||
} catch (err) {
|
||
console.error('[transmittal] Refresh failed', err);
|
||
} finally {
|
||
setScanningState(false);
|
||
}
|
||
}
|
||
|
||
// ── Verify row helpers ────────────────────────────────────
|
||
function findRowByFileIndex(idx) {
|
||
var cell = document.querySelector('td[data-index="' + idx + '"]');
|
||
return cell ? cell.closest('tr') : null;
|
||
}
|
||
|
||
function getHashCell(row) {
|
||
return row ? row.querySelector('td:last-child') : null;
|
||
}
|
||
|
||
function setRowVerifyState(row, state) {
|
||
if (!row) { return; }
|
||
row.classList.remove('verify-match', 'verify-mismatch', 'verify-missing', 'verify-new', 'verify-progress');
|
||
if (state) { row.classList.add('verify-' + state); }
|
||
}
|
||
|
||
function clearAllVerifyStates() {
|
||
var rows = document.querySelectorAll('tr.verify-match, tr.verify-mismatch, tr.verify-missing, tr.verify-new, tr.verify-progress');
|
||
rows.forEach(function (r) {
|
||
r.classList.remove('verify-match', 'verify-mismatch', 'verify-missing', 'verify-new', 'verify-progress');
|
||
});
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
var div = document.createElement('div');
|
||
div.textContent = str;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function showPathDiff(trackingCell, expectedPath, actualPath) {
|
||
var diffEl = document.createElement('div');
|
||
diffEl.className = 'path-diff';
|
||
var del = document.createElement('del');
|
||
del.textContent = expectedPath;
|
||
var ins = document.createElement('ins');
|
||
ins.textContent = actualPath;
|
||
diffEl.appendChild(del);
|
||
diffEl.appendChild(document.createTextNode(' \u2192 '));
|
||
diffEl.appendChild(ins);
|
||
trackingCell.appendChild(diffEl);
|
||
}
|
||
|
||
// Hash every directory entry upfront, before any event-loop yields.
|
||
// On file:// origins in Chromium, FileSystemFileHandle.getFile() fails after
|
||
// macrotask boundaries have elapsed since the handle was created. The fix is
|
||
// to call getFile() + hashFile() back-to-back for each entry in one tight
|
||
// sequential pass, producing a complete index before any UI yields occur.
|
||
// Returns { sizeIndex, nameIndex } where each entry has hash already computed.
|
||
async function buildVerifyIndex(entries, onProgress) {
|
||
var sizeIndex = {}; // fileSize → [candidate]
|
||
var nameIndex = {}; // "tracking\trevision" → candidate
|
||
var entryList = []; // List of file metadata for later use
|
||
for (var i = 0; i < entries.length; i++) {
|
||
var entry = entries[i];
|
||
try {
|
||
var file = await entry.handle.getFile();
|
||
var hash = await util.hashFile(file);
|
||
var cand = {
|
||
handle: entry.handle,
|
||
path: entry.path,
|
||
name: file.name,
|
||
fileSize: file.size,
|
||
hash: hash,
|
||
matched: false
|
||
};
|
||
// Store file metadata for later use
|
||
entryList.push({
|
||
handle: entry.handle,
|
||
path: entry.path,
|
||
name: file.name,
|
||
fileSize: file.size,
|
||
hash: hash,
|
||
parsed: (zddc.parseFilename(entry.name) || {})
|
||
});
|
||
// size index
|
||
if (!sizeIndex[file.size]) { sizeIndex[file.size] = []; }
|
||
sizeIndex[file.size].push(cand);
|
||
// name index
|
||
var parsed = (zddc.parseFilename(entry.name) || {});
|
||
if (parsed.trackingNumber) {
|
||
var nkey = parsed.trackingNumber.toLowerCase() + '\t' + (parsed.revision || '').toLowerCase();
|
||
if (!nameIndex[nkey]) { nameIndex[nkey] = []; }
|
||
nameIndex[nkey].push(cand);
|
||
}
|
||
if (onProgress) { onProgress(i + 1, entries.length); }
|
||
} catch (err) {
|
||
console.warn('[transmittal] verify skip entry', entry.path, err);
|
||
}
|
||
}
|
||
return { sizeIndex: sizeIndex, nameIndex: nameIndex, entryList: entryList };
|
||
}
|
||
|
||
// Find a matching directory entry by sha256 hash.
|
||
function findByHash(sizeIndex, fileSize, expectedHash) {
|
||
if (!expectedHash || fileSize == null) { return null; }
|
||
var candidates = sizeIndex[fileSize];
|
||
if (!candidates) { return null; }
|
||
var target = expectedHash.toLowerCase();
|
||
for (var c = 0; c < candidates.length; c++) {
|
||
var cand = candidates[c];
|
||
if (cand.matched) { continue; }
|
||
if (cand.hash && cand.hash.toLowerCase() === target) {
|
||
cand.matched = true;
|
||
return cand;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Find a matching directory entry by tracking number + revision.
|
||
function findByTrackingRevision(nameIndex, trackingNumber, revision) {
|
||
var key = (trackingNumber || '').toLowerCase() + '\t' + (revision || '').toLowerCase();
|
||
var candidates = nameIndex[key];
|
||
if (!candidates || !candidates.length) { return null; }
|
||
for (var i = 0; i < candidates.length; i++) {
|
||
if (!candidates[i].matched) {
|
||
candidates[i].matched = true;
|
||
return candidates[i];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Count unmatched entries in a size index
|
||
function countUnmatched(sizeIndex) {
|
||
var count = 0;
|
||
for (var size in sizeIndex) {
|
||
var group = sizeIndex[size];
|
||
for (var i = 0; i < group.length; i++) {
|
||
if (!group[i].matched) { count++; }
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
async function verifyFiles() {
|
||
var isPublished = !!app.state.published;
|
||
var setStatus = app.modules.data && app.modules.data.setStatus;
|
||
try {
|
||
var dirHandle = app.data.selectedDirHandle;
|
||
if (!dirHandle) {
|
||
dirHandle = await ensureDirHandle();
|
||
}
|
||
clearAllVerifyStates();
|
||
if (setStatus) { setStatus('Hashing directory\u2026', 'info'); }
|
||
|
||
var entries = await collectDirectoryEntries(dirHandle);
|
||
var allFiles = app.data.files || [];
|
||
|
||
// Hash every file upfront before any UI yields — on file:// origins,
|
||
// Chromium invalidates FileSystemFileHandle after macrotask boundaries.
|
||
var idx = await buildVerifyIndex(entries);
|
||
var sizeIndex = idx.sizeIndex;
|
||
var nameIndex = idx.nameIndex;
|
||
var entryList = idx.entryList || [];
|
||
|
||
// Build pasteKey index for existing files to avoid duplicates
|
||
var existingIndex = buildExistingIndex(allFiles);
|
||
var existingPasteKeys = Object.keys(existingIndex);
|
||
|
||
if (isPublished) {
|
||
// ── Published: read-only verification by hash ──
|
||
var verified = 0;
|
||
var missingCount = 0;
|
||
|
||
for (var p = 0; p < allFiles.length; p++) {
|
||
var tf = allFiles[p];
|
||
var row = findRowByFileIndex(p);
|
||
var hCell = getHashCell(row);
|
||
var trackingCell = row ? row.querySelector('td[data-field="trackingNumber"]') : null;
|
||
|
||
setRowVerifyState(row, 'progress');
|
||
await new Promise(function (r) { setTimeout(r, 0); });
|
||
|
||
var fileSize = tf.fileSize != null ? tf.fileSize : tf.size;
|
||
var found = findByHash(sizeIndex, fileSize, tf.sha256);
|
||
|
||
if (found) {
|
||
verified++;
|
||
tf.fileHandle = found.handle;
|
||
setRowVerifyState(row, 'match');
|
||
if (hCell) {
|
||
hCell.innerHTML = '<span style="color:#166534;font-weight:700;">\u2713</span> ' + escapeHtml(util.formatShortFileHash(tf.sha256));
|
||
}
|
||
var expectedPath = tf.path || tf.name || '';
|
||
if (trackingCell && expectedPath && found.path && expectedPath !== found.path) {
|
||
showPathDiff(trackingCell, expectedPath, found.path);
|
||
}
|
||
} else {
|
||
missingCount++;
|
||
setRowVerifyState(row, 'missing');
|
||
if (hCell) {
|
||
hCell.innerHTML = '<span style="color:#92400e;">\u26A0 not found</span>';
|
||
}
|
||
}
|
||
}
|
||
|
||
var extra = countUnmatched(sizeIndex);
|
||
var msg = verified + ' verified';
|
||
if (missingCount) { msg += ', ' + missingCount + ' missing'; }
|
||
if (extra) { msg += ', ' + extra + ' extra in directory'; }
|
||
if (setStatus) {
|
||
setStatus(msg, missingCount ? 'error' : 'success');
|
||
}
|
||
} else {
|
||
// ── Edit: match rows against directory ──
|
||
// Rows WITH hash+size → match by size+hash
|
||
// Rows WITHOUT hash → match by tracking+revision, then populate hash/size
|
||
var verified = 0;
|
||
var populated = 0;
|
||
var notFound = 0;
|
||
var dirty = false;
|
||
|
||
for (var mi = 0; mi < allFiles.length; mi++) {
|
||
var mf = allFiles[mi];
|
||
var mRow = findRowByFileIndex(mi);
|
||
var mhCell = getHashCell(mRow);
|
||
|
||
setRowVerifyState(mRow, 'progress');
|
||
await new Promise(function (r) { setTimeout(r, 0); });
|
||
|
||
var hasHash = !!mf.sha256;
|
||
var mFileSize = mf.fileSize != null ? mf.fileSize : mf.size;
|
||
|
||
if (hasHash && mFileSize != null) {
|
||
var foundByHash = findByHash(sizeIndex, mFileSize, mf.sha256);
|
||
if (foundByHash) {
|
||
mf.fileHandle = foundByHash.handle;
|
||
verified++;
|
||
setRowVerifyState(mRow, 'match');
|
||
if (mhCell) {
|
||
mhCell.innerHTML = '<span style="color:#166534;font-weight:700;">\u2713</span> ' + escapeHtml(util.formatShortFileHash(mf.sha256));
|
||
}
|
||
} else {
|
||
mf.fileHandle = null;
|
||
notFound++;
|
||
setRowVerifyState(mRow, 'missing');
|
||
if (mhCell) {
|
||
mhCell.innerHTML = '<span style="color:#92400e;">\u26A0 not found</span>';
|
||
}
|
||
}
|
||
} else {
|
||
var foundByName = findByTrackingRevision(nameIndex, mf.trackingNumber, mf.revision);
|
||
if (foundByName) {
|
||
mf.fileHandle = foundByName.handle;
|
||
mf.path = foundByName.path;
|
||
mf.name = foundByName.name;
|
||
mf.size = foundByName.fileSize;
|
||
mf.fileSize = foundByName.fileSize;
|
||
mf.sha256 = foundByName.hash;
|
||
mf.extension = mf.extension || zddc.splitExtension(foundByName.name).extension;
|
||
populated++;
|
||
dirty = true;
|
||
setRowVerifyState(mRow, 'match');
|
||
if (mhCell) {
|
||
mhCell.innerHTML = '<span style="color:#166534;font-weight:700;">\u2713</span> ' + escapeHtml(util.formatShortFileHash(mf.sha256));
|
||
}
|
||
} else {
|
||
notFound++;
|
||
setRowVerifyState(mRow, 'missing');
|
||
if (mhCell) {
|
||
mhCell.innerHTML = '<span style="color:#92400e;">\u26A0 not found</span>';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (dirty) {
|
||
updateFilesInJson(allFiles);
|
||
filesModule.render();
|
||
app.state.apply();
|
||
app.markDirty();
|
||
}
|
||
|
||
var editMsg = '';
|
||
var parts = [];
|
||
if (verified) { parts.push(verified + ' verified'); }
|
||
if (populated) { parts.push(populated + ' matched'); }
|
||
if (notFound) { parts.push(notFound + ' not found'); }
|
||
editMsg = parts.join(', ') || 'No files to verify';
|
||
|
||
// Add new files from directory that don't match existing pasteKeys
|
||
var added = 0;
|
||
for (var ei = 0; ei < entryList.length; ei++) {
|
||
var entryData = entryList[ei];
|
||
if (!entryData.parsed.trackingNumber && !entryData.parsed.revision) continue; // Skip files without ZDDC pattern
|
||
var pasteKey = (entryData.parsed.trackingNumber || '').toLowerCase() + '|' + (entryData.parsed.revision || '').toLowerCase();
|
||
if (existingPasteKeys.indexOf(pasteKey) === -1) {
|
||
// New file - use pre-hashed file data
|
||
var fileData = {
|
||
path: entryData.path,
|
||
name: entryData.name,
|
||
size: entryData.fileSize,
|
||
fileSize: entryData.fileSize,
|
||
sha256: '',
|
||
trackingNumber: entryData.parsed.trackingNumber,
|
||
title: entryData.parsed.title,
|
||
revision: entryData.parsed.revision,
|
||
status: entryData.parsed.status,
|
||
extension: entryData.parsed.extension || zddc.splitExtension(entryData.name).extension,
|
||
fileHandle: entryData.handle
|
||
};
|
||
app.data.files.push(fileData);
|
||
added++;
|
||
}
|
||
}
|
||
if (added > 0) {
|
||
dirty = true;
|
||
updateFilesInJson(app.data.files);
|
||
filesModule.render();
|
||
app.state.apply();
|
||
app.markDirty();
|
||
editMsg = (editMsg ? editMsg + ', ' : '') + added + ' new file(s) added';
|
||
}
|
||
|
||
if (setStatus) {
|
||
setStatus(editMsg, notFound && !added ? 'error' : 'success');
|
||
}
|
||
}
|
||
} catch (err) {
|
||
if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) {
|
||
if (DEBUG) console.log('[transmittal] User cancelled verify');
|
||
} else {
|
||
console.error('[transmittal] verifyFiles failed', err);
|
||
if (setStatus) { setStatus('Verify failed: ' + (err.message || err), 'error'); }
|
||
}
|
||
}
|
||
}
|
||
|
||
document.addEventListener('transmittal:scan-directory', function () {
|
||
selectDirectory({ currentTarget: dom.qs('#bottom-primary') });
|
||
});
|
||
|
||
document.addEventListener('transmittal:verify-directory', function () {
|
||
verifyFiles();
|
||
});
|
||
|
||
filesModule.bindActionButtons = function bindActionButtons() {
|
||
// Reveal the menu now that JS is running
|
||
var bottomMenu = dom.qs('#bottom-menu');
|
||
if (bottomMenu) { bottomMenu.hidden = false; }
|
||
var noJsNotice = dom.qs('#no-js-notice');
|
||
if (noJsNotice) { dom.show(noJsNotice, false); }
|
||
initBottomBarToggle();
|
||
updateToolbars();
|
||
};
|
||
|
||
filesModule.updateToolbars = updateToolbars;
|
||
|
||
filesModule.syncUiToJson = function syncUiToJson() {
|
||
const val = function (selector) {
|
||
const el = dom.qs(selector);
|
||
return el ? (el.value || '') : '';
|
||
};
|
||
|
||
function toIsoDate(value) {
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
const trimmed = String(value).trim();
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
||
return trimmed;
|
||
}
|
||
const match = trimmed.match(/^(?:[A-Za-z]+\s+)?([A-Za-z]{3,})\s+(\d{1,2}),\s*(\d{4})$/);
|
||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||
if (match) {
|
||
const monthIndex = months.indexOf(match[1].slice(0, 3));
|
||
if (monthIndex >= 0) {
|
||
const day = String(parseInt(match[2], 10)).padStart(2, '0');
|
||
const month = String(monthIndex + 1).padStart(2, '0');
|
||
const year = match[3];
|
||
return year + '-' + month + '-' + day;
|
||
}
|
||
}
|
||
const date = new Date(trimmed);
|
||
if (!Number.isNaN(date.getTime())) {
|
||
return date.toISOString().slice(0, 10);
|
||
}
|
||
return trimmed;
|
||
}
|
||
|
||
const data = json.parse();
|
||
const envelope = { ...(data.envelope || {}) };
|
||
const presentation = { ...(data.presentation || {}) };
|
||
|
||
const payload = {
|
||
version: 1,
|
||
type: val('#type'),
|
||
title: val('#title') || '',
|
||
client: (dom.qs('#owner-name')?.textContent) || '',
|
||
project: (dom.qs('#project-name')?.textContent) || '',
|
||
projectNumber: (dom.qs('#project-number')?.textContent) || '',
|
||
date: toIsoDate(val('#date')),
|
||
trackingNumber: val('#tracking-number'),
|
||
from: val('#from'),
|
||
to: val('#to'),
|
||
purpose: val('#purpose'),
|
||
responseDue: val('#response-due'),
|
||
subject: val('#subject'),
|
||
remarks: val('#remarks'),
|
||
files: (function () {
|
||
var memFiles = Array.isArray(app.data.files) ? app.data.files : [];
|
||
var sorted = memFiles.slice().sort(util.compareFilesByTrackingRevision).map(canonicalFilePayload);
|
||
var self = buildSelfEntry();
|
||
return [canonicalFilePayload(self)].concat(sorted);
|
||
})()
|
||
};
|
||
|
||
const leftLogoEl = dom.qs('#left-logo');
|
||
const rightLogoEl = dom.qs('#right-logo');
|
||
presentation.leftLogo = leftLogoEl && leftLogoEl.src ? leftLogoEl.src : '';
|
||
presentation.rightLogo = rightLogoEl && rightLogoEl.src ? rightLogoEl.src : '';
|
||
|
||
json.setData({ envelope: envelope, payload: payload, presentation: presentation });
|
||
};
|
||
|
||
filesModule.loadFromJson = function loadFromJson(options) {
|
||
const opts = options || {};
|
||
const data = json.parse();
|
||
const payload = (data && data.payload) || {};
|
||
if (!opts.filesOnly) {
|
||
const assignValue = function (selector, value) {
|
||
const element = dom.qs(selector);
|
||
if (element) {
|
||
element.value = value || '';
|
||
}
|
||
};
|
||
const assignText = function (selector, value) {
|
||
const element = dom.qs(selector);
|
||
if (element) {
|
||
element.textContent = value || '';
|
||
}
|
||
};
|
||
assignValue('#type', payload.type || 'Transmittal');
|
||
var typeDisplay = dom.qs('#type-display');
|
||
if (typeDisplay) { typeDisplay.textContent = payload.type || 'Transmittal'; }
|
||
assignValue('#title', payload.title || '');
|
||
assignText('#owner-name', payload.client || '');
|
||
assignText('#project-name', payload.project || '');
|
||
assignText('#project-number', payload.projectNumber || '');
|
||
assignValue('#date', payload.date || '');
|
||
assignValue('#tracking-number', payload.trackingNumber || '');
|
||
assignValue('#from', payload.from || '');
|
||
assignValue('#to', payload.to || '');
|
||
assignValue('#purpose', payload.purpose || '');
|
||
assignValue('#response-due', payload.responseDue || '');
|
||
assignValue('#subject', payload.subject || '');
|
||
const remarks = dom.qs('#remarks');
|
||
if (remarks) {
|
||
remarks.value = payload.remarks || '';
|
||
}
|
||
|
||
// Load logos from presentation data
|
||
const presentation = (data && data.presentation) || {};
|
||
const leftLogoEl = dom.qs('#left-logo');
|
||
const rightLogoEl = dom.qs('#right-logo');
|
||
if (leftLogoEl && presentation.leftLogo) {
|
||
leftLogoEl.src = presentation.leftLogo;
|
||
}
|
||
if (rightLogoEl && presentation.rightLogo) {
|
||
rightLogoEl.src = presentation.rightLogo;
|
||
}
|
||
}
|
||
var SELF_HASH = app.constants.SELF_HASH;
|
||
const files = Array.isArray(payload.files)
|
||
? payload.files.filter(function (f) { return f.sha256 !== SELF_HASH; })
|
||
: [];
|
||
app.data.files = files.map(function (entry) {
|
||
const pathOnly = entry.path || '';
|
||
const filename = entry.filename || '';
|
||
const relativePath = pathOnly ? (pathOnly + '/' + filename) : filename;
|
||
|
||
// When filename is empty (e.g., from pasted files), reconstruct from trackingNumber
|
||
let baseName = filename || relativePath.split('/').pop() || '';
|
||
if (!baseName && entry.trackingNumber) {
|
||
baseName = zddc.joinExtension(entry.trackingNumber, entry.extension || '');
|
||
}
|
||
|
||
const fileSize = entry.fileSize || entry.size || 0;
|
||
return {
|
||
path: relativePath,
|
||
name: baseName,
|
||
size: fileSize,
|
||
fileSize: fileSize,
|
||
sha256: entry.sha256 || '',
|
||
trackingNumber: entry.trackingNumber || '',
|
||
title: entry.title || '',
|
||
revision: entry.revision || '',
|
||
status: entry.status || '',
|
||
extension: zddc.splitExtension(baseName).extension
|
||
};
|
||
});
|
||
sortFilesInPlace(app.data.files);
|
||
// Don't call updateFilesInJson here - it clears digest/signatures
|
||
// The files are already in the JSON, we're just loading them into app.data.files
|
||
updateDirectoryIndicator(app.data.selectedDirHandle ? app.data.selectedDirHandle.name : '');
|
||
|
||
// Render the table to populate it with file data
|
||
filesModule.render();
|
||
|
||
if (!opts.filesOnly && app.modules.markdown && typeof app.modules.markdown.refresh === 'function') {
|
||
app.modules.markdown.refresh();
|
||
}
|
||
// Apply field visibility after loading data to ensure UI stays in sync
|
||
if (!opts.filesOnly && app.modules.visibility && typeof app.modules.visibility.applyFieldVisibility === 'function') {
|
||
app.modules.visibility.applyFieldVisibility();
|
||
}
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
updateDirectoryIndicator(app.data.selectedDirHandle ? app.data.selectedDirHandle.name : '');
|
||
filesModule.bindActionButtons();
|
||
filesModule.setupTableEditing();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
var dom = app.dom;
|
||
var util = app.util;
|
||
var filesModule = app.modules.files;
|
||
|
||
var ARCHIVE_DIR_NAME = '.archive';
|
||
|
||
function sanitizeUrlSegment(value, fallback) {
|
||
var str = (value || '').toString().trim();
|
||
if (!str) {
|
||
return fallback;
|
||
}
|
||
var cleaned = str
|
||
.replace(/[^A-Za-z0-9._~-]+/g, '-')
|
||
.replace(/-+/g, '-')
|
||
.replace(/^-+|-+$/g, '');
|
||
return cleaned || fallback;
|
||
}
|
||
|
||
function encodeRelativePath(path) {
|
||
return path.split('/').map(function (segment) {
|
||
return encodeURIComponent(segment);
|
||
}).join('/');
|
||
}
|
||
|
||
function buildRedirectHtml(targetHref, displayName) {
|
||
var safeHrefAttr = util.escapeHtmlAttribute(targetHref);
|
||
var safeLabel = util.escapeHtml(displayName || targetHref);
|
||
return '<!DOCTYPE html>\n' +
|
||
'<html lang="en">\n' +
|
||
'<head>\n' +
|
||
'<meta charset="utf-8">\n' +
|
||
'<meta http-equiv="refresh" content="0; url=' + safeHrefAttr + '">\n' +
|
||
'<title>' + safeLabel + '</title>\n' +
|
||
'</head>\n' +
|
||
'<body>\n' +
|
||
'<p>Redirecting to <a href="' + safeHrefAttr + '">' + safeLabel + '</a>.</p>\n' +
|
||
'</body>\n' +
|
||
'</html>\n';
|
||
}
|
||
|
||
async function ensureArchiveDirectory(rootHandle) {
|
||
return rootHandle.getDirectoryHandle(ARCHIVE_DIR_NAME, { create: true });
|
||
}
|
||
|
||
async function writeRedirectFile(directoryHandle, filename, html) {
|
||
var fileHandle = await directoryHandle.getFileHandle(filename, { create: true });
|
||
var writable = await fileHandle.createWritable();
|
||
await writable.write(new Blob([html], { type: 'text/html' }));
|
||
await writable.close();
|
||
}
|
||
|
||
function groupFilesByTracking(files) {
|
||
var map = new Map();
|
||
files.forEach(function (file) {
|
||
var key = file.trackingNumber || '';
|
||
if (!map.has(key)) {
|
||
map.set(key, []);
|
||
}
|
||
map.get(key).push(file);
|
||
});
|
||
return map;
|
||
}
|
||
|
||
function determineLatestByTracking(grouped) {
|
||
var latest = new Map();
|
||
grouped.forEach(function (files, tracking) {
|
||
if (!files || !files.length) {
|
||
return;
|
||
}
|
||
var sorted = files.slice().sort(function (a, b) {
|
||
var revisionCompare = util.compareRevisionPriority(a.revision, b.revision);
|
||
if (revisionCompare !== 0) {
|
||
return revisionCompare;
|
||
}
|
||
var pathCompare = (a.path || '').localeCompare(b.path || '');
|
||
if (pathCompare !== 0) {
|
||
return pathCompare;
|
||
}
|
||
return (a.name || '').localeCompare(b.name || '');
|
||
});
|
||
latest.set(tracking, sorted[sorted.length - 1]);
|
||
});
|
||
return latest;
|
||
}
|
||
|
||
function buildRedirectFilenames(file) {
|
||
var trackingSegment = sanitizeUrlSegment(file.trackingNumber || '', 'tracking');
|
||
var rawRevision = (file.revision || '').trim();
|
||
var revisionSegment = rawRevision ? sanitizeUrlSegment(rawRevision, 'rev') : '';
|
||
var rawHash = (file.sha256 || '').trim();
|
||
var hashSegment = rawHash ? sanitizeUrlSegment(rawHash, 'hash') : '';
|
||
var htmlSuffix = '.html';
|
||
|
||
var revisionFilename = revisionSegment ? (trackingSegment + '_' + revisionSegment + htmlSuffix) : '';
|
||
var hashFilename = hashSegment ? (hashSegment + htmlSuffix) : '';
|
||
var latestFilename = trackingSegment + htmlSuffix;
|
||
|
||
return {
|
||
latestFilename: latestFilename,
|
||
revisionFilename: revisionFilename,
|
||
hashFilename: hashFilename
|
||
};
|
||
}
|
||
|
||
async function resolveFileMetadata(entry) {
|
||
var parsed = zddc.parseFilename(entry.name || entry.path || '') || {};
|
||
if (!parsed.trackingNumber) {
|
||
return null;
|
||
}
|
||
var file = await entry.handle.getFile();
|
||
var sha256 = await util.hashFile(file);
|
||
return {
|
||
path: entry.path,
|
||
name: file.name,
|
||
extension: parsed.extension || zddc.splitExtension(file.name).extension,
|
||
trackingNumber: parsed.trackingNumber,
|
||
revision: parsed.revision,
|
||
status: parsed.status,
|
||
title: parsed.title,
|
||
sha256: sha256,
|
||
size: file.size
|
||
};
|
||
}
|
||
|
||
function buildRelativeHref(relativePath) {
|
||
var encoded = encodeRelativePath(relativePath);
|
||
return '../' + encoded;
|
||
}
|
||
|
||
async function createRedirectFiles(archiveHandle, file, isLatest) {
|
||
var filenames = buildRedirectFilenames(file);
|
||
var href = buildRelativeHref(file.path);
|
||
var html = buildRedirectHtml(href, file.name);
|
||
|
||
var writes = [];
|
||
if (filenames.revisionFilename) {
|
||
writes.push(writeRedirectFile(archiveHandle, filenames.revisionFilename, html));
|
||
}
|
||
if (filenames.hashFilename) {
|
||
writes.push(writeRedirectFile(archiveHandle, filenames.hashFilename, html));
|
||
}
|
||
if (isLatest) {
|
||
writes.push(writeRedirectFile(archiveHandle, filenames.latestFilename, html));
|
||
}
|
||
|
||
await Promise.all(writes);
|
||
return true;
|
||
}
|
||
|
||
async function pickIndexDirectory() {
|
||
if (typeof window.showDirectoryPicker !== 'function') {
|
||
throw new Error('File System Access API showDirectoryPicker is required');
|
||
}
|
||
return window.showDirectoryPicker();
|
||
}
|
||
|
||
filesModule.generateArchiveRedirects = async function generateArchiveRedirects() {
|
||
var rootHandle;
|
||
try {
|
||
rootHandle = await pickIndexDirectory();
|
||
} catch (err) {
|
||
if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) {
|
||
console.log('[transmittal] Create Index cancelled.');
|
||
return;
|
||
}
|
||
throw err;
|
||
}
|
||
|
||
var overallStart = filesModule.nowMs();
|
||
var entries = await filesModule.collectDirectoryEntries(rootHandle);
|
||
if (!entries.length) {
|
||
console.log('[transmittal] Create Index: No files found.');
|
||
return;
|
||
}
|
||
|
||
var archiveHandle = await ensureArchiveDirectory(rootHandle);
|
||
var files = [];
|
||
for (var i = 0; i < entries.length; i++) {
|
||
try {
|
||
var metadata = await resolveFileMetadata(entries[i]);
|
||
if (metadata) {
|
||
files.push(metadata);
|
||
}
|
||
} catch (err) {
|
||
console.warn('[transmittal] create-index skip', entries[i].path, err);
|
||
}
|
||
}
|
||
if (!files.length) {
|
||
console.log('[transmittal] Create Index: No ZDDC files found.');
|
||
return;
|
||
}
|
||
|
||
var grouped = groupFilesByTracking(files);
|
||
var latest = determineLatestByTracking(grouped);
|
||
|
||
var writePromises = files.map(function (file) {
|
||
var isLatest = latest.get(file.trackingNumber) === file;
|
||
return createRedirectFiles(archiveHandle, file, isLatest)
|
||
.catch(function (err) {
|
||
console.error('[transmittal] create-index write failed', file.path, err);
|
||
return null;
|
||
});
|
||
});
|
||
|
||
var results = await Promise.all(writePromises);
|
||
var created = results.filter(function (result) { return result !== null; }).length;
|
||
|
||
var totalElapsed = filesModule.nowMs() - overallStart;
|
||
console.log('[transmittal] Create Index: Generated redirects for ' + created + ' files in ' + filesModule.formatDuration(totalElapsed) + '.');
|
||
};
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
var dom = app.dom;
|
||
var util = app.util;
|
||
var filesModule = app.modules.files;
|
||
|
||
// Build ZDDC filename from file row data — delegates to shared zddc.formatFilename()
|
||
function buildZddcFileName(fileData, droppedExt) {
|
||
var ext = (fileData.extension || droppedExt || '').toLowerCase().replace(/^\.+/, '');
|
||
return zddc.formatFilename({
|
||
trackingNumber: fileData.trackingNumber || '',
|
||
revision: fileData.revision || '',
|
||
status: fileData.status || '',
|
||
title: fileData.title || '',
|
||
extension: ext,
|
||
});
|
||
}
|
||
|
||
function setupRowDropTargets() {
|
||
if (app.state.mode !== 'edit') { return; }
|
||
var rows = document.querySelectorAll('table tbody tr:not(.self-entry)');
|
||
rows.forEach(function (row) {
|
||
row.addEventListener('dragover', function (e) {
|
||
if (app.state.mode !== 'edit') { return; }
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
row.classList.add('ring-2', 'ring-blue-400', 'bg-blue-50');
|
||
});
|
||
row.addEventListener('dragleave', function () {
|
||
row.classList.remove('ring-2', 'ring-blue-400', 'bg-blue-50');
|
||
});
|
||
row.addEventListener('drop', async function (e) {
|
||
row.classList.remove('ring-2', 'ring-blue-400', 'bg-blue-50');
|
||
if (app.state.mode !== 'edit') { return; }
|
||
var droppedFile = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
||
if (!droppedFile) { return; }
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
var setStatus = app.modules.data && app.modules.data.setStatus;
|
||
|
||
// Prompt for directory if none selected
|
||
if (!app.data.selectedDirHandle) {
|
||
try {
|
||
app.data.selectedDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||
app.data.selectedDirName = app.data.selectedDirHandle.name || '';
|
||
if (filesModule.updateDirectoryIndicator) {
|
||
filesModule.updateDirectoryIndicator();
|
||
}
|
||
} catch (pickErr) {
|
||
if (setStatus) { setStatus('A directory is required to copy files', 'error'); }
|
||
return;
|
||
}
|
||
}
|
||
|
||
var indexCell = row.querySelector('td[data-index]');
|
||
if (!indexCell) { return; }
|
||
var fileIndex = Number(indexCell.dataset.index);
|
||
var fileData = app.data.files[fileIndex];
|
||
if (!fileData) { return; }
|
||
|
||
var droppedExt = zddc.splitExtension(droppedFile.name).extension;
|
||
var zddcName = buildZddcFileName(fileData, droppedExt);
|
||
if (!zddcName) { zddcName = droppedFile.name; }
|
||
|
||
var hashCell = row.querySelector('td:last-child');
|
||
try {
|
||
if (hashCell) { hashCell.textContent = 'copying\u2026'; }
|
||
var dirHandle = app.data.selectedDirHandle;
|
||
var newHandle = await dirHandle.getFileHandle(zddcName, { create: true });
|
||
var writable = await newHandle.createWritable();
|
||
await writable.write(droppedFile);
|
||
await writable.close();
|
||
|
||
if (hashCell) { hashCell.textContent = 'hashing\u2026'; }
|
||
var written = await newHandle.getFile();
|
||
var hash = await util.hashFile(written);
|
||
|
||
fileData.fileHandle = newHandle;
|
||
fileData.path = zddcName;
|
||
fileData.name = zddcName;
|
||
fileData.size = written.size;
|
||
fileData.fileSize = written.size;
|
||
fileData.sha256 = hash;
|
||
if (!fileData.extension) { fileData.extension = droppedExt; }
|
||
|
||
filesModule.updateFilesInJson(app.data.files);
|
||
filesModule.render();
|
||
app.state.apply();
|
||
app.markDirty();
|
||
if (setStatus) { setStatus('Copied \u2192 ' + zddcName, 'success'); }
|
||
} catch (err) {
|
||
console.error('[transmittal] row file drop failed', err);
|
||
if (hashCell) { hashCell.textContent = 'error'; }
|
||
if (setStatus) { setStatus('Drop failed: ' + (err.message || err), 'error'); }
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
filesModule.clearTable = function clearTable() {
|
||
var tbody = document.querySelector('table tbody');
|
||
if (tbody) { tbody.innerHTML = ''; }
|
||
};
|
||
|
||
filesModule.renderSingleRow = function renderSingleRow(file, index) {
|
||
var tbody = document.querySelector('table tbody');
|
||
if (!tbody) { return null; }
|
||
var row = document.createElement('tr');
|
||
|
||
var numCell = document.createElement('td');
|
||
numCell.className = 'px-2 py-1 align-top text-center text-gray-400';
|
||
numCell.textContent = String(index + 1);
|
||
row.appendChild(numCell);
|
||
|
||
var trackingCell = document.createElement('td');
|
||
trackingCell.className = 'px-2 py-1 align-top whitespace-nowrap font-mono';
|
||
var link = document.createElement('a');
|
||
link.textContent = file.trackingNumber || '';
|
||
var relativePath = file.path || file.name || '';
|
||
if (relativePath) {
|
||
link.href = encodeURI(relativePath);
|
||
link.dataset.relativePath = relativePath;
|
||
} else {
|
||
link.href = '#';
|
||
}
|
||
link.title = relativePath || (file.name || '');
|
||
link.className = 'text-blue-600 hover:underline';
|
||
var ext = (file.extension || zddc.splitExtension(relativePath).extension);
|
||
if (app.constants.viewableExts.indexOf(ext) !== -1) {
|
||
link.target = '_blank';
|
||
link.rel = 'noopener';
|
||
} else {
|
||
var filename = (relativePath.split('/').pop() || file.name || 'download');
|
||
link.setAttribute('download', filename);
|
||
}
|
||
trackingCell.appendChild(link);
|
||
trackingCell.contentEditable = 'false';
|
||
trackingCell.dataset.index = String(index);
|
||
trackingCell.dataset.field = 'trackingNumber';
|
||
row.appendChild(trackingCell);
|
||
|
||
var titleCell = document.createElement('td');
|
||
titleCell.className = 'px-2 py-1 align-top whitespace-normal break-words w-full';
|
||
titleCell.textContent = file.title || '';
|
||
titleCell.contentEditable = (app.state.mode === 'edit').toString();
|
||
titleCell.setAttribute('role', 'textbox');
|
||
titleCell.setAttribute('aria-multiline', 'false');
|
||
titleCell.setAttribute('tabindex', '0');
|
||
titleCell.dataset.index = String(index);
|
||
titleCell.dataset.field = 'title';
|
||
row.appendChild(titleCell);
|
||
|
||
var revisionCell = document.createElement('td');
|
||
revisionCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
|
||
revisionCell.textContent = file.revision || '';
|
||
revisionCell.contentEditable = (app.state.mode === 'edit').toString();
|
||
revisionCell.setAttribute('role', 'textbox');
|
||
revisionCell.setAttribute('aria-multiline', 'false');
|
||
revisionCell.setAttribute('tabindex', '0');
|
||
revisionCell.dataset.index = String(index);
|
||
revisionCell.dataset.field = 'revision';
|
||
row.appendChild(revisionCell);
|
||
|
||
var statusCell = document.createElement('td');
|
||
statusCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
|
||
statusCell.textContent = file.status || '';
|
||
statusCell.contentEditable = (app.state.mode === 'edit').toString();
|
||
statusCell.setAttribute('role', 'textbox');
|
||
statusCell.setAttribute('aria-multiline', 'false');
|
||
statusCell.setAttribute('tabindex', '0');
|
||
statusCell.dataset.index = String(index);
|
||
statusCell.dataset.field = 'status';
|
||
row.appendChild(statusCell);
|
||
|
||
var extCell = document.createElement('td');
|
||
extCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
|
||
extCell.textContent = (file.extension || '').toLowerCase();
|
||
extCell.contentEditable = 'false';
|
||
row.appendChild(extCell);
|
||
|
||
var sizeCell = document.createElement('td');
|
||
sizeCell.className = 'px-2 py-1 align-top whitespace-nowrap text-right font-mono';
|
||
var fileSizeValue = (file.fileSize != null ? file.fileSize : file.size);
|
||
sizeCell.textContent = util.formatFileSize(fileSizeValue);
|
||
sizeCell.contentEditable = 'false';
|
||
row.appendChild(sizeCell);
|
||
|
||
var hashCell = document.createElement('td');
|
||
hashCell.className = 'px-2 py-1 align-top font-mono text-[9px] whitespace-normal break-all leading-snug';
|
||
if (file.sha256) {
|
||
hashCell.textContent = util.formatShortFileHash(file.sha256);
|
||
} else {
|
||
var prog = document.createElement('div');
|
||
prog.className = 'hash-progress';
|
||
var bar = document.createElement('div');
|
||
bar.className = 'hash-progress-bar';
|
||
var fill = document.createElement('div');
|
||
fill.className = 'hash-progress-fill';
|
||
bar.appendChild(fill);
|
||
prog.appendChild(bar);
|
||
hashCell.appendChild(prog);
|
||
}
|
||
row.appendChild(hashCell);
|
||
|
||
tbody.appendChild(row);
|
||
return hashCell;
|
||
};
|
||
|
||
function renderSelfRow(tbody, rowNum) {
|
||
var self = filesModule.buildSelfEntry ? filesModule.buildSelfEntry() : null;
|
||
if (!self) { return; }
|
||
var row = document.createElement('tr');
|
||
row.className = 'self-entry';
|
||
|
||
var numCell = document.createElement('td');
|
||
numCell.className = 'px-2 py-1 align-top text-center text-gray-400';
|
||
numCell.textContent = String(rowNum);
|
||
row.appendChild(numCell);
|
||
|
||
var trackingCell = document.createElement('td');
|
||
trackingCell.className = 'px-2 py-1 align-top whitespace-nowrap font-mono text-gray-500';
|
||
var selfPath = self.path || self.name || '';
|
||
if (selfPath) {
|
||
var link = document.createElement('a');
|
||
link.href = encodeURI(selfPath);
|
||
link.textContent = self.trackingNumber || '';
|
||
link.className = 'text-gray-500 hover:underline';
|
||
link.target = '_blank';
|
||
link.rel = 'noopener';
|
||
trackingCell.appendChild(link);
|
||
} else {
|
||
trackingCell.textContent = self.trackingNumber || '';
|
||
}
|
||
row.appendChild(trackingCell);
|
||
|
||
var titleCell = document.createElement('td');
|
||
titleCell.className = 'px-2 py-1 align-top whitespace-normal break-words w-full text-gray-500';
|
||
titleCell.textContent = self.title || '';
|
||
row.appendChild(titleCell);
|
||
|
||
var revisionCell = document.createElement('td');
|
||
revisionCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono text-gray-500';
|
||
revisionCell.textContent = self.revision || '';
|
||
row.appendChild(revisionCell);
|
||
|
||
var statusCell = document.createElement('td');
|
||
statusCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono text-gray-500';
|
||
statusCell.textContent = self.status || '';
|
||
row.appendChild(statusCell);
|
||
|
||
var extCell = document.createElement('td');
|
||
extCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono text-gray-500';
|
||
extCell.textContent = 'html';
|
||
row.appendChild(extCell);
|
||
|
||
var sizeCell = document.createElement('td');
|
||
sizeCell.className = 'px-2 py-1 align-top whitespace-nowrap text-right font-mono text-gray-400';
|
||
sizeCell.textContent = '\u2014';
|
||
row.appendChild(sizeCell);
|
||
|
||
var hashCell = document.createElement('td');
|
||
hashCell.className = 'px-2 py-1 align-top font-mono text-[9px] whitespace-nowrap text-gray-400 italic';
|
||
hashCell.textContent = 'see above';
|
||
row.appendChild(hashCell);
|
||
|
||
tbody.appendChild(row);
|
||
}
|
||
|
||
filesModule.render = function render() {
|
||
var tbody = document.querySelector('table tbody');
|
||
if (!tbody) {
|
||
return;
|
||
}
|
||
tbody.innerHTML = '';
|
||
filesModule.sortFilesInPlace(app.data.files);
|
||
var filters = app.modules.filters ? app.modules.filters.getActiveFilters() : {};
|
||
var filtered = [];
|
||
(app.data.files || []).forEach(function (file, originalIndex) {
|
||
if (!app.modules.filters || app.modules.filters.fileMatchesFilters(file, filters)) {
|
||
filtered.push({ file: file, index: originalIndex });
|
||
}
|
||
});
|
||
|
||
// Row 0: transmittal self-entry (always pinned first)
|
||
renderSelfRow(tbody, 0);
|
||
|
||
var rowNum = 0;
|
||
filtered.forEach(function (entry) {
|
||
var file = entry.file;
|
||
var index = entry.index;
|
||
var row = document.createElement('tr');
|
||
rowNum++;
|
||
|
||
var numCell = document.createElement('td');
|
||
numCell.className = 'px-2 py-1 align-top text-center text-gray-400 whitespace-nowrap';
|
||
if (app.state.mode === 'edit') {
|
||
var delBtn = document.createElement('button');
|
||
delBtn.type = 'button';
|
||
delBtn.className = 'row-delete-btn';
|
||
delBtn.textContent = '\u00d7';
|
||
delBtn.title = 'Remove this file';
|
||
delBtn.dataset.fileIndex = String(index);
|
||
numCell.appendChild(delBtn);
|
||
var numSpan = document.createElement('span');
|
||
numSpan.textContent = String(rowNum);
|
||
numCell.appendChild(numSpan);
|
||
} else {
|
||
numCell.textContent = String(rowNum);
|
||
}
|
||
row.appendChild(numCell);
|
||
|
||
var trackingCell = document.createElement('td');
|
||
trackingCell.className = 'px-2 py-1 align-top whitespace-nowrap font-mono';
|
||
var link = document.createElement('a');
|
||
link.textContent = file.trackingNumber || '';
|
||
var relativePath = file.path || file.name || '';
|
||
if (relativePath) {
|
||
link.href = encodeURI(relativePath);
|
||
link.dataset.relativePath = relativePath;
|
||
} else {
|
||
link.href = '#';
|
||
}
|
||
link.title = relativePath || (file.name || '');
|
||
link.className = 'text-blue-600 hover:underline';
|
||
var ext = (file.extension || zddc.splitExtension(relativePath).extension);
|
||
if (app.constants.viewableExts.indexOf(ext) !== -1) {
|
||
link.target = '_blank';
|
||
link.rel = 'noopener';
|
||
} else {
|
||
var filename = (relativePath.split('/').pop() || file.name || 'download');
|
||
link.setAttribute('download', filename);
|
||
}
|
||
trackingCell.appendChild(link);
|
||
trackingCell.contentEditable = 'false';
|
||
trackingCell.dataset.index = String(index);
|
||
trackingCell.dataset.field = 'trackingNumber';
|
||
row.appendChild(trackingCell);
|
||
|
||
var titleCell = document.createElement('td');
|
||
titleCell.className = 'px-2 py-1 align-top whitespace-normal break-words w-full';
|
||
titleCell.textContent = file.title || '';
|
||
titleCell.contentEditable = (app.state.mode === 'edit').toString();
|
||
titleCell.setAttribute('role', 'textbox');
|
||
titleCell.setAttribute('aria-multiline', 'false');
|
||
titleCell.setAttribute('tabindex', '0');
|
||
titleCell.dataset.index = String(index);
|
||
titleCell.dataset.field = 'title';
|
||
row.appendChild(titleCell);
|
||
|
||
var revisionCell = document.createElement('td');
|
||
revisionCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
|
||
revisionCell.textContent = file.revision || '';
|
||
revisionCell.contentEditable = (app.state.mode === 'edit').toString();
|
||
revisionCell.setAttribute('role', 'textbox');
|
||
revisionCell.setAttribute('aria-multiline', 'false');
|
||
revisionCell.setAttribute('tabindex', '0');
|
||
revisionCell.dataset.index = String(index);
|
||
revisionCell.dataset.field = 'revision';
|
||
row.appendChild(revisionCell);
|
||
|
||
var statusCell = document.createElement('td');
|
||
statusCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
|
||
statusCell.textContent = file.status || '';
|
||
statusCell.contentEditable = (app.state.mode === 'edit').toString();
|
||
statusCell.setAttribute('role', 'textbox');
|
||
statusCell.setAttribute('aria-multiline', 'false');
|
||
statusCell.setAttribute('tabindex', '0');
|
||
statusCell.dataset.index = String(index);
|
||
statusCell.dataset.field = 'status';
|
||
row.appendChild(statusCell);
|
||
|
||
var extCell = document.createElement('td');
|
||
extCell.className = 'px-2 py-1 align-top whitespace-nowrap text-center font-mono';
|
||
extCell.textContent = (file.extension || '').toLowerCase();
|
||
extCell.contentEditable = 'false';
|
||
row.appendChild(extCell);
|
||
|
||
var sizeCell = document.createElement('td');
|
||
sizeCell.className = 'px-2 py-1 align-top whitespace-nowrap text-right font-mono';
|
||
var isUnmatched = !file.fileHandle && !file.sha256;
|
||
if (isUnmatched) {
|
||
sizeCell.textContent = '\u2014';
|
||
sizeCell.classList.add('text-gray-400');
|
||
} else {
|
||
var fileSizeValue = (file.fileSize != null ? file.fileSize : file.size);
|
||
sizeCell.textContent = util.formatFileSize(fileSizeValue);
|
||
}
|
||
sizeCell.contentEditable = 'false';
|
||
row.appendChild(sizeCell);
|
||
|
||
var hashCell = document.createElement('td');
|
||
hashCell.className = 'px-2 py-1 align-top font-mono text-[9px] whitespace-normal break-all leading-snug';
|
||
if (isUnmatched) {
|
||
hashCell.textContent = 'pending';
|
||
hashCell.classList.add('italic', 'text-gray-400');
|
||
} else {
|
||
hashCell.textContent = util.formatShortFileHash(file.sha256 || '');
|
||
}
|
||
row.appendChild(hashCell);
|
||
|
||
if (file._verifyResult) {
|
||
row.classList.add('verify-' + file._verifyResult);
|
||
}
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
if (!filtered.length) {
|
||
var placeholderRow = document.createElement('tr');
|
||
for (var i = 0; i < 8; i += 1) {
|
||
var cell = document.createElement('td');
|
||
cell.className = 'px-2 py-1 align-top';
|
||
placeholderRow.appendChild(cell);
|
||
}
|
||
tbody.appendChild(placeholderRow);
|
||
}
|
||
|
||
if (app.modules.filters && typeof app.modules.filters.refreshPlaceholders === 'function') {
|
||
app.modules.filters.refreshPlaceholders();
|
||
}
|
||
setupRowDropTargets();
|
||
};
|
||
|
||
// Undo state for row deletion
|
||
var _lastDeleted = null;
|
||
var _undoTimer = null;
|
||
|
||
function deleteFileRow(fileIndex) {
|
||
var file = app.data.files[fileIndex];
|
||
if (!file) { return; }
|
||
var setStatus = app.modules.data && app.modules.data.setStatus;
|
||
var label = (file.trackingNumber || '') + (file.title ? ' - ' + file.title : '');
|
||
|
||
// Store for undo
|
||
_lastDeleted = { file: file, index: fileIndex };
|
||
if (_undoTimer) { clearTimeout(_undoTimer); }
|
||
_undoTimer = setTimeout(function () { _lastDeleted = null; }, 10000);
|
||
|
||
app.data.files.splice(fileIndex, 1);
|
||
filesModule.updateFilesInJson(app.data.files);
|
||
filesModule.render();
|
||
app.state.apply();
|
||
app.markDirty();
|
||
|
||
if (setStatus) {
|
||
setStatus('Removed ' + (label || 'row') + ' — click here to undo', 'success');
|
||
// Attach one-time undo click handler to status bar
|
||
var statusEl = document.querySelector('#data-status');
|
||
if (statusEl) {
|
||
statusEl.style.cursor = 'pointer';
|
||
var cleanup = function () {
|
||
statusEl.removeEventListener('click', handler);
|
||
statusEl.style.cursor = '';
|
||
};
|
||
var handler = function () {
|
||
cleanup();
|
||
if (!_lastDeleted) { return; }
|
||
var d = _lastDeleted;
|
||
_lastDeleted = null;
|
||
if (_undoTimer) { clearTimeout(_undoTimer); _undoTimer = null; }
|
||
var idx = Math.min(d.index, app.data.files.length);
|
||
app.data.files.splice(idx, 0, d.file);
|
||
filesModule.updateFilesInJson(app.data.files);
|
||
filesModule.render();
|
||
app.state.apply();
|
||
app.markDirty();
|
||
if (setStatus) { setStatus('Restored ' + (label || 'row'), 'success'); }
|
||
};
|
||
// Clear cursor when undo expires
|
||
if (_undoTimer) { clearTimeout(_undoTimer); }
|
||
_undoTimer = setTimeout(function () { _lastDeleted = null; cleanup(); }, 10000);
|
||
statusEl.addEventListener('click', handler);
|
||
}
|
||
}
|
||
}
|
||
|
||
filesModule.setupTableEditing = function setupTableEditing() {
|
||
var tbody = document.querySelector('table tbody');
|
||
if (!tbody) {
|
||
return;
|
||
}
|
||
// Delegated handler for row delete buttons
|
||
tbody.addEventListener('click', function (event) {
|
||
var delBtn = event.target.closest('.row-delete-btn');
|
||
if (delBtn && delBtn.dataset.fileIndex !== undefined) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
deleteFileRow(Number(delBtn.dataset.fileIndex));
|
||
return;
|
||
}
|
||
});
|
||
// Click delegation for file preview
|
||
// Edit mode: always preview (relative paths don't work until HTML is saved)
|
||
// View mode: preview only when checkbox is checked and file source is loaded
|
||
tbody.addEventListener('click', function (event) {
|
||
var link = event.target.closest('a');
|
||
if (!link) {
|
||
return;
|
||
}
|
||
var cell = link.closest('td');
|
||
if (!cell || !cell.dataset.index) {
|
||
return;
|
||
}
|
||
var file = app.data.files[Number(cell.dataset.index)];
|
||
if (!file || !filesModule.hasFileSource || !filesModule.hasFileSource(file)) {
|
||
return;
|
||
}
|
||
var usePreview = (app.state.mode === 'edit') ||
|
||
(filesModule.isPreviewEnabled && filesModule.isPreviewEnabled()) ||
|
||
!!(file.fileHandle);
|
||
if (usePreview) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
filesModule.showFilePreview(file);
|
||
}
|
||
});
|
||
tbody.addEventListener('input', function (event) {
|
||
var target = event.target;
|
||
if (!(target instanceof HTMLElement)) {
|
||
return;
|
||
}
|
||
var index = target.dataset.index;
|
||
var field = target.dataset.field;
|
||
if (index === undefined || !field) {
|
||
return;
|
||
}
|
||
var entry = app.data.files[Number(index)];
|
||
if (!entry) {
|
||
return;
|
||
}
|
||
entry[field] = (target.textContent || '').replace(/\r?\n/g, ' ').trim();
|
||
filesModule.sortFilesInPlace(app.data.files);
|
||
filesModule.updateFilesInJson(app.data.files);
|
||
filesModule.render();
|
||
app.markDirty();
|
||
if (app.modules.liveDigest && app.modules.liveDigest.schedule) {
|
||
app.modules.liveDigest.schedule();
|
||
}
|
||
});
|
||
tbody.addEventListener('keydown', function (event) {
|
||
var target = event.target;
|
||
if (!(target instanceof HTMLElement)) {
|
||
return;
|
||
}
|
||
if (target.isContentEditable && event.key === 'Enter') {
|
||
event.preventDefault();
|
||
}
|
||
});
|
||
tbody.addEventListener('paste', function (event) {
|
||
var target = event.target;
|
||
if (!(target instanceof HTMLElement)) {
|
||
return;
|
||
}
|
||
if (!target.isContentEditable) {
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
var text = (event.clipboardData || window.clipboardData).getData('text');
|
||
var sanitized = (text || '').replace(/\r?\n/g, ' ');
|
||
try {
|
||
document.execCommand('insertText', false, sanitized);
|
||
} catch (err) {
|
||
target.textContent = (target.textContent || '') + sanitized;
|
||
}
|
||
});
|
||
};
|
||
|
||
filesModule.handleClear = function handleClear() {
|
||
if (app.state.mode !== 'edit') {
|
||
return;
|
||
}
|
||
if (filesModule.cleanupBlobUrls) {
|
||
filesModule.cleanupBlobUrls();
|
||
}
|
||
app.data.files = [];
|
||
app.data.selectedDirHandle = null;
|
||
filesModule.updateDirectoryIndicator('');
|
||
filesModule.updateFilesInJson([]);
|
||
filesModule.render();
|
||
app.state.apply();
|
||
app.markDirty();
|
||
};
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
var dom = app.dom;
|
||
var util = app.util;
|
||
var filesModule = app.modules.files;
|
||
|
||
// Blob URL cache keyed by file path
|
||
var blobCache = new Map();
|
||
|
||
// Current preview popup window reference
|
||
var previewWindow = null;
|
||
|
||
// Extensions that support rich in-browser preview (in addition to images,
|
||
// tiff, zip, and text — wired up in isPreviewable below).
|
||
var PREVIEW_EXTENSIONS = ['pdf', 'docx', 'xlsx', 'xls'];
|
||
|
||
// Use shared image / tiff / text lists from zddc.preview so the four tools
|
||
// stay in sync on what is previewable.
|
||
var IMAGE_EXTENSIONS = window.zddc.preview.IMAGE_EXTENSIONS;
|
||
var TIFF_EXTENSIONS = window.zddc.preview.TIFF_EXTENSIONS;
|
||
var TEXT_EXTENSIONS = window.zddc.preview.TEXT_EXTENSIONS;
|
||
|
||
// Cache for lazily loaded CDN libraries
|
||
var loadedLibraries = new Map();
|
||
|
||
var MIME_TYPES = {
|
||
'pdf': 'application/pdf',
|
||
'doc': 'application/msword',
|
||
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
'xls': 'application/vnd.ms-excel',
|
||
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
'ppt': 'application/vnd.ms-powerpoint',
|
||
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||
'txt': 'text/plain',
|
||
'csv': 'text/csv',
|
||
'html': 'text/html',
|
||
'htm': 'text/html',
|
||
'xml': 'text/xml',
|
||
'json': 'application/json',
|
||
'jpg': 'image/jpeg',
|
||
'jpeg': 'image/jpeg',
|
||
'png': 'image/png',
|
||
'gif': 'image/gif',
|
||
'bmp': 'image/bmp',
|
||
'svg': 'image/svg+xml',
|
||
'webp': 'image/webp',
|
||
'ico': 'image/x-icon',
|
||
'zip': 'application/zip',
|
||
'mp4': 'video/mp4',
|
||
'mp3': 'audio/mpeg',
|
||
'wav': 'audio/wav',
|
||
'md': 'text/markdown'
|
||
};
|
||
|
||
function getMimeType(ext) {
|
||
return MIME_TYPES[(ext || '').toLowerCase()] || 'application/octet-stream';
|
||
}
|
||
|
||
function loadLibrary(url) {
|
||
if (loadedLibraries.has(url)) {
|
||
return loadedLibraries.get(url);
|
||
}
|
||
var promise = new Promise(function (resolve, reject) {
|
||
var script = document.createElement('script');
|
||
script.src = url;
|
||
script.onload = resolve;
|
||
script.onerror = function () {
|
||
reject(new Error('Failed to load library: ' + url));
|
||
};
|
||
document.head.appendChild(script);
|
||
});
|
||
loadedLibraries.set(url, promise);
|
||
return promise;
|
||
}
|
||
|
||
function isPreviewable(ext) {
|
||
var lower = (ext || '').toLowerCase();
|
||
return PREVIEW_EXTENSIONS.indexOf(lower) !== -1
|
||
|| IMAGE_EXTENSIONS.indexOf(lower) !== -1
|
||
|| TIFF_EXTENSIONS.indexOf(lower) !== -1
|
||
|| TEXT_EXTENSIONS.indexOf(lower) !== -1
|
||
|| lower === 'zip';
|
||
}
|
||
|
||
function hasFileSource(file) {
|
||
return !!(file.fileHandle || file.zipEntry);
|
||
}
|
||
|
||
async function getFileArrayBuffer(file) {
|
||
if (file.fileHandle) {
|
||
var fileData = await file.fileHandle.getFile();
|
||
return fileData.arrayBuffer();
|
||
}
|
||
if (file.zipEntry) {
|
||
return file.zipEntry.async('arraybuffer');
|
||
}
|
||
throw new Error('No file source available');
|
||
}
|
||
|
||
async function getFileBlobUrl(file) {
|
||
var cacheKey = file.path || file.name || '';
|
||
if (blobCache.has(cacheKey)) {
|
||
return blobCache.get(cacheKey);
|
||
}
|
||
var arrayBuffer = await getFileArrayBuffer(file);
|
||
var mimeType = getMimeType(file.extension);
|
||
var blob = new Blob([arrayBuffer], { type: mimeType });
|
||
var url = URL.createObjectURL(blob);
|
||
blobCache.set(cacheKey, url);
|
||
return url;
|
||
}
|
||
|
||
function cleanupBlobUrls() {
|
||
blobCache.forEach(function (url) {
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
blobCache.clear();
|
||
}
|
||
|
||
function buildPreviewHtml(file, url) {
|
||
var ext = (file.extension || '').toLowerCase();
|
||
var safeName = util.escapeHtml(file.name || file.path || 'file');
|
||
var safeHref = util.escapeHtmlAttribute(url);
|
||
|
||
// PDF and HTML preview natively in an iframe — for HTML this
|
||
// means the page is RENDERED (not shown as literal source text);
|
||
// the blob's MIME type ('text/html', see getMimeType) tells the
|
||
// browser to render. The HTML iframe is sandboxed:
|
||
// - allow-same-origin: needed so the iframe's resource loads
|
||
// (img / link / etc.) work normally for same-origin paths.
|
||
// - allow-popups + allow-popups-to-escape-sandbox: clicking
|
||
// <a target="_blank"> (or middle-click) opens a real new tab
|
||
// with full browser features. Without these, link clicks
|
||
// intended for new tabs silently no-op.
|
||
// - NO allow-scripts: archived HTML cannot run JS in this
|
||
// popup's origin.
|
||
var contentHtml;
|
||
if (ext === 'pdf') {
|
||
contentHtml = '<iframe src="' + safeHref + '"></iframe>';
|
||
} else if (ext === 'html' || ext === 'htm') {
|
||
contentHtml = '<iframe src="' + safeHref + '" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>';
|
||
} else {
|
||
contentHtml = '<div id="previewContent"><div class="loading">Loading preview...</div></div>';
|
||
}
|
||
|
||
return '<!DOCTYPE html>\n' +
|
||
'<html>\n' +
|
||
'<head>\n' +
|
||
'<title>' + safeName + ' - Preview</title>\n' +
|
||
'<style>\n' +
|
||
'* { margin: 0; padding: 0; box-sizing: border-box; }\n' +
|
||
'body { display: flex; flex-direction: column; height: 100vh; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }\n' +
|
||
'.toolbar { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; background: #f5f5f5; border-bottom: 1px solid #ddd; }\n' +
|
||
'.toolbar h1 { flex: 1; font-size: 0.95rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
|
||
'.btn { padding: 0.4rem 0.8rem; font-size: 0.85rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer; }\n' +
|
||
'.btn:hover { background: #e8e8e8; }\n' +
|
||
'iframe { flex: 1; width: 100%; border: none; }\n' +
|
||
'#previewContent { flex: 1; overflow: auto; }\n' +
|
||
'.loading { display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 1.1rem; }\n' +
|
||
'.docx-wrapper { padding: 1rem; }\n' +
|
||
'.xlsx-table { border-collapse: collapse; width: 100%; font-size: 0.85rem; }\n' +
|
||
'.xlsx-table th, .xlsx-table td { border: 1px solid #ddd; padding: 0.35rem 0.5rem; text-align: left; white-space: nowrap; }\n' +
|
||
'.xlsx-table th { background: #f0f0f0; font-weight: 600; position: sticky; top: 0; }\n' +
|
||
'.xlsx-table tr:nth-child(even) { background: #fafafa; }\n' +
|
||
'.xlsx-table tr:hover { background: #f0f7ff; }\n' +
|
||
'.sheet-tabs { display: flex; gap: 0; border-bottom: 1px solid #ddd; background: #f5f5f5; }\n' +
|
||
'.sheet-tab { padding: 0.4rem 1rem; cursor: pointer; border: 1px solid transparent; border-bottom: none; font-size: 0.85rem; background: transparent; }\n' +
|
||
'.sheet-tab:hover { background: #e8e8e8; }\n' +
|
||
'.sheet-tab.active { background: white; border-color: #ddd; border-bottom-color: white; margin-bottom: -1px; font-weight: 500; }\n' +
|
||
'img.preview-image { max-width: 100%; max-height: 100%; object-fit: contain; margin: auto; display: block; }\n' +
|
||
'pre.preview-text { padding: 1rem; font-family: Consolas, Monaco, monospace; font-size: .85rem; white-space: pre-wrap; word-wrap: break-word; }\n' +
|
||
'</style>\n' +
|
||
'</head>\n' +
|
||
'<body>\n' +
|
||
'<div class="toolbar">\n' +
|
||
'<h1>' + safeName + '</h1>\n' +
|
||
'<button class="btn" onclick="downloadFile()">Download</button>\n' +
|
||
'</div>\n' +
|
||
contentHtml + '\n' +
|
||
'<script>\n' +
|
||
'var blobUrl = "' + url.replace(/"/g, '\\"') + '";\n' +
|
||
'var fileName = "' + safeName.replace(/"/g, '\\"') + '";\n' +
|
||
'function downloadFile() {\n' +
|
||
' var a = document.createElement("a");\n' +
|
||
' a.href = blobUrl;\n' +
|
||
' a.download = fileName;\n' +
|
||
' document.body.appendChild(a);\n' +
|
||
' a.click();\n' +
|
||
' document.body.removeChild(a);\n' +
|
||
'}\n' +
|
||
'</' + 'script>\n' +
|
||
'</body>\n' +
|
||
'</html>';
|
||
}
|
||
|
||
async function renderDocxInWindow(file) {
|
||
var container = previewWindow.document.getElementById('previewContent');
|
||
if (!container) {
|
||
return;
|
||
}
|
||
try {
|
||
// jszip + docx-preview vendored by build.sh — already in scope.
|
||
var arrayBuffer = await getFileArrayBuffer(file);
|
||
container.innerHTML = '';
|
||
await window.docx.renderAsync(arrayBuffer, container);
|
||
} catch (err) {
|
||
console.error('[transmittal] Error rendering DOCX:', err);
|
||
container.innerHTML = '<div class="loading">Error rendering DOCX: ' + util.escapeHtml(err.message || '') + '<br>Click Download to view in Word.</div>';
|
||
}
|
||
}
|
||
|
||
async function renderXlsxInWindow(file) {
|
||
var container = previewWindow.document.getElementById('previewContent');
|
||
if (!container) {
|
||
return;
|
||
}
|
||
try {
|
||
await loadLibrary('https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js');
|
||
var arrayBuffer = await getFileArrayBuffer(file);
|
||
var workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||
container.innerHTML = '';
|
||
|
||
var tableContainer = previewWindow.document.createElement('div');
|
||
tableContainer.style.flex = '1';
|
||
tableContainer.style.overflow = 'auto';
|
||
|
||
if (workbook.SheetNames.length > 1) {
|
||
var tabs = previewWindow.document.createElement('div');
|
||
tabs.className = 'sheet-tabs';
|
||
workbook.SheetNames.forEach(function (name, i) {
|
||
var tab = previewWindow.document.createElement('button');
|
||
tab.className = 'sheet-tab' + (i === 0 ? ' active' : '');
|
||
tab.textContent = name;
|
||
tab.addEventListener('click', function () {
|
||
tabs.querySelectorAll('.sheet-tab').forEach(function (t) {
|
||
t.classList.remove('active');
|
||
});
|
||
tab.classList.add('active');
|
||
renderSheet(workbook, name, tableContainer);
|
||
});
|
||
tabs.appendChild(tab);
|
||
});
|
||
container.appendChild(tabs);
|
||
}
|
||
|
||
container.appendChild(tableContainer);
|
||
renderSheet(workbook, workbook.SheetNames[0], tableContainer);
|
||
} catch (err) {
|
||
console.error('[transmittal] Error rendering XLSX:', err);
|
||
container.innerHTML = '<div class="loading">Error rendering spreadsheet: ' + util.escapeHtml(err.message || '') + '<br>Click Download to view in Excel.</div>';
|
||
}
|
||
}
|
||
|
||
function renderSheet(workbook, sheetName, container) {
|
||
var sheet = workbook.Sheets[sheetName];
|
||
var html = XLSX.utils.sheet_to_html(sheet, { editable: false });
|
||
container.innerHTML = html;
|
||
var table = container.querySelector('table');
|
||
if (table) {
|
||
table.className = 'xlsx-table';
|
||
}
|
||
}
|
||
|
||
async function renderImageInWindow(file, url) {
|
||
var container = previewWindow.document.getElementById('previewContent');
|
||
if (!container) {
|
||
return;
|
||
}
|
||
container.innerHTML = '';
|
||
var img = previewWindow.document.createElement('img');
|
||
img.className = 'preview-image';
|
||
img.src = url;
|
||
img.alt = file.name || '';
|
||
container.appendChild(img);
|
||
}
|
||
|
||
async function renderTiffInWindow(file) {
|
||
var container = previewWindow.document.getElementById('previewContent');
|
||
if (!container) return;
|
||
try {
|
||
var arrayBuffer = await getFileArrayBuffer(file);
|
||
await window.zddc.preview.renderTiff(previewWindow.document, container, arrayBuffer, {
|
||
fileName: file.name
|
||
});
|
||
} catch (err) {
|
||
console.error('[transmittal] Error rendering TIFF:', err);
|
||
container.innerHTML = '<div class="loading">Error rendering TIFF: ' + util.escapeHtml(err.message || '') + '<br>Click Download to view in another application.</div>';
|
||
}
|
||
}
|
||
|
||
async function renderZipInWindow(file) {
|
||
var container = previewWindow.document.getElementById('previewContent');
|
||
if (!container) return;
|
||
try {
|
||
var arrayBuffer = await getFileArrayBuffer(file);
|
||
await window.zddc.preview.renderZipListing(previewWindow.document, container, arrayBuffer, {
|
||
fileName: file.name
|
||
});
|
||
} catch (err) {
|
||
console.error('[transmittal] Error rendering ZIP listing:', err);
|
||
container.innerHTML = '<div class="loading">Error reading ZIP: ' + util.escapeHtml(err.message || '') + '</div>';
|
||
}
|
||
}
|
||
|
||
async function renderTextInWindow(file) {
|
||
var container = previewWindow.document.getElementById('previewContent');
|
||
if (!container) return;
|
||
try {
|
||
var arrayBuffer = await getFileArrayBuffer(file);
|
||
var text = new TextDecoder('utf-8', { fatal: false }).decode(arrayBuffer);
|
||
var MAX = 200000;
|
||
if (text.length > MAX) {
|
||
text = text.substring(0, MAX) + '\n\n... (truncated, ' + (text.length - MAX) + ' more chars — Download for full file)';
|
||
}
|
||
container.innerHTML = '';
|
||
var pre = previewWindow.document.createElement('pre');
|
||
pre.className = 'preview-text';
|
||
pre.textContent = text;
|
||
container.appendChild(pre);
|
||
} catch (err) {
|
||
console.error('[transmittal] Error reading text file:', err);
|
||
container.innerHTML = '<div class="loading">Error reading file: ' + util.escapeHtml(err.message || '') + '</div>';
|
||
}
|
||
}
|
||
|
||
async function showFilePreview(file) {
|
||
var ext = (file.extension || '').toLowerCase();
|
||
try {
|
||
var url = await getFileBlobUrl(file);
|
||
var html = buildPreviewHtml(file, url);
|
||
|
||
if (previewWindow && !previewWindow.closed) {
|
||
previewWindow.document.open();
|
||
previewWindow.document.write(html);
|
||
previewWindow.document.close();
|
||
previewWindow.focus();
|
||
} else {
|
||
var width = Math.round(screen.width * 0.6);
|
||
var height = Math.round(screen.height * 0.8);
|
||
var left = Math.round((screen.width - width) / 2);
|
||
var top = Math.round((screen.height - height) / 2);
|
||
previewWindow = window.open('', 'filePreview',
|
||
'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',resizable=yes,scrollbars=yes');
|
||
if (!previewWindow) {
|
||
window.open(url, '_blank');
|
||
return;
|
||
}
|
||
previewWindow.document.write(html);
|
||
previewWindow.document.close();
|
||
previewWindow.focus();
|
||
}
|
||
|
||
if (ext === 'pdf' || ext === 'html' || ext === 'htm') {
|
||
// iframe already wired in popup HTML; nothing more to do
|
||
} else if (ext === 'docx') {
|
||
await renderDocxInWindow(file);
|
||
} else if (ext === 'xlsx' || ext === 'xls') {
|
||
await renderXlsxInWindow(file);
|
||
} else if (TIFF_EXTENSIONS.indexOf(ext) !== -1) {
|
||
await renderTiffInWindow(file);
|
||
} else if (ext === 'zip') {
|
||
await renderZipInWindow(file);
|
||
} else if (IMAGE_EXTENSIONS.indexOf(ext) !== -1) {
|
||
await renderImageInWindow(file, url);
|
||
} else if (TEXT_EXTENSIONS.indexOf(ext) !== -1) {
|
||
await renderTextInWindow(file);
|
||
}
|
||
} catch (err) {
|
||
console.error('[transmittal] Error loading file preview:', err);
|
||
alert('Error loading preview: ' + (err && err.message ? err.message : err));
|
||
}
|
||
}
|
||
|
||
// --- Load files for preview (directory or ZIP) ---
|
||
|
||
function updatePreviewStatus(text) {
|
||
var el = dom.qs('#preview-status');
|
||
if (el) {
|
||
el.textContent = text;
|
||
}
|
||
}
|
||
|
||
function matchFilesToSources(sourceEntries) {
|
||
var files = app.data.files || [];
|
||
if (!files.length || !sourceEntries.length) {
|
||
return 0;
|
||
}
|
||
|
||
// Build lookup by relative path and by filename
|
||
var byPath = new Map();
|
||
var byName = new Map();
|
||
sourceEntries.forEach(function (entry) {
|
||
if (entry.path) {
|
||
byPath.set(entry.path, entry);
|
||
}
|
||
if (entry.name) {
|
||
// Only use filename match if unique
|
||
if (byName.has(entry.name)) {
|
||
byName.set(entry.name, null); // Mark as ambiguous
|
||
} else {
|
||
byName.set(entry.name, entry);
|
||
}
|
||
}
|
||
});
|
||
|
||
var matched = 0;
|
||
files.forEach(function (file) {
|
||
var source = null;
|
||
// Try path match first
|
||
if (file.path && byPath.has(file.path)) {
|
||
source = byPath.get(file.path);
|
||
}
|
||
// Fall back to name match
|
||
if (!source && file.name) {
|
||
var nameMatch = byName.get(file.name);
|
||
if (nameMatch) {
|
||
source = nameMatch;
|
||
}
|
||
}
|
||
if (source) {
|
||
if (source.fileHandle) {
|
||
file.fileHandle = source.fileHandle;
|
||
}
|
||
if (source.zipEntry) {
|
||
file.zipEntry = source.zipEntry;
|
||
}
|
||
matched += 1;
|
||
}
|
||
});
|
||
return matched;
|
||
}
|
||
|
||
async function loadFromDirectory() {
|
||
if (typeof window.showDirectoryPicker !== 'function') {
|
||
alert('Your browser does not support directory selection. Try Chrome or Edge.');
|
||
return;
|
||
}
|
||
var dirHandle;
|
||
try {
|
||
dirHandle = await window.showDirectoryPicker();
|
||
} catch (err) {
|
||
if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) {
|
||
return;
|
||
}
|
||
throw err;
|
||
}
|
||
updatePreviewStatus('Scanning directory...');
|
||
var entries = await filesModule.collectDirectoryEntries(dirHandle);
|
||
var sourceEntries = entries.map(function (entry) {
|
||
return { path: entry.path, name: entry.name, fileHandle: entry.handle };
|
||
});
|
||
var matched = matchFilesToSources(sourceEntries);
|
||
if (matched > 0) {
|
||
filesLoadedForPreview = true;
|
||
updatePreviewStatus('from "' + dirHandle.name + '" — ' + matched + ' of ' + (app.data.files || []).length + ' files matched');
|
||
filesModule.render();
|
||
} else {
|
||
updatePreviewStatus('No matching files found in "' + dirHandle.name + '"');
|
||
}
|
||
}
|
||
|
||
async function loadFromZip() {
|
||
return new Promise(function (resolve) {
|
||
var input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.zip';
|
||
input.addEventListener('change', async function () {
|
||
var zipFile = input.files && input.files[0];
|
||
if (!zipFile) {
|
||
resolve();
|
||
return;
|
||
}
|
||
try {
|
||
updatePreviewStatus('Loading ZIP...');
|
||
// JSZip vendored by build.sh — already in scope.
|
||
var arrayBuffer = await zipFile.arrayBuffer();
|
||
var zip = await JSZip.loadAsync(arrayBuffer);
|
||
var sourceEntries = [];
|
||
zip.forEach(function (relativePath, zipEntry) {
|
||
if (zipEntry.dir) {
|
||
return;
|
||
}
|
||
// Strip the outermost folder if all entries share one
|
||
var name = relativePath.split('/').pop();
|
||
sourceEntries.push({
|
||
path: relativePath,
|
||
name: name,
|
||
zipEntry: zipEntry
|
||
});
|
||
});
|
||
|
||
// Try stripping common prefix for better path matching
|
||
if (sourceEntries.length > 0) {
|
||
var firstPath = sourceEntries[0].path;
|
||
var prefix = firstPath.indexOf('/') !== -1 ? firstPath.substring(0, firstPath.indexOf('/') + 1) : '';
|
||
if (prefix) {
|
||
var allSharePrefix = sourceEntries.every(function (e) {
|
||
return e.path.indexOf(prefix) === 0;
|
||
});
|
||
if (allSharePrefix) {
|
||
sourceEntries.forEach(function (e) {
|
||
e.path = e.path.substring(prefix.length);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
var matched = matchFilesToSources(sourceEntries);
|
||
if (matched > 0) {
|
||
filesLoadedForPreview = true;
|
||
updatePreviewStatus('from "' + zipFile.name + '" — ' + matched + ' of ' + (app.data.files || []).length + ' files matched');
|
||
filesModule.render();
|
||
} else {
|
||
updatePreviewStatus('No matching files found in "' + zipFile.name + '"');
|
||
}
|
||
} catch (err) {
|
||
console.error('[transmittal] Error loading ZIP:', err);
|
||
updatePreviewStatus('Failed to load ZIP: ' + (err && err.message ? err.message : err));
|
||
}
|
||
resolve();
|
||
});
|
||
input.click();
|
||
});
|
||
}
|
||
|
||
// Track whether files have been loaded for preview in view mode
|
||
var filesLoadedForPreview = false;
|
||
|
||
function isPreviewEnabled() {
|
||
var checkbox = dom.qs('#preview-toggle');
|
||
return checkbox && checkbox.checked;
|
||
}
|
||
|
||
function syncCheckboxToMode() {
|
||
var checkbox = dom.qs('#preview-toggle');
|
||
if (!checkbox) {
|
||
return;
|
||
}
|
||
if (app.state.mode === 'edit') {
|
||
checkbox.checked = true;
|
||
checkbox.disabled = true;
|
||
updatePreviewStatus('');
|
||
} else {
|
||
checkbox.disabled = false;
|
||
if (!filesLoadedForPreview) {
|
||
checkbox.checked = false;
|
||
updatePreviewStatus('');
|
||
}
|
||
}
|
||
}
|
||
|
||
function showSourcePicker() {
|
||
var bar = dom.qs('#preview-bar');
|
||
var existing = dom.qs('#preview-load-menu');
|
||
if (existing) {
|
||
existing.remove();
|
||
return;
|
||
}
|
||
var menu = document.createElement('div');
|
||
menu.id = 'preview-load-menu';
|
||
menu.style.cssText = 'position:absolute;z-index:50;background:white;border:1px solid #ccc;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.15);padding:0.25rem 0;min-width:160px;';
|
||
|
||
function addOption(label, handler) {
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.textContent = label;
|
||
btn.style.cssText = 'display:block;width:100%;text-align:left;padding:0.4rem 0.75rem;border:none;background:none;cursor:pointer;font-size:0.8rem;';
|
||
btn.addEventListener('mouseenter', function () { btn.style.background = '#f0f0f0'; });
|
||
btn.addEventListener('mouseleave', function () { btn.style.background = 'none'; });
|
||
btn.addEventListener('click', function () {
|
||
menu.remove();
|
||
handler();
|
||
});
|
||
menu.appendChild(btn);
|
||
}
|
||
|
||
addOption('Select Directory', function () {
|
||
loadFromDirectory().then(function () {
|
||
if (!filesLoadedForPreview) {
|
||
uncheckPreview();
|
||
}
|
||
}).catch(function (err) {
|
||
console.error('[transmittal] loadFromDirectory failed', err);
|
||
updatePreviewStatus('Failed: ' + (err && err.message ? err.message : err));
|
||
uncheckPreview();
|
||
});
|
||
});
|
||
|
||
addOption('Select ZIP File', function () {
|
||
loadFromZip().then(function () {
|
||
if (!filesLoadedForPreview) {
|
||
uncheckPreview();
|
||
}
|
||
}).catch(function (err) {
|
||
console.error('[transmittal] loadFromZip failed', err);
|
||
updatePreviewStatus('Failed: ' + (err && err.message ? err.message : err));
|
||
uncheckPreview();
|
||
});
|
||
});
|
||
|
||
if (bar) {
|
||
bar.style.position = 'relative';
|
||
menu.style.top = '1.5rem';
|
||
menu.style.left = '0';
|
||
bar.appendChild(menu);
|
||
}
|
||
|
||
function closeMenu(e) {
|
||
if (!menu.contains(e.target)) {
|
||
menu.remove();
|
||
document.removeEventListener('click', closeMenu, true);
|
||
}
|
||
}
|
||
setTimeout(function () {
|
||
document.addEventListener('click', closeMenu, true);
|
||
}, 0);
|
||
}
|
||
|
||
function uncheckPreview() {
|
||
var checkbox = dom.qs('#preview-toggle');
|
||
if (checkbox) {
|
||
checkbox.checked = false;
|
||
}
|
||
}
|
||
|
||
function bindPreviewBar() {
|
||
var checkbox = dom.qs('#preview-toggle');
|
||
if (!checkbox) {
|
||
return;
|
||
}
|
||
checkbox.addEventListener('change', function () {
|
||
if (app.state.mode === 'edit') {
|
||
checkbox.checked = true;
|
||
return;
|
||
}
|
||
if (checkbox.checked) {
|
||
if (!filesLoadedForPreview) {
|
||
showSourcePicker();
|
||
}
|
||
} else {
|
||
updatePreviewStatus('');
|
||
}
|
||
});
|
||
syncCheckboxToMode();
|
||
}
|
||
|
||
// Expose on filesModule
|
||
filesModule.isPreviewable = isPreviewable;
|
||
filesModule.hasFileSource = hasFileSource;
|
||
filesModule.isPreviewEnabled = isPreviewEnabled;
|
||
filesModule.syncPreviewCheckbox = syncCheckboxToMode;
|
||
filesModule.showFilePreview = showFilePreview;
|
||
filesModule.cleanupBlobUrls = cleanupBlobUrls;
|
||
filesModule.getFileArrayBuffer = getFileArrayBuffer;
|
||
filesModule.getFileBlobUrl = getFileBlobUrl;
|
||
filesModule.loadLibrary = loadLibrary;
|
||
filesModule.getMimeType = getMimeType;
|
||
|
||
app.registerInit(function () {
|
||
bindPreviewBar();
|
||
window.addEventListener('beforeunload', cleanupBlobUrls);
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// Escape a string for use in a RegExp (literal match)
|
||
function escapeRegex(str) {
|
||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
}
|
||
|
||
// Build regex pattern at parse time based on anchors
|
||
function compilePattern(raw, anchorStart, anchorEnd) {
|
||
var src = (anchorStart ? '^' : '') + raw + (anchorEnd ? '$' : '');
|
||
try {
|
||
return new RegExp(src, 'i');
|
||
} catch (e) {
|
||
// Invalid regex — escape and retry (always succeeds)
|
||
var safe = (anchorStart ? '^' : '') + escapeRegex(raw) + (anchorEnd ? '$' : '');
|
||
return new RegExp(safe, 'i');
|
||
}
|
||
}
|
||
|
||
// Parse a single token string into a node
|
||
function parseToken(token) {
|
||
var s = token;
|
||
var negate = false;
|
||
var anchorStart = false;
|
||
var anchorEnd = false;
|
||
|
||
if (s.charAt(0) === '!') {
|
||
negate = true;
|
||
s = s.slice(1);
|
||
}
|
||
if (s.charAt(0) === '^') {
|
||
anchorStart = true;
|
||
s = s.slice(1);
|
||
}
|
||
if (s.length > 0 && s.charAt(s.length - 1) === '$') {
|
||
anchorEnd = true;
|
||
s = s.slice(0, -1);
|
||
}
|
||
|
||
if (s === '') return null;
|
||
|
||
// bare * (possibly after stripping !) → wildcard-all or wildcard-none
|
||
if (s === '*' && !anchorStart && !anchorEnd) {
|
||
return negate ? null : { type: 'wildcard-all' };
|
||
}
|
||
|
||
var re = compilePattern(s, anchorStart, anchorEnd);
|
||
return { type: negate ? 'no-match' : 'match', re: re };
|
||
}
|
||
|
||
// Parse expression string into AST array
|
||
function parse(expression) {
|
||
if (!expression || typeof expression !== 'string') return [];
|
||
var trimmed = expression.trim();
|
||
if (trimmed === '') return [];
|
||
if (trimmed === '*') return [{ type: 'wildcard-all' }];
|
||
|
||
var ast = [];
|
||
var i = 0;
|
||
var len = trimmed.length;
|
||
|
||
while (i < len) {
|
||
var ch = trimmed.charAt(i);
|
||
|
||
if (ch === '(') {
|
||
var depth = 1;
|
||
var j = i + 1;
|
||
while (j < len && depth > 0) {
|
||
if (trimmed.charAt(j) === '(') depth++;
|
||
else if (trimmed.charAt(j) === ')') depth--;
|
||
j++;
|
||
}
|
||
var innerAst = parse(trimmed.slice(i + 1, j - 1));
|
||
if (innerAst.length === 1) {
|
||
ast.push(innerAst[0]);
|
||
} else if (innerAst.length > 1) {
|
||
for (var k = 0; k < innerAst.length; k++) ast.push(innerAst[k]);
|
||
}
|
||
i = j;
|
||
} else if (ch === '|') {
|
||
ast.push({ type: 'pipe' });
|
||
i++;
|
||
} else if (ch === ' ') {
|
||
i++;
|
||
} else {
|
||
var j = i;
|
||
while (j < len) {
|
||
var c = trimmed.charAt(j);
|
||
if (c === ' ' || c === '(' || c === '|' || c === ')') break;
|
||
j++;
|
||
}
|
||
var token = trimmed.slice(i, j);
|
||
if (token.length > 0) {
|
||
var node = parseToken(token);
|
||
if (node !== null) ast.push(node);
|
||
}
|
||
i = j;
|
||
}
|
||
}
|
||
|
||
// Group pipes into OR nodes
|
||
var hasPipe = false;
|
||
var branches = [[]];
|
||
for (var l = 0; l < ast.length; l++) {
|
||
if (ast[l].type === 'pipe') {
|
||
hasPipe = true;
|
||
branches.push([]);
|
||
} else {
|
||
branches[branches.length - 1].push(ast[l]);
|
||
}
|
||
}
|
||
branches = branches.filter(function(b) { return b.length > 0; });
|
||
|
||
if (!hasPipe) {
|
||
return ast.filter(function(n) { return n.type !== 'pipe'; });
|
||
}
|
||
|
||
var orNodes = branches.map(function(branch) {
|
||
if (branch.length === 1) return branch[0];
|
||
return { type: 'and', nodes: branch };
|
||
});
|
||
return [{ type: 'or', nodes: orNodes }];
|
||
}
|
||
|
||
// Check if a single node matches the value
|
||
function nodeMatches(node, value) {
|
||
switch (node.type) {
|
||
case 'wildcard-all': return true;
|
||
case 'match': return node.re.test(value);
|
||
case 'no-match': return !node.re.test(value);
|
||
case 'or':
|
||
for (var i = 0; i < node.nodes.length; i++) {
|
||
if (nodeMatches(node.nodes[i], value)) return true;
|
||
}
|
||
return false;
|
||
case 'and':
|
||
for (var i = 0; i < node.nodes.length; i++) {
|
||
if (!nodeMatches(node.nodes[i], value)) return false;
|
||
}
|
||
return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
// Evaluate AST against value
|
||
function matches(value, ast) {
|
||
if (!ast || ast.length === 0) return true;
|
||
var v = String(value); // no forced lowercase — regex has 'i' flag
|
||
for (var i = 0; i < ast.length; i++) {
|
||
if (!nodeMatches(ast[i], v)) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
if (!window.zddc) {
|
||
throw new Error('shared/zddc-filter.js: window.zddc must be loaded first');
|
||
}
|
||
window.zddc.filter = { parse: parse, matches: matches };
|
||
})();
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const filters = app.modules.filters = {};
|
||
const dom = app.dom;
|
||
|
||
filters.fieldString = function fieldString(file, field) {
|
||
let value = '';
|
||
switch (field) {
|
||
case 'trackingNumber': value = file.trackingNumber || ''; break;
|
||
case 'title': value = file.title || ''; break;
|
||
case 'revision': value = file.revision || ''; break;
|
||
case 'status': value = file.status || ''; break;
|
||
case 'extension': value = file.extension || ''; break;
|
||
case 'sha256': value = file.sha256 || ''; break;
|
||
default: value = '';
|
||
}
|
||
return String(value).toLowerCase();
|
||
};
|
||
|
||
filters.getActiveFilters = function getActiveFilters() {
|
||
const active = {};
|
||
const inputs = document.querySelectorAll('thead input[data-filter-field]');
|
||
inputs.forEach(function (input) {
|
||
const field = input.getAttribute('data-filter-field');
|
||
const query = (input.value || '').trim();
|
||
if (!query) return;
|
||
active[field] = window.zddc.filter.parse(query);
|
||
});
|
||
return active;
|
||
};
|
||
|
||
filters.fileMatchesFilters = function fileMatchesFilters(file, activeFilters) {
|
||
const fields = Object.keys(activeFilters);
|
||
for (let i = 0; i < fields.length; i++) {
|
||
const field = fields[i];
|
||
const ast = activeFilters[field];
|
||
const value = filters.fieldString(file, field);
|
||
if (!window.zddc.filter.matches(value, ast)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
};
|
||
|
||
filters.refreshPlaceholders = function refreshPlaceholders() {
|
||
const inputs = document.querySelectorAll('thead input[data-filter-field]');
|
||
inputs.forEach(function (input) {
|
||
const empty = !(input.value || '').trim().length;
|
||
input.toggleAttribute('data-empty', empty);
|
||
});
|
||
};
|
||
|
||
filters.bindFilters = function bindFilters() {
|
||
const head = document.querySelector('thead');
|
||
if (!head) return;
|
||
|
||
head.addEventListener('input', function (event) {
|
||
const target = event.target;
|
||
if (target && target.getAttribute && target.getAttribute('data-filter-field')) {
|
||
filters.refreshPlaceholders();
|
||
if (app.modules.files && typeof app.modules.files.render === 'function') {
|
||
app.modules.files.render();
|
||
}
|
||
}
|
||
});
|
||
|
||
head.addEventListener('keydown', function (event) {
|
||
const target = event.target;
|
||
if (!(target instanceof HTMLElement)) return;
|
||
if (!target.getAttribute || !target.getAttribute('data-filter-field')) return;
|
||
if (event.key === 'Escape') {
|
||
target.value = '';
|
||
filters.refreshPlaceholders();
|
||
if (app.modules.files && typeof app.modules.files.render === 'function') {
|
||
app.modules.files.render();
|
||
}
|
||
event.preventDefault();
|
||
} else if (event.key === 'Enter') {
|
||
event.preventDefault();
|
||
const inputs = Array.from(head.querySelectorAll('input[data-filter-field]'));
|
||
const index = inputs.indexOf(target);
|
||
if (index !== -1) {
|
||
const next = inputs[(index + 1) % inputs.length];
|
||
if (next && typeof next.focus === 'function') next.focus();
|
||
}
|
||
}
|
||
});
|
||
|
||
filters.refreshPlaceholders();
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
filters.bindFilters();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const markdown = app.modules.markdown = {};
|
||
const dom = app.dom;
|
||
|
||
var escapeHtml = app.util.escapeHtml;
|
||
|
||
function renderInline(text) {
|
||
if (!text) {
|
||
return '';
|
||
}
|
||
let rendered = escapeHtml(text);
|
||
const codePlaceholders = [];
|
||
rendered = rendered.replace(/`([^`]+)`/g, function (_, code) {
|
||
codePlaceholders.push('<code>' + escapeHtml(code) + '</code>');
|
||
return '\u0000C' + (codePlaceholders.length - 1) + '\u0000';
|
||
});
|
||
rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_, label, url) {
|
||
const trimmedUrl = url.trim();
|
||
const allowed = /^(https?:\/\/|mailto:|tel:|\/|\.{1,2}\/)/i.test(trimmedUrl);
|
||
if (!allowed) {
|
||
return '[' + label + '](' + url + ')';
|
||
}
|
||
const safeUrl = trimmedUrl.replace(/\s+/g, '%20').replace(/"/g, '%22');
|
||
return '<a href="' + safeUrl + '" target="_blank" rel="noopener">' + escapeHtml(label) + '</a>';
|
||
});
|
||
rendered = rendered.replace(/(^|\s)((https?:\/\/)[^\s<]+)/gi, function (_, prefix, url) {
|
||
const safe = url.replace(/"/g, '%22');
|
||
return prefix + '<a href="' + safe + '" target="_blank" rel="noopener">' + url + '</a>';
|
||
});
|
||
rendered = rendered
|
||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/__([^_]+)__/g, '<strong>$1</strong>')
|
||
.replace(/(^|\W)\*([^*]+)\*(?=\W|$)/g, '$1<em>$2</em>')
|
||
.replace(/(^|\W)_([^_]+)_(?=\W|$)/g, '$1<em>$2</em>')
|
||
.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||
rendered = rendered.replace(/\u0000C(\d+)\u0000/g, function (_, index) {
|
||
return codePlaceholders[Number(index)] || '';
|
||
});
|
||
return rendered;
|
||
}
|
||
|
||
function leadingIndent(text) {
|
||
const expanded = text.replace(/\t/g, ' ');
|
||
const match = expanded.match(/^(\s*)/);
|
||
return match ? match[1].length : 0;
|
||
}
|
||
|
||
markdown.render = function renderMarkdownBasic(markdownText) {
|
||
const lines = (markdownText || '').replace(/\r\n?/g, '\n').split('\n');
|
||
const output = [];
|
||
let inCode = false;
|
||
let codeBuffer = [];
|
||
const listStack = [];
|
||
const tableSeparator = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/;
|
||
|
||
function flushCode() {
|
||
if (!inCode) {
|
||
return;
|
||
}
|
||
output.push('<pre><code>' + escapeHtml(codeBuffer.join('\n')) + '</code></pre>');
|
||
inCode = false;
|
||
codeBuffer = [];
|
||
}
|
||
|
||
function closeListsTo(indent) {
|
||
while (listStack.length && listStack[listStack.length - 1].indent > indent) {
|
||
output.push('</' + listStack.pop().type + '>');
|
||
}
|
||
}
|
||
|
||
function closeAllLists() {
|
||
closeListsTo(-1);
|
||
}
|
||
|
||
function openList(type, indent) {
|
||
listStack.push({ type, indent });
|
||
output.push('<' + type + '>');
|
||
}
|
||
|
||
function splitCells(row) {
|
||
const trimmed = row.trim().replace(/^\|/, '').replace(/\|$/, '');
|
||
return trimmed.split('|').map(function (cell) {
|
||
return cell.trim();
|
||
});
|
||
}
|
||
|
||
for (let index = 0; index < lines.length; index += 1) {
|
||
const raw = lines[index];
|
||
const line = raw;
|
||
if (/^```/.test(line)) {
|
||
if (inCode) {
|
||
flushCode();
|
||
} else {
|
||
inCode = true;
|
||
codeBuffer = [];
|
||
}
|
||
continue;
|
||
}
|
||
if (inCode) {
|
||
codeBuffer.push(raw);
|
||
continue;
|
||
}
|
||
if (!line.trim()) {
|
||
closeAllLists();
|
||
continue;
|
||
}
|
||
if (/\|/.test(line) && index + 1 < lines.length && tableSeparator.test(lines[index + 1])) {
|
||
closeAllLists();
|
||
const headerCells = splitCells(line);
|
||
index += 2;
|
||
const body = [];
|
||
while (index < lines.length && /\|/.test(lines[index]) && lines[index].trim()) {
|
||
body.push(splitCells(lines[index]));
|
||
index += 1;
|
||
}
|
||
index -= 1;
|
||
const thead = '<thead><tr>' + headerCells.map(function (cell) {
|
||
return '<th>' + renderInline(cell) + '</th>';
|
||
}).join('') + '</tr></thead>';
|
||
const tbody = '<tbody>' + body.map(function (rowCells) {
|
||
return '<tr>' + rowCells.map(function (cell) {
|
||
return '<td>' + renderInline(cell) + '</td>';
|
||
}).join('') + '</tr>';
|
||
}).join('') + '</tbody>';
|
||
output.push('<table>' + thead + tbody + '</table>');
|
||
continue;
|
||
}
|
||
if (/^\s*(?:---|\*\*\*|___)\s*$/.test(line)) {
|
||
closeAllLists();
|
||
output.push('<hr/>');
|
||
continue;
|
||
}
|
||
if (/^\s*>\s?/.test(line)) {
|
||
closeAllLists();
|
||
const quoteLines = [];
|
||
while (index < lines.length && /^\s*>\s?/.test(lines[index])) {
|
||
quoteLines.push(lines[index].replace(/^\s*>\s?/, ''));
|
||
index += 1;
|
||
}
|
||
index -= 1;
|
||
const html = quoteLines.map(function (text) {
|
||
return '<p>' + renderInline(text) + '</p>';
|
||
}).join('\n');
|
||
output.push('<blockquote>' + html + '</blockquote>');
|
||
continue;
|
||
}
|
||
let match = line.match(/^(#{1,6})\s+(.*)$/);
|
||
if (match) {
|
||
closeAllLists();
|
||
const level = match[1].length;
|
||
const text = renderInline(match[2].trim());
|
||
output.push('<h' + level + '>' + text + '</h' + level + '>');
|
||
continue;
|
||
}
|
||
match = line.match(/^(\s*)[-*+]\s+(.*)$/);
|
||
if (match) {
|
||
const indent = leadingIndent(match[1]);
|
||
const type = 'ul';
|
||
if (!listStack.length || listStack[listStack.length - 1].indent < indent) {
|
||
openList(type, indent);
|
||
} else {
|
||
closeListsTo(indent);
|
||
if (!listStack.length || listStack[listStack.length - 1].type !== type || listStack[listStack.length - 1].indent !== indent) {
|
||
openList(type, indent);
|
||
}
|
||
}
|
||
output.push('<li>' + renderInline(match[2].trim()) + '</li>');
|
||
continue;
|
||
}
|
||
match = line.match(/^(\s*)\d+\.\s+(.*)$/);
|
||
if (match) {
|
||
const indent = leadingIndent(match[1]);
|
||
const type = 'ol';
|
||
if (!listStack.length || listStack[listStack.length - 1].indent < indent) {
|
||
openList(type, indent);
|
||
} else {
|
||
closeListsTo(indent);
|
||
if (!listStack.length || listStack[listStack.length - 1].type !== type || listStack[listStack.length - 1].indent !== indent) {
|
||
openList(type, indent);
|
||
}
|
||
}
|
||
output.push('<li>' + renderInline(match[2].trim()) + '</li>');
|
||
continue;
|
||
}
|
||
closeAllLists();
|
||
output.push('<p>' + renderInline(line) + '</p>');
|
||
}
|
||
|
||
closeAllLists();
|
||
flushCode();
|
||
return output.join('\n');
|
||
};
|
||
|
||
markdown.refresh = function refreshPreview() {
|
||
const textarea = dom.qs('#remarks');
|
||
const target = dom.qs('#remarks-render');
|
||
if (!textarea || !target) {
|
||
return;
|
||
}
|
||
target.innerHTML = markdown.render(textarea.value || '');
|
||
};
|
||
|
||
markdown.bindRemarksLivePreview = function bindRemarksLivePreview() {
|
||
const textarea = dom.qs('#remarks');
|
||
if (!textarea) {
|
||
return;
|
||
}
|
||
textarea.addEventListener('input', function () {
|
||
if (app.state.mode === 'edit') {
|
||
markdown.refresh();
|
||
}
|
||
});
|
||
markdown.refresh();
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
markdown.bindRemarksLivePreview();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
var dom = app.dom;
|
||
var markdown = app.modules.markdown;
|
||
|
||
var editor = app.modules.markdownEditor = {};
|
||
var inputEl = null; // plain textarea
|
||
var toolbarEl = null; // button bar
|
||
var wrapperEl = null; // container for all editor elements
|
||
var initialized = false;
|
||
var renderClickBound = false;
|
||
var outsideClickBound = false;
|
||
|
||
// ── Textarea helpers ──────────────────────────────────────────────
|
||
|
||
function syncToHiddenTextarea() {
|
||
var remarks = dom.qs('#remarks');
|
||
if (remarks && inputEl) { remarks.value = inputEl.value; }
|
||
}
|
||
|
||
function insertText(before, after, placeholder) {
|
||
if (!inputEl) { return; }
|
||
inputEl.focus();
|
||
var start = inputEl.selectionStart;
|
||
var end = inputEl.selectionEnd;
|
||
var selected = inputEl.value.substring(start, end);
|
||
var insert = selected || placeholder || '';
|
||
var full = before + insert + (after || '');
|
||
// execCommand preserves undo stack in most browsers
|
||
document.execCommand('insertText', false, full);
|
||
// If we used placeholder, select it for easy replacement
|
||
if (!selected && insert) {
|
||
inputEl.selectionStart = start + before.length;
|
||
inputEl.selectionEnd = start + before.length + insert.length;
|
||
}
|
||
}
|
||
|
||
// ── Button bar ────────────────────────────────────────────────────
|
||
|
||
var buttons = [
|
||
{ label: 'B', title: 'Bold', wrap: ['**', '**'], placeholder: 'bold' },
|
||
{ label: 'I', title: 'Italic', wrap: ['*', '*'], placeholder: 'italic' },
|
||
{ label: 'H', title: 'Heading', wrap: ['## ', ''], placeholder: 'heading' },
|
||
{ label: '\u2022', title: 'Bullet list', wrap: ['- ', ''], placeholder: 'item' },
|
||
{ label: '1.', title: 'Numbered list', wrap: ['1. ', ''], placeholder: 'item' },
|
||
{ label: '\uD83D\uDD17', title: 'Link', wrap: ['[', '](url)'], placeholder: 'link text' },
|
||
{ label: '\u229E', title: 'Table', insert: '| Col 1 | Col 2 | Col 3 |\n| --- | --- | --- |\n| | | |\n' }
|
||
];
|
||
|
||
function onButtonClick(event) {
|
||
var btn = event.currentTarget;
|
||
var idx = parseInt(btn.getAttribute('data-idx'), 10);
|
||
var def = buttons[idx];
|
||
if (!def) { return; }
|
||
event.preventDefault();
|
||
if (def.wrap) {
|
||
insertText(def.wrap[0], def.wrap[1], def.placeholder);
|
||
} else if (def.insert) {
|
||
insertText(def.insert, '', '');
|
||
}
|
||
syncToHiddenTextarea();
|
||
app.markDirty();
|
||
}
|
||
|
||
function createToolbar() {
|
||
if (toolbarEl) { return toolbarEl; }
|
||
toolbarEl = document.createElement('div');
|
||
toolbarEl.className = 'md-toolbar';
|
||
buttons.forEach(function (def, i) {
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'md-toolbar-btn';
|
||
btn.textContent = def.label;
|
||
btn.title = def.title;
|
||
btn.setAttribute('data-idx', i);
|
||
btn.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
||
btn.addEventListener('click', onButtonClick);
|
||
toolbarEl.appendChild(btn);
|
||
});
|
||
return toolbarEl;
|
||
}
|
||
|
||
// ── Visibility helpers ────────────────────────────────────────────
|
||
|
||
function refreshRender() {
|
||
var textarea = dom.qs('#remarks');
|
||
var renderEl = dom.qs('#remarks-render');
|
||
if (!textarea || !renderEl) { return; }
|
||
var value = textarea.value || '';
|
||
if (value.trim()) {
|
||
renderEl.innerHTML = markdown.render(value);
|
||
} else if (app.state.mode === 'edit' && !app.state.published) {
|
||
renderEl.innerHTML = '<span class="remarks-placeholder">Click to add remarks</span>';
|
||
} else {
|
||
renderEl.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
function showRendered() {
|
||
if (wrapperEl) { wrapperEl.style.display = 'none'; }
|
||
var renderContainer = dom.qs('#remarks-render-container');
|
||
if (renderContainer) { renderContainer.hidden = false; }
|
||
syncToHiddenTextarea();
|
||
refreshRender();
|
||
}
|
||
|
||
function showEditor() {
|
||
var renderContainer = dom.qs('#remarks-render-container');
|
||
if (renderContainer) { renderContainer.hidden = true; }
|
||
if (!initialized) { editor.init(); }
|
||
if (wrapperEl) { wrapperEl.style.display = ''; }
|
||
var remarks = dom.qs('#remarks');
|
||
if (remarks && inputEl) {
|
||
inputEl.value = remarks.value || '';
|
||
}
|
||
if (inputEl) { inputEl.focus(); }
|
||
}
|
||
|
||
// ── Click-to-edit on rendered preview ─────────────────────────────
|
||
|
||
function onRenderClick() {
|
||
if (app.state.mode !== 'edit' || app.state.published) { return; }
|
||
showEditor();
|
||
}
|
||
|
||
function bindRenderClick() {
|
||
if (renderClickBound) { return; }
|
||
var renderContainer = dom.qs('#remarks-render-container');
|
||
if (!renderContainer) { return; }
|
||
renderContainer.addEventListener('click', onRenderClick);
|
||
renderClickBound = true;
|
||
}
|
||
|
||
function setRenderClickable(clickable) {
|
||
var renderContainer = dom.qs('#remarks-render-container');
|
||
if (!renderContainer) { return; }
|
||
if (clickable) {
|
||
renderContainer.classList.add('remarks-clickable');
|
||
} else {
|
||
renderContainer.classList.remove('remarks-clickable');
|
||
}
|
||
}
|
||
|
||
// ── Outside-click collapse ────────────────────────────────────────
|
||
|
||
function onDocumentMousedown(event) {
|
||
if (!wrapperEl) { return; }
|
||
if (wrapperEl.style.display === 'none') { return; }
|
||
if (wrapperEl.contains(event.target)) { return; }
|
||
showRendered();
|
||
}
|
||
|
||
function bindOutsideClick() {
|
||
if (outsideClickBound) { return; }
|
||
document.addEventListener('mousedown', onDocumentMousedown, true);
|
||
outsideClickBound = true;
|
||
}
|
||
|
||
// ── Public API ────────────────────────────────────────────────────
|
||
|
||
editor.init = function initEditor() {
|
||
if (initialized) { return; }
|
||
var remarksWrapper = dom.qs('#remarks-wrapper');
|
||
if (!remarksWrapper) { return; }
|
||
|
||
wrapperEl = document.createElement('div');
|
||
wrapperEl.id = 'remarks-editor';
|
||
wrapperEl.style.display = 'none';
|
||
|
||
wrapperEl.appendChild(createToolbar());
|
||
|
||
// Edit area container
|
||
var editArea = document.createElement('div');
|
||
editArea.className = 'md-edit-area';
|
||
|
||
// Plain textarea
|
||
inputEl = document.createElement('textarea');
|
||
inputEl.className = 'md-input';
|
||
inputEl.spellcheck = true;
|
||
inputEl.setAttribute('aria-label', 'Remarks');
|
||
editArea.appendChild(inputEl);
|
||
|
||
wrapperEl.appendChild(editArea);
|
||
remarksWrapper.appendChild(wrapperEl);
|
||
|
||
inputEl.addEventListener('input', function () {
|
||
syncToHiddenTextarea();
|
||
app.markDirty();
|
||
});
|
||
|
||
inputEl.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Tab') {
|
||
e.preventDefault();
|
||
document.execCommand('insertText', false, ' ');
|
||
}
|
||
});
|
||
|
||
bindOutsideClick();
|
||
initialized = true;
|
||
};
|
||
|
||
editor.destroy = function destroyEditor() {
|
||
if (wrapperEl) { wrapperEl.style.display = 'none'; }
|
||
syncToHiddenTextarea();
|
||
};
|
||
|
||
editor.showRendered = showRendered;
|
||
editor.showEditor = showEditor;
|
||
editor.refreshRender = refreshRender;
|
||
editor.bindRenderClick = bindRenderClick;
|
||
editor.setRenderClickable = setRenderClickable;
|
||
|
||
editor.isActive = function isActive() {
|
||
return initialized && wrapperEl && wrapperEl.style.display !== 'none';
|
||
};
|
||
|
||
editor.getValue = function getValue() {
|
||
return inputEl ? inputEl.value : '';
|
||
};
|
||
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
var dom = app.dom;
|
||
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
|
||
function escapeHtml(str) {
|
||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
function escapeAttr(str) {
|
||
return str.replace(/&/g, '&').replace(/"/g, '"');
|
||
}
|
||
|
||
// Split on semicolons/commas but respect quoted strings
|
||
function splitRecipients(raw) {
|
||
var parts = [];
|
||
var current = '';
|
||
var inQuote = false;
|
||
for (var i = 0; i < raw.length; i++) {
|
||
var ch = raw[i];
|
||
if (ch === '"') {
|
||
inQuote = !inQuote;
|
||
current += ch;
|
||
} else if ((ch === ';' || ch === ',') && !inQuote) {
|
||
var trimmed = current.trim();
|
||
if (trimmed) { parts.push(trimmed); }
|
||
current = '';
|
||
} else {
|
||
current += ch;
|
||
}
|
||
}
|
||
var last = current.trim();
|
||
if (last) { parts.push(last); }
|
||
return parts;
|
||
}
|
||
|
||
// Extract email from entries like "Display Name <email@host.com>" or bare email
|
||
function extractEmail(entry) {
|
||
var angleMatch = entry.match(/<([^>]+)>/);
|
||
if (angleMatch && EMAIL_RE.test(angleMatch[1])) {
|
||
return angleMatch[1];
|
||
}
|
||
if (EMAIL_RE.test(entry)) {
|
||
return entry;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function renderRecipient(entry) {
|
||
var email = extractEmail(entry);
|
||
if (email) {
|
||
return '<a href="mailto:' + escapeAttr(email) + '" class="to-mailto">' + escapeHtml(entry) + '</a>';
|
||
}
|
||
return '<span>' + escapeHtml(entry) + '</span>';
|
||
}
|
||
|
||
function renderToField() {
|
||
var input = dom.qs('#to');
|
||
var render = dom.qs('#to-render');
|
||
if (!input || !render) { return; }
|
||
|
||
var raw = input.value || '';
|
||
var parts = splitRecipients(raw);
|
||
if (!parts.length) {
|
||
render.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
var html = parts.map(renderRecipient).join('<span class="to-sep">; </span>');
|
||
render.innerHTML = html;
|
||
}
|
||
|
||
function renderFromField() {
|
||
var input = dom.qs('#from');
|
||
var render = dom.qs('#from-render');
|
||
if (!input || !render) { return; }
|
||
|
||
var raw = (input.value || '').trim();
|
||
if (!raw) {
|
||
render.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
var email = extractEmail(raw);
|
||
if (email) {
|
||
render.innerHTML = '<a href="mailto:' + escapeAttr(email) + '" class="from-mailto">' + escapeHtml(raw) + '</a>';
|
||
} else {
|
||
render.innerHTML = '<span>' + escapeHtml(raw) + '</span>';
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
var toInput = dom.qs('#to');
|
||
if (toInput) {
|
||
toInput.addEventListener('input', renderToField);
|
||
toInput.addEventListener('change', renderToField);
|
||
renderToField();
|
||
}
|
||
var fromInput = dom.qs('#from');
|
||
if (fromInput) {
|
||
fromInput.addEventListener('input', renderFromField);
|
||
fromInput.addEventListener('change', renderFromField);
|
||
renderFromField();
|
||
}
|
||
}
|
||
|
||
app.modules.emailTags = {
|
||
render: renderToField,
|
||
renderFrom: renderFromField
|
||
};
|
||
|
||
app.registerInit(init);
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
|
||
const validation = app.modules.validation = {};
|
||
|
||
validation.bindTrackingNumberValidation = function bindTrackingNumberValidation() {
|
||
const input = dom.qs('#tracking-number');
|
||
if (!input) {
|
||
return;
|
||
}
|
||
function isInvalid(value) {
|
||
return /\s/.test(value) || /_/.test(value);
|
||
}
|
||
function apply() {
|
||
const value = input.value || '';
|
||
const bad = isInvalid(value);
|
||
input.setAttribute('aria-invalid', bad ? 'true' : 'false');
|
||
if (bad) {
|
||
input.classList.add('ring-1', 'ring-red-400', 'border-red-500');
|
||
} else {
|
||
input.classList.remove('ring-1', 'ring-red-400', 'border-red-500');
|
||
}
|
||
}
|
||
input.addEventListener('input', apply);
|
||
input.addEventListener('blur', apply);
|
||
apply();
|
||
};
|
||
|
||
validation.validateBeforePublish = function validateBeforePublish() {
|
||
const errors = [];
|
||
let focusEl = null;
|
||
const trackingInput = dom.qs('#tracking-number');
|
||
if (trackingInput) {
|
||
const value = trackingInput.value || '';
|
||
if (/\s/.test(value) || /_/.test(value)) {
|
||
errors.push('Tracking Number must not contain spaces or underscores.');
|
||
if (!focusEl) {
|
||
focusEl = trackingInput;
|
||
}
|
||
}
|
||
}
|
||
(app.data.files || []).forEach(function (file, index) {
|
||
if (file.trackingNumber && (/\s/.test(file.trackingNumber) || /_/.test(file.trackingNumber))) {
|
||
errors.push('Row ' + (index + 1) + ': File tracking number must not contain spaces or underscores.');
|
||
}
|
||
if (file.revision && /\s/.test(file.revision)) {
|
||
errors.push('Row ' + (index + 1) + ': Revision must not contain spaces.');
|
||
}
|
||
});
|
||
return {
|
||
ok: errors.length === 0,
|
||
focusEl: focusEl,
|
||
message: errors.join('\n')
|
||
};
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
validation.bindTrackingNumberValidation();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
const util = app.util;
|
||
const json = app.json;
|
||
|
||
const security = app.modules.security = {};
|
||
|
||
// ── Verify-card DOM helpers ────────────────────────
|
||
function makeCard(variant) {
|
||
var el = document.createElement('div');
|
||
el.className = 'verify-card verify-card--' + variant;
|
||
return el;
|
||
}
|
||
function makeStatus(text, variant) {
|
||
var el = document.createElement('div');
|
||
el.className = 'verify-card__status verify-card__status--' + variant;
|
||
el.textContent = text;
|
||
return el;
|
||
}
|
||
function makeDetail(html) {
|
||
var el = document.createElement('div');
|
||
el.className = 'verify-card__detail';
|
||
el.innerHTML = html;
|
||
return el;
|
||
}
|
||
|
||
security.renderSignaturesList = async function renderSignaturesList() {
|
||
var digestEl = dom.qs('#digest-display');
|
||
var container = dom.qs('#signatures-list');
|
||
var addButton = dom.qs('#add-signature-btn');
|
||
if (!container) { return; }
|
||
|
||
var data = json.parse();
|
||
var envelope = data.envelope || {};
|
||
var signatures = Array.isArray(envelope.signatures) ? envelope.signatures : [];
|
||
var hasDigest = !!(envelope.digest);
|
||
var dv = app.data.digestVerified;
|
||
var results = app.data.signatureVerificationResults || [];
|
||
|
||
// ── Digest card ──
|
||
if (digestEl) {
|
||
digestEl.innerHTML = '';
|
||
if (hasDigest && dv) {
|
||
var unavailable = dv.unavailable;
|
||
var cardType = unavailable ? 'info' : (dv.match ? 'ok' : 'fail');
|
||
var dCard = makeCard(cardType);
|
||
if (unavailable) {
|
||
dCard.appendChild(makeStatus('Digest Present \u2014 verification unavailable', 'info'));
|
||
} else {
|
||
dCard.appendChild(makeStatus(
|
||
(dv.match ? '\u2713 Digest Verified' : '\u2717 Digest Failed'),
|
||
dv.match ? 'ok' : 'fail'
|
||
));
|
||
}
|
||
dCard.appendChild(makeDetail('SHA-256: <code>' + envelope.digest + '</code>'));
|
||
if (envelope.digestedAt) {
|
||
dCard.appendChild(makeDetail(util.formatISOWithTZ(envelope.digestedAt)));
|
||
}
|
||
if (unavailable) {
|
||
dCard.appendChild(makeDetail('<em>Open from file:// or https:// to verify integrity</em>'));
|
||
}
|
||
digestEl.appendChild(dCard);
|
||
}
|
||
}
|
||
|
||
// ── Signature cards ──
|
||
container.innerHTML = '';
|
||
if (!hasDigest) {
|
||
if (addButton) { addButton.hidden = true; }
|
||
return;
|
||
}
|
||
|
||
if (signatures.length === 0) {
|
||
var note = makeCard('info');
|
||
note.appendChild(makeStatus('No signatures \u2014 digest only', 'info'));
|
||
container.appendChild(note);
|
||
}
|
||
|
||
var cryptoUnavailable = dv && dv.unavailable;
|
||
for (var i = 0; i < signatures.length; i++) {
|
||
var sig = signatures[i];
|
||
var fingerprint = await util.publicKeyFingerprint(sig.publicKeyJwk);
|
||
var signedAt = sig.signedAt ? util.formatISOWithTZ(sig.signedAt) : 'Unknown';
|
||
var result = results.find(function (r) { return r.index === i; });
|
||
var ok = result ? result.valid : false;
|
||
var digestBad = dv && !dv.match && !cryptoUnavailable;
|
||
var pass = ok && !digestBad;
|
||
|
||
var sigLabel = sig.label || ('Signature ' + (i + 1));
|
||
var cardType = cryptoUnavailable ? 'info' : (pass ? 'ok' : 'fail');
|
||
var sCard = makeCard(cardType);
|
||
if (cryptoUnavailable) {
|
||
sCard.appendChild(makeStatus(sigLabel + ' \u2014 verification unavailable', 'info'));
|
||
} else {
|
||
sCard.appendChild(makeStatus(
|
||
(pass ? '\u2713 ' + sigLabel + ' Verified' : '\u2717 ' + sigLabel + ' Failed Verification'),
|
||
pass ? 'ok' : 'fail'
|
||
));
|
||
}
|
||
sCard.appendChild(makeDetail('Key: <code>' + (fingerprint || 'Unknown') + '</code>'));
|
||
sCard.appendChild(makeDetail(signedAt));
|
||
container.appendChild(sCard);
|
||
}
|
||
|
||
if (addButton) { addButton.hidden = false; }
|
||
};
|
||
|
||
security.deleteSignature = async function deleteSignature(index) {
|
||
if (!confirm('Delete this signature?')) { return; }
|
||
try {
|
||
var data = json.parse();
|
||
var envelope = data.envelope || {};
|
||
var signatures = Array.isArray(envelope.signatures) ? envelope.signatures : [];
|
||
signatures.splice(index, 1);
|
||
envelope.signatures = signatures;
|
||
json.setData({ envelope: envelope, payload: data.payload, presentation: data.presentation });
|
||
await security.verifySignatureIfPresent();
|
||
if (app.modules.data && app.modules.data.setStatus) {
|
||
app.modules.data.setStatus('Signature deleted', 'success');
|
||
}
|
||
} catch (err) {
|
||
console.error('[transmittal] failed to delete signature', err);
|
||
alert('Failed to delete signature: ' + (err && err.message ? err.message : err));
|
||
}
|
||
};
|
||
|
||
security.addSignature = async function addSignature(options) {
|
||
var opts = options || {};
|
||
var label = opts.label || '';
|
||
try {
|
||
var data = json.parse();
|
||
var envelope = data.envelope || {};
|
||
var payload = data.payload || {};
|
||
|
||
if (!envelope.digest) {
|
||
alert('Cannot add signature: document must be published first (needs digest).');
|
||
return;
|
||
}
|
||
|
||
var jwk = await security.pickSigningKey();
|
||
if (!jwk || jwk.kty !== 'EC') { return; }
|
||
|
||
var sigResult = await util.signEnvelope(envelope, jwk);
|
||
var publicJwk = security.derivePublicFromPrivate(jwk);
|
||
|
||
var sigEntry = {
|
||
signature: sigResult.signature,
|
||
signedAt: sigResult.signedAt,
|
||
publicKeyJwk: publicJwk
|
||
};
|
||
if (label) { sigEntry.label = label; }
|
||
|
||
var signatures = Array.isArray(envelope.signatures) ? envelope.signatures : [];
|
||
signatures.push(sigEntry);
|
||
|
||
envelope.signatures = signatures;
|
||
json.setData({ envelope: envelope, payload: payload, presentation: data.presentation });
|
||
await security.verifySignatureIfPresent();
|
||
|
||
// Persist by downloading updated HTML
|
||
var publish = app.modules.publish;
|
||
if (publish && publish.buildHtmlString) {
|
||
var html = await publish.buildHtmlString();
|
||
var dataModule = app.modules.data;
|
||
var filename = dataModule && dataModule.buildFileName
|
||
? dataModule.buildFileName(payload, { extension: 'html' })
|
||
: 'transmittal.html';
|
||
util.downloadBlob(filename, new Blob([html], { type: 'text/html' }), 'text/html');
|
||
}
|
||
|
||
var statusMsg = (label ? label : 'Signature') + ' added \u2014 download triggered';
|
||
if (app.modules.data && app.modules.data.setStatus) {
|
||
app.modules.data.setStatus(statusMsg, 'success');
|
||
}
|
||
} catch (err) {
|
||
console.error('[transmittal] failed to add signature', err);
|
||
alert('Failed to add signature: ' + (err && err.message ? err.message : err));
|
||
}
|
||
};
|
||
|
||
security.verifyPayloadSignature = async function verifyPayloadSignature(payloadStr, signatureB64, publicJwk) {
|
||
if (!signatureB64 || !publicJwk) {
|
||
return false;
|
||
}
|
||
try {
|
||
const key = await window.crypto.subtle.importKey(
|
||
'jwk',
|
||
publicJwk,
|
||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||
false,
|
||
['verify']
|
||
);
|
||
const signatureBuffer = util.base64ToArrayBuffer(signatureB64);
|
||
const ok = await window.crypto.subtle.verify(
|
||
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
||
key,
|
||
signatureBuffer,
|
||
new TextEncoder().encode(payloadStr)
|
||
);
|
||
return !!ok;
|
||
} catch (err) {
|
||
console.warn('[transmittal] verify error', err);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
security.verifySignatureIfPresent = async function verifySignatureIfPresent() {
|
||
try {
|
||
var data = json.parse();
|
||
var envelope = data.envelope || {};
|
||
var payload = data.payload || {};
|
||
var signatures = Array.isArray(envelope.signatures) ? envelope.signatures : [];
|
||
var hasDigest = !!(envelope.digest);
|
||
|
||
if (!hasDigest) {
|
||
app.data.digestVerified = null;
|
||
app.data.signatureVerificationResults = [];
|
||
await security.renderSignaturesList();
|
||
return;
|
||
}
|
||
|
||
if (!util.hasCrypto()) {
|
||
app.data.digestVerified = { match: false, unavailable: true };
|
||
app.data.signatureVerificationResults = [];
|
||
await security.renderSignaturesList();
|
||
return;
|
||
}
|
||
|
||
// Verify digest matches payload
|
||
var payloadStr = util.canonicalStringify(payload);
|
||
var computedDigest = await util.hashString(payloadStr);
|
||
var digestMatch = envelope.digest && computedDigest &&
|
||
(String(envelope.digest).toLowerCase() === computedDigest.toLowerCase());
|
||
|
||
app.data.digestVerified = {
|
||
match: digestMatch,
|
||
expected: envelope.digest,
|
||
computed: computedDigest
|
||
};
|
||
|
||
if (!digestMatch || signatures.length === 0) {
|
||
app.data.signatureVerificationResults = [];
|
||
await security.renderSignaturesList();
|
||
return;
|
||
}
|
||
|
||
// Reconstruct the envelope that was signed (without signatures)
|
||
var envelopeToVerify = {
|
||
version: envelope.version || 1,
|
||
digestAlgorithm: envelope.digestAlgorithm || app.constants.digestAlgorithm,
|
||
digest: envelope.digest,
|
||
digestedAt: envelope.digestedAt,
|
||
signatureAlgorithm: envelope.signatureAlgorithm || app.constants.signatureAlgorithm
|
||
};
|
||
var envelopeStr = util.canonicalStringify(envelopeToVerify);
|
||
|
||
var results = [];
|
||
for (var i = 0; i < signatures.length; i++) {
|
||
var sig = signatures[i];
|
||
var signatureOk = await security.verifyPayloadSignature(envelopeStr, sig.signature, sig.publicKeyJwk);
|
||
results.push({
|
||
index: i,
|
||
valid: signatureOk,
|
||
publicKey: sig.publicKeyJwk,
|
||
signedAt: sig.signedAt
|
||
});
|
||
}
|
||
|
||
app.data.signatureVerificationResults = results;
|
||
await security.renderSignaturesList();
|
||
} catch (err) {
|
||
console.error('[transmittal] signature verification failed', err);
|
||
app.data.digestVerified = { match: false, error: true };
|
||
app.data.signatureVerificationResults = [];
|
||
await security.renderSignaturesList();
|
||
}
|
||
};
|
||
|
||
security.derivePublicFromPrivate = function derivePublicFromPrivate(jwk) {
|
||
if (!jwk) {
|
||
return null;
|
||
}
|
||
if (jwk.x && jwk.y) {
|
||
return {
|
||
kty: jwk.kty || 'EC',
|
||
crv: jwk.crv || 'P-256',
|
||
x: jwk.x,
|
||
y: jwk.y,
|
||
ext: true
|
||
};
|
||
}
|
||
return null;
|
||
};
|
||
|
||
security.pickSigningKey = function pickSigningKey() {
|
||
return new Promise(function (resolve, reject) {
|
||
var input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.zddc-key';
|
||
input.addEventListener('change', async function () {
|
||
try {
|
||
var file = input.files && input.files[0];
|
||
if (!file) { resolve(null); return; }
|
||
var text = await file.text();
|
||
var jwk = await util.loadKeyFile(text, security.promptForPassword);
|
||
resolve(jwk);
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
});
|
||
input.click();
|
||
});
|
||
};
|
||
|
||
security.promptForPassword = function promptForPassword(fingerprint) {
|
||
return new Promise(function (resolve) {
|
||
var pw = window.prompt(
|
||
fingerprint
|
||
? 'Enter password for key ' + fingerprint + ':'
|
||
: 'Enter key password:'
|
||
);
|
||
resolve(pw);
|
||
});
|
||
};
|
||
|
||
app.registerInit(function () {
|
||
const addSigButton = dom.qs('#add-signature-btn');
|
||
if (addSigButton) {
|
||
addSigButton.addEventListener('click', function() {
|
||
security.addSignature();
|
||
});
|
||
}
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
var dom = app.dom;
|
||
var util = app.util;
|
||
|
||
var verification = app.modules.verification = {};
|
||
|
||
// ── Directory scanning for file hash verification ────
|
||
verification.scanDirectoryForFiles = async function scanDirectoryForFiles(dirHandle, basePath) {
|
||
var base = basePath || '';
|
||
var files = [];
|
||
|
||
for await (var entry of dirHandle.values()) {
|
||
if (entry.kind === 'file') {
|
||
var file = await entry.getFile();
|
||
var sha256 = await util.hashFile(file);
|
||
var path = base ? base + '/' + entry.name : entry.name;
|
||
files.push({ name: entry.name, path: path, sha256: sha256, size: file.size });
|
||
} else if (entry.kind === 'directory') {
|
||
var subPath = base ? base + '/' + entry.name : entry.name;
|
||
var subFiles = await verification.scanDirectoryForFiles(entry, subPath);
|
||
for (var j = 0; j < subFiles.length; j++) { files.push(subFiles[j]); }
|
||
}
|
||
}
|
||
|
||
return files;
|
||
};
|
||
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
const json = app.json;
|
||
|
||
const dataModule = app.modules.data = {};
|
||
|
||
const STATUS_DISPLAY_MS = 6000;
|
||
let statusTimer = null;
|
||
|
||
function qs(selector) {
|
||
return dom.qs(selector);
|
||
}
|
||
|
||
function setStatus(message, type) {
|
||
const element = qs('#data-status');
|
||
if (!element) {
|
||
return;
|
||
}
|
||
element.textContent = message || '';
|
||
element.dataset.statusType = type || '';
|
||
element.classList.remove('text-red-600', 'text-green-600');
|
||
if (type === 'success') {
|
||
element.classList.add('text-green-600');
|
||
} else if (type === 'error') {
|
||
element.classList.add('text-red-600');
|
||
}
|
||
if (statusTimer) {
|
||
clearTimeout(statusTimer);
|
||
}
|
||
if (message) {
|
||
statusTimer = setTimeout(function () {
|
||
element.textContent = '';
|
||
element.classList.remove('text-red-600', 'text-green-600');
|
||
}, STATUS_DISPLAY_MS);
|
||
}
|
||
}
|
||
|
||
const INVALID_FILENAME_CHARS = /[\\/:*?"<>|]+/g;
|
||
|
||
function sanitizeFilenameSegment(value, fallback) {
|
||
const trimmed = (value || '').toString().trim();
|
||
if (!trimmed) {
|
||
return fallback || '';
|
||
}
|
||
return trimmed
|
||
.replace(INVALID_FILENAME_CHARS, '-')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function sanitizeTitle(value) {
|
||
const sanitized = sanitizeFilenameSegment(value, 'Transmittal');
|
||
return sanitized || 'Transmittal';
|
||
}
|
||
|
||
function sanitizeTracking(value) {
|
||
const sanitized = sanitizeFilenameSegment(value, 'TRANS');
|
||
return sanitized.replace(/[_\s]+/g, '-');
|
||
}
|
||
|
||
function sanitizeStatus(value, fallback) {
|
||
const sanitized = sanitizeFilenameSegment(value, fallback || '');
|
||
const cleaned = sanitized.replace(/[()]/g, '').trim();
|
||
if (cleaned) {
|
||
return cleaned;
|
||
}
|
||
return (fallback || 'Unspecified');
|
||
}
|
||
|
||
function sanitizeDate(value) {
|
||
const trimmed = (value || '').toString().trim();
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
||
return trimmed;
|
||
}
|
||
if (!trimmed) {
|
||
return '0000-00-00';
|
||
}
|
||
return trimmed.replace(/\s+/g, '-');
|
||
}
|
||
|
||
// Folder name: DATE_TRACKING (STATUS) - Title (date first for chronological sort)
|
||
dataModule.buildFolderName = function buildFolderName(payload) {
|
||
const p = payload || {};
|
||
const date = sanitizeDate(p.date || '');
|
||
const tracking = sanitizeTracking(p.trackingNumber || '');
|
||
const status = sanitizeStatus(p.status || p.purpose || '', 'Unspecified');
|
||
const title = sanitizeTitle(p.subject || p.title || '');
|
||
const statusPart = status ? ' (' + status + ')' : '';
|
||
return date + '_' + tracking + statusPart + ' - ' + title;
|
||
};
|
||
|
||
// File name: TRACKING_[~]DATE (STATUS) - Subject.ext
|
||
// draft: true → ~DATE (tilde prefix indicates draft)
|
||
dataModule.buildFileName = function buildFileName(payload, options) {
|
||
const p = payload || {};
|
||
const opts = options || {};
|
||
const ext = opts.extension ? opts.extension.replace(/^\.+/, '') : 'json';
|
||
const isDraft = !!opts.draft;
|
||
const tracking = sanitizeTracking(p.trackingNumber || '');
|
||
const date = sanitizeDate(p.date || '');
|
||
const status = sanitizeStatus(p.status || p.purpose || '', 'Unspecified');
|
||
const title = sanitizeTitle(p.subject || p.title || '');
|
||
const statusPart = status ? ' (' + status + ')' : '';
|
||
const datePrefix = isDraft ? '~' : '';
|
||
return tracking + '_' + datePrefix + date + statusPart + ' - ' + title + '.' + ext;
|
||
};
|
||
|
||
async function saveFileWithPicker(filename, contents, mime) {
|
||
if (typeof window.showSaveFilePicker !== 'function') {
|
||
app.util.downloadBlob(filename, contents, mime);
|
||
return filename;
|
||
}
|
||
const handle = await window.showSaveFilePicker({
|
||
suggestedName: filename,
|
||
types: [
|
||
{
|
||
description: mime || 'File',
|
||
accept: { [mime || 'application/octet-stream']: ['.' + zddc.splitExtension(filename).extension] }
|
||
}
|
||
]
|
||
});
|
||
const writable = await handle.createWritable();
|
||
await writable.write(new Blob([contents], { type: mime || 'application/octet-stream' }));
|
||
await writable.close();
|
||
return handle.name || filename;
|
||
}
|
||
|
||
async function serializeUiToJson() {
|
||
if (!app.modules.publish || typeof app.modules.publish.syncUiToJson !== 'function') {
|
||
throw new Error('Publish module not ready');
|
||
}
|
||
await app.modules.publish.syncUiToJson({ sign: false, computeDigest: false });
|
||
app.state.dirty = false;
|
||
}
|
||
|
||
// ── Extract JSON from an HTML transmittal string ────
|
||
function extractJsonFromHtml(htmlText) {
|
||
var m = htmlText.match(new RegExp('<script\\s+id\\s*=\\s*["\']transmittal-data["\'][^>]*>([\\s\\S]*?)</' + 'script>', 'i'));
|
||
if (!m || !m[1]) { return null; }
|
||
try {
|
||
var data = JSON.parse(m[1]);
|
||
if (data && typeof data === 'object' && data.payload) { return data; }
|
||
} catch (_) { /* not valid JSON */ }
|
||
return null;
|
||
}
|
||
|
||
function pickHtmlFile() {
|
||
if (typeof window.showOpenFilePicker === 'function') {
|
||
return window.showOpenFilePicker({
|
||
multiple: false,
|
||
types: [{ description: 'HTML Files', accept: { 'text/html': ['.html', '.htm'] } }]
|
||
}).then(function (handles) {
|
||
var handle = handles[0];
|
||
return handle.getFile().then(function (file) {
|
||
return { file: file, name: handle.name || file.name || 'import.html' };
|
||
});
|
||
});
|
||
}
|
||
return new Promise(function (resolve, reject) {
|
||
var input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = 'text/html,.html,.htm';
|
||
input.addEventListener('change', function () {
|
||
var file = input.files && input.files[0];
|
||
if (!file) { reject(new Error('No file selected')); return; }
|
||
resolve({ file: file, name: file.name || 'import.html' });
|
||
});
|
||
input.click();
|
||
});
|
||
}
|
||
|
||
async function handleImportHtml() {
|
||
try {
|
||
var picked = await pickHtmlFile();
|
||
var text = await picked.file.text();
|
||
var data = extractJsonFromHtml(text);
|
||
if (!data) { throw new Error('No valid transmittal data found in this HTML file'); }
|
||
await applyLoadedData(data, picked.name || 'import.html');
|
||
} catch (err) {
|
||
if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) { return; }
|
||
console.error('[transmittal] import-html failed', err);
|
||
setStatus('Failed to import HTML: ' + (err && err.message ? err.message : err), 'error');
|
||
}
|
||
}
|
||
|
||
async function applyLoadedData(data, sourceName) {
|
||
json.setData(data);
|
||
app.modules.files.loadFromJson();
|
||
app.state.mode = 'edit';
|
||
app.state.dirty = false;
|
||
if (app.modules.visibility && app.modules.visibility.applyFieldVisibility) {
|
||
app.modules.visibility.applyFieldVisibility();
|
||
}
|
||
await app.modules.security.verifySignatureIfPresent();
|
||
if (app.modules.security.renderSignaturesList) {
|
||
await app.modules.security.renderSignaturesList();
|
||
}
|
||
app.modules.mode.refresh();
|
||
app.state.apply();
|
||
if (app.modules.liveDigest && app.modules.liveDigest.schedule) {
|
||
app.modules.liveDigest.schedule();
|
||
}
|
||
setStatus('Loaded: ' + sourceName, 'success');
|
||
}
|
||
|
||
app.onDirty(function () {
|
||
setStatus('Unsaved changes', 'info');
|
||
});
|
||
|
||
async function handleLoadFromClipboard() {
|
||
try {
|
||
var text = await navigator.clipboard.readText();
|
||
if (!text || !text.trim()) {
|
||
setStatus('Clipboard is empty', 'error');
|
||
return;
|
||
}
|
||
var data = JSON.parse(text.trim());
|
||
if (!data || typeof data !== 'object' || !data.payload) {
|
||
throw new Error('Invalid ZDDC JSON data');
|
||
}
|
||
await applyLoadedData(data, 'clipboard');
|
||
} catch (err) {
|
||
console.error('[transmittal] load-from-clipboard failed', err);
|
||
if (err && err.name === 'NotAllowedError') {
|
||
setStatus('Clipboard access denied — please paste manually', 'error');
|
||
} else {
|
||
setStatus('Failed to load from clipboard: ' + (err && err.message ? err.message : err), 'error');
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleCopyJson() {
|
||
try {
|
||
await serializeUiToJson();
|
||
var data = json.parse();
|
||
var text = JSON.stringify(data, null, 2);
|
||
await navigator.clipboard.writeText(text);
|
||
setStatus('JSON copied to clipboard', 'success');
|
||
} catch (err) {
|
||
console.error('[transmittal] copy-json failed', err);
|
||
if (err && err.name === 'NotAllowedError') {
|
||
setStatus('Clipboard access denied', 'error');
|
||
} else {
|
||
setStatus('Failed to copy JSON: ' + (err && err.message ? err.message : err), 'error');
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleSaveHtmlDraft() {
|
||
try {
|
||
var pub = app.modules.publish;
|
||
if (!pub || typeof pub.syncUiToJson !== 'function' || typeof pub.buildHtmlString !== 'function') {
|
||
throw new Error('Publish module not ready');
|
||
}
|
||
await pub.syncUiToJson({ sign: false, computeDigest: false });
|
||
var html = await pub.buildHtmlString();
|
||
var data = json.parse();
|
||
var payload = (data && data.payload) || {};
|
||
var filename = dataModule.buildFileName(payload, { extension: 'html', draft: true });
|
||
await saveFileWithPicker(filename, html, 'text/html');
|
||
setStatus('Saved draft as ' + filename, 'success');
|
||
} catch (err) {
|
||
console.error('[transmittal] save-html-draft failed', err);
|
||
setStatus('Failed to save draft: ' + (err && err.message ? err.message : err), 'error');
|
||
}
|
||
}
|
||
|
||
async function handleCopyTable() {
|
||
try {
|
||
var TAB = '\t';
|
||
var NL = '\r\n';
|
||
var esc = app.util.escapeHtml;
|
||
var textStyle = ' style="mso-number-format:\'\\@\'"';
|
||
var headers = ['#', 'TRACKING NUMBER', 'TITLE', 'REVISION', 'STATUS', 'EXT', 'SIZE', 'SHA256'];
|
||
var headerTsv = headers.join(TAB);
|
||
var headerHtml = '<tr>' + headers.map(function (h) { return '<th' + textStyle + '>' + esc(h) + '</th>'; }).join('') + '</tr>';
|
||
var tsvLines = [headerTsv];
|
||
var htmlRows = [headerHtml];
|
||
|
||
function addRow(cells) {
|
||
tsvLines.push(cells.join(TAB));
|
||
htmlRows.push('<tr>' + cells.map(function (c) { return '<td' + textStyle + '>' + esc(c) + '</td>'; }).join('') + '</tr>');
|
||
}
|
||
|
||
// Row 0: self-entry
|
||
var filesModule = app.modules.files;
|
||
var self = filesModule && filesModule.buildSelfEntry ? filesModule.buildSelfEntry() : null;
|
||
if (self) {
|
||
addRow(['0', self.trackingNumber || '', self.title || '', self.revision || '', self.status || '', 'html', '\u2014', 'see above']);
|
||
}
|
||
|
||
// Regular files
|
||
var files = Array.isArray(app.data.files) ? app.data.files : [];
|
||
for (var i = 0; i < files.length; i++) {
|
||
var f = files[i];
|
||
var size = (f.fileSize != null ? f.fileSize : f.size);
|
||
addRow([
|
||
String(i + 1),
|
||
f.trackingNumber || '',
|
||
f.title || '',
|
||
f.revision || '',
|
||
f.status || '',
|
||
(f.extension || '').toLowerCase(),
|
||
size ? String(size) : '',
|
||
f.sha256 || ''
|
||
]);
|
||
}
|
||
|
||
var plainText = tsvLines.join(NL);
|
||
var html = '<table>' + htmlRows.join('') + '</table>';
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({
|
||
'text/plain': new Blob([plainText], { type: 'text/plain' }),
|
||
'text/html': new Blob([html], { type: 'text/html' })
|
||
})
|
||
]);
|
||
setStatus('Table copied (' + files.length + ' files)', 'success');
|
||
} catch (err) {
|
||
console.error('[transmittal] copy-table failed', err);
|
||
setStatus('Failed to copy table: ' + (err && err.message ? err.message : err), 'error');
|
||
}
|
||
}
|
||
|
||
async function handleRevise() {
|
||
if (!confirm('This will create a new draft from the published transmittal. The digest and signatures will be removed. Continue?')) {
|
||
return;
|
||
}
|
||
var data = json.parse();
|
||
if (!data) { return; }
|
||
// Strip digest and signatures; preserve existing date for user to update
|
||
data.envelope = { version: 1, digestAlgorithm: app.constants.digestAlgorithm, digest: '', digestedAt: '', signatureAlgorithm: app.constants.signatureAlgorithm, signatures: [] };
|
||
json.setData(data);
|
||
app.modules.files.loadFromJson();
|
||
app.state.mode = 'edit';
|
||
app.state.published = false;
|
||
app.state.dirty = true;
|
||
app.modules.mode.refresh();
|
||
setStatus('Created new draft from published transmittal', 'success');
|
||
}
|
||
|
||
dataModule.extractJsonFromHtml = extractJsonFromHtml;
|
||
dataModule.setStatus = setStatus;
|
||
dataModule.serializeUiToJson = serializeUiToJson;
|
||
dataModule.handleLoadFromClipboard = handleLoadFromClipboard;
|
||
dataModule.handleCopyJson = handleCopyJson;
|
||
dataModule.handleCopyTable = handleCopyTable;
|
||
dataModule.handleSaveHtmlDraft = handleSaveHtmlDraft;
|
||
dataModule.handleRevise = handleRevise;
|
||
dataModule.handleImportHtml = handleImportHtml;
|
||
dataModule.applyLoadedData = applyLoadedData;
|
||
dataModule.buildFileName = dataModule.buildFileName || buildFileName;
|
||
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
const util = app.util;
|
||
const json = app.json;
|
||
const security = app.modules.security;
|
||
const validation = app.modules.validation;
|
||
const filesModule = app.modules.files;
|
||
const dataModule = app.modules.data;
|
||
const mode = app.modules.mode;
|
||
|
||
async function syncUiToJson(options) {
|
||
const sign = !!(options && options.sign);
|
||
const computeDigest = !!(options && options.computeDigest);
|
||
|
||
// First, sync UI to JSON to ensure JSON is up-to-date
|
||
filesModule.syncUiToJson();
|
||
|
||
// Now use JSON as single source of truth
|
||
const data = json.parse();
|
||
const previousEnvelope = (data && data.envelope) || {};
|
||
const payload = (data && data.payload) || {};
|
||
const previousPresentation = (data && data.presentation) || {};
|
||
const payloadStr = util.canonicalStringify(payload);
|
||
const previousPayloadStr = util.canonicalStringify(payload);
|
||
const payloadChanged = false; // Always false since we just synced
|
||
|
||
let digest = previousEnvelope.digest || '';
|
||
let digestedAt = previousEnvelope.digestedAt || '';
|
||
let signatures = Array.isArray(previousEnvelope.signatures) ? previousEnvelope.signatures.slice() : [];
|
||
|
||
let computedDigest = '';
|
||
if (sign || computeDigest || payloadChanged || !digest) {
|
||
try {
|
||
computedDigest = await util.hashString(payloadStr);
|
||
} catch (err) {
|
||
computedDigest = '';
|
||
}
|
||
}
|
||
|
||
if (sign || computeDigest) {
|
||
if (!computedDigest) {
|
||
throw new Error('Unable to compute digest.');
|
||
}
|
||
digest = computedDigest;
|
||
digestedAt = util.fetchTrustedTime();
|
||
}
|
||
|
||
if (sign) {
|
||
const jwk = options && options.signingKey;
|
||
if (!jwk || jwk.kty !== 'EC') {
|
||
throw new Error('A valid EC private key (JWK) is required to sign this transmittal.');
|
||
}
|
||
const envelopeForSigning = {
|
||
version: 1,
|
||
digestAlgorithm: app.constants.digestAlgorithm,
|
||
digest: digest,
|
||
digestedAt: digestedAt,
|
||
signatureAlgorithm: app.constants.signatureAlgorithm
|
||
};
|
||
const sigResult = await util.signEnvelope(envelopeForSigning, jwk);
|
||
const publicJwk = security.derivePublicFromPrivate(jwk);
|
||
signatures.push({
|
||
signature: sigResult.signature,
|
||
signedAt: sigResult.signedAt,
|
||
publicKeyJwk: publicJwk
|
||
});
|
||
}
|
||
|
||
const envelope = {
|
||
version: 1,
|
||
digestAlgorithm: app.constants.digestAlgorithm,
|
||
digest: digest,
|
||
digestedAt: digestedAt,
|
||
signatureAlgorithm: app.constants.signatureAlgorithm,
|
||
signatures: signatures
|
||
};
|
||
const presentation = previousPresentation;
|
||
json.setData({ envelope: envelope, payload: payload, presentation: presentation });
|
||
if (computeDigest || sign) {
|
||
try {
|
||
await security.verifySignatureIfPresent();
|
||
} catch (_) {
|
||
// ignore
|
||
}
|
||
app.state.apply();
|
||
}
|
||
app.state.dirty = false;
|
||
}
|
||
|
||
async function buildHtmlString() {
|
||
// Hide editor before cloning so its markup is not in the output
|
||
var mdEditor = app.modules.markdownEditor;
|
||
var editorWasActive = mdEditor && mdEditor.isActive();
|
||
if (editorWasActive) { mdEditor.destroy(); }
|
||
|
||
// ── Preferred path: fetch the page's own source and patch the JSON ──
|
||
// This produces an exact copy of the original file with only the data
|
||
// changed, avoiding DOM-snapshot drift. Falls back to the DOM-snapshot
|
||
// approach when the page cannot fetch itself (e.g. opaque origins).
|
||
try {
|
||
var jsonData = app.json.parse();
|
||
var html = await util.fetchAndPatchHtml(jsonData);
|
||
if (editorWasActive && mdEditor && typeof mdEditor.showRendered === 'function') {
|
||
mdEditor.showRendered();
|
||
}
|
||
return html;
|
||
} catch (fetchErr) {
|
||
console.warn('[transmittal] fetch-based save unavailable, falling back to DOM snapshot:', fetchErr.message);
|
||
}
|
||
|
||
// ── Fallback path: DOM snapshot ─────────────────────────────────────
|
||
// Populate static content before cloning for progressive enhancement
|
||
if (app.modules.hydrate && app.modules.hydrate.populateStatic) {
|
||
await app.modules.hydrate.populateStatic();
|
||
}
|
||
|
||
// Remove any existing success notifications
|
||
var notifications = document.querySelectorAll('[data-publish-notification]');
|
||
notifications.forEach(function (notif) { notif.remove(); });
|
||
|
||
// Hide workflow edit-only steps before cloning (viewers shouldn't see them)
|
||
var workflowEditSteps = dom.qsa('.workflow-step[data-edit-only], .workflow-tools');
|
||
var wasVisible = [];
|
||
workflowEditSteps.forEach(function (el) {
|
||
wasVisible.push(!el.hidden);
|
||
el.hidden = true;
|
||
});
|
||
|
||
// Re-hide bottom menu and restore no-js notice before cloning
|
||
var bottomMenu = dom.qs('#bottom-menu');
|
||
var bottomMenuWasVisible = bottomMenu && !bottomMenu.hidden;
|
||
if (bottomMenu) { bottomMenu.hidden = true; }
|
||
var noJsNotice = dom.qs('#no-js-notice');
|
||
var addedNotice = false;
|
||
if (!noJsNotice && bottomMenu && bottomMenu.parentNode) {
|
||
noJsNotice = document.createElement('span');
|
||
noJsNotice.id = 'no-js-notice';
|
||
noJsNotice.className = 'text-gray-400 text-xs italic';
|
||
noJsNotice.textContent = 'JavaScript not available';
|
||
bottomMenu.parentNode.insertBefore(noJsNotice, bottomMenu.nextSibling);
|
||
addedNotice = true;
|
||
}
|
||
|
||
// Close any open dialogs before cloning (they get reopened after)
|
||
var openDialogs = Array.from(document.querySelectorAll('dialog[open]'));
|
||
openDialogs.forEach(function (dlg) { dlg.close(); });
|
||
|
||
var snapshotHtml = util.cloneDocumentHtml();
|
||
|
||
// Restore bottom menu visibility
|
||
if (bottomMenu && bottomMenuWasVisible) { bottomMenu.hidden = false; }
|
||
if (noJsNotice) { dom.show(noJsNotice, false); }
|
||
|
||
// Reopen dialogs that were open
|
||
openDialogs.forEach(function (dlg) { dlg.showModal(); });
|
||
|
||
// Restore workflow elements
|
||
workflowEditSteps.forEach(function (el, i) {
|
||
if (wasVisible[i]) { el.hidden = false; }
|
||
});
|
||
|
||
// Re-hydrate to restore dynamic state after cloning
|
||
if (app.modules.hydrate && app.modules.hydrate.hydrate) {
|
||
app.modules.hydrate.hydrate();
|
||
}
|
||
|
||
// Show rendered preview again (editor loads on next click)
|
||
if (mdEditor) { mdEditor.showRendered(); }
|
||
|
||
return snapshotHtml;
|
||
}
|
||
|
||
function requireDirectoryHandle() {
|
||
if (!app.data.selectedDirHandle) {
|
||
throw new Error('Select a directory before saving.');
|
||
}
|
||
return app.data.selectedDirHandle;
|
||
}
|
||
|
||
async function writeHtmlToDirectory(filename, html) {
|
||
requireDirectoryHandle();
|
||
await filesModule.writeFileToSelectedDir(filename, html, 'text/html');
|
||
}
|
||
|
||
function loadSigningKey(providedKey) {
|
||
if (providedKey) { return providedKey; }
|
||
throw new Error('A signing key is required. Select or generate one first.');
|
||
}
|
||
|
||
async function executePublish(config) {
|
||
const modeSelection = (config && config.mode) || 'digest';
|
||
const wantDownload = !!(config && config.download);
|
||
const wantSaveToFolder = !!(config && config.saveToFolder);
|
||
|
||
const validationResult = validation.validateBeforePublish();
|
||
if (!validationResult.ok) {
|
||
const error = new Error(validationResult.message || 'Validation failed before publishing.');
|
||
error.focusEl = validationResult.focusEl || null;
|
||
throw error;
|
||
}
|
||
|
||
if (wantSaveToFolder) {
|
||
requireDirectoryHandle();
|
||
}
|
||
|
||
const shouldSign = modeSelection === 'signed';
|
||
const shouldComputeDigest = modeSelection === 'signed' || modeSelection === 'digest';
|
||
const isDraft = modeSelection === 'draft';
|
||
|
||
let signingKey = null;
|
||
if (shouldSign) {
|
||
signingKey = await loadSigningKey(config && config.signingKey);
|
||
}
|
||
|
||
await syncUiToJson({
|
||
sign: shouldSign,
|
||
computeDigest: shouldComputeDigest,
|
||
signingKey: signingKey
|
||
});
|
||
|
||
mode.refresh();
|
||
const html = await buildHtmlString();
|
||
const data = json.parse();
|
||
const payload = (data && data.payload) || {};
|
||
const filename = dataModule.buildFileName(payload, { extension: 'html', draft: isDraft });
|
||
|
||
if (wantSaveToFolder) {
|
||
await writeHtmlToDirectory(filename, html);
|
||
// If publishing (not draft), delete the draft file from the directory
|
||
if (!isDraft) {
|
||
var draftName = dataModule.buildFileName(payload, { extension: 'html', draft: true });
|
||
if (draftName !== filename) {
|
||
try {
|
||
await app.data.selectedDirHandle.removeEntry(draftName);
|
||
} catch (_) {
|
||
// Draft file may not exist — that's fine
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (wantDownload) {
|
||
util.downloadBlob(filename, new Blob([html], { type: 'text/html' }), 'text/html');
|
||
}
|
||
mode.refresh();
|
||
dataModule.setStatus('Published HTML as ' + filename, 'success');
|
||
|
||
return {
|
||
filename: filename,
|
||
mode: modeSelection
|
||
};
|
||
}
|
||
|
||
app.modules.publish = {
|
||
syncUiToJson,
|
||
buildHtmlString,
|
||
executePublish
|
||
};
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
var dom = app.dom;
|
||
var json = app.json;
|
||
var filesModule = app.modules.files;
|
||
var security = app.modules.security;
|
||
|
||
function originalEmptyData() {
|
||
return {
|
||
envelope: {
|
||
version: 1,
|
||
digestAlgorithm: app.constants.digestAlgorithm,
|
||
digest: '',
|
||
digestedAt: '',
|
||
signatureAlgorithm: app.constants.signatureAlgorithm,
|
||
signatures: []
|
||
},
|
||
payload: {
|
||
version: 1,
|
||
type: 'Transmittal',
|
||
title: '',
|
||
client: '',
|
||
project: '',
|
||
projectNumber: '',
|
||
date: '',
|
||
trackingNumber: '',
|
||
from: '',
|
||
to: '',
|
||
purpose: '',
|
||
responseDue: '',
|
||
subject: '',
|
||
remarks: '',
|
||
files: []
|
||
},
|
||
presentation: {
|
||
leftLogo: '',
|
||
rightLogo: '',
|
||
theme: 'default',
|
||
customCss: ''
|
||
}
|
||
};
|
||
}
|
||
|
||
async function handleReset() {
|
||
if (!confirm('Reset will clear all data. Continue?')) {
|
||
return;
|
||
}
|
||
json.setData(originalEmptyData());
|
||
|
||
// Clear logos in DOM
|
||
var leftLogo = dom.qs('#left-logo');
|
||
var rightLogo = dom.qs('#right-logo');
|
||
if (leftLogo) { leftLogo.src = ''; }
|
||
if (rightLogo) { rightLogo.src = ''; }
|
||
|
||
// Clear runtime state
|
||
app.data.files = [];
|
||
app.data.selectedDirHandle = null;
|
||
app.data.selectedDirName = '';
|
||
|
||
filesModule.loadFromJson();
|
||
app.state.mode = 'edit';
|
||
app.state.published = false;
|
||
app.state.dirty = false;
|
||
if (security.renderSignaturesList) {
|
||
security.renderSignaturesList();
|
||
}
|
||
if (app.modules.mode && app.modules.mode.refresh) {
|
||
app.modules.mode.refresh();
|
||
}
|
||
app.state.apply();
|
||
if (app.modules.liveDigest && app.modules.liveDigest.schedule) {
|
||
app.modules.liveDigest.schedule();
|
||
}
|
||
}
|
||
|
||
app.modules.reset = {
|
||
handleReset: handleReset
|
||
};
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
if (!app || !app.dom || !app.modules) {
|
||
console.error('[publish-modal] App not properly initialized');
|
||
return;
|
||
}
|
||
|
||
var dom = app.dom;
|
||
var util = app.util;
|
||
|
||
function qs(sel) { return dom.qs(sel); }
|
||
|
||
// ── Date helpers ────────────────────────────────────
|
||
function getTodayIso() {
|
||
var d = new Date();
|
||
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
||
}
|
||
|
||
function toIsoDate(str) {
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) { return str; }
|
||
var d = new Date(str);
|
||
if (isNaN(d.getTime())) { return str; }
|
||
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
||
}
|
||
|
||
function syncDateWarning() {
|
||
var dateInput = qs('#publish-date-input');
|
||
var warning = qs('#publish-date-warning');
|
||
if (!dateInput || !warning) { return; }
|
||
var val = (dateInput.value || '').trim();
|
||
var today = getTodayIso();
|
||
if (!val || val === today) {
|
||
warning.textContent = '';
|
||
warning.hidden = true;
|
||
} else {
|
||
warning.textContent = '\u26A0 Date differs from today (' + today + ')';
|
||
warning.hidden = false;
|
||
}
|
||
}
|
||
|
||
// ── Feedback helpers ────────────────────────────────
|
||
function showFeedback(feedbackEl, message, type) {
|
||
if (!feedbackEl) { return; }
|
||
feedbackEl.textContent = message || '';
|
||
feedbackEl.style.color = type === 'error' ? '#dc2626' : type === 'success' ? '#16a34a' : '';
|
||
}
|
||
|
||
// ── Success notification ────────────────────────────
|
||
function showSuccessNotification(filename) {
|
||
var notification = document.createElement('div');
|
||
notification.className = 'publish-notification';
|
||
notification.setAttribute('data-publish-notification', 'true');
|
||
|
||
var title = document.createElement('div');
|
||
title.className = 'publish-notification__title';
|
||
title.textContent = '\u2713 Transmittal Published Successfully';
|
||
|
||
var file = document.createElement('div');
|
||
file.className = 'publish-notification__file';
|
||
file.textContent = 'Saved as: ' + filename;
|
||
|
||
var closeBtn = document.createElement('button');
|
||
closeBtn.className = 'publish-notification__close';
|
||
closeBtn.textContent = 'Close';
|
||
closeBtn.addEventListener('click', function () { notification.remove(); });
|
||
|
||
notification.appendChild(title);
|
||
notification.appendChild(file);
|
||
notification.appendChild(closeBtn);
|
||
document.body.appendChild(notification);
|
||
setTimeout(function () { if (notification.parentElement) { notification.remove(); } }, 10000);
|
||
}
|
||
|
||
// ── Password dialog ─────────────────────────────────
|
||
function promptForPassword(fingerprint) {
|
||
return new Promise(function (resolve) {
|
||
var dialog = qs('#password-dialog');
|
||
var input = qs('#password-dialog-input');
|
||
var okBtn = qs('#password-dialog-ok');
|
||
var feedback = qs('#password-dialog-feedback');
|
||
var fpEl = qs('#password-dialog-fingerprint');
|
||
if (!dialog || !input || !okBtn) { resolve(null); return; }
|
||
|
||
input.value = '';
|
||
showFeedback(feedback, '', '');
|
||
if (fpEl) {
|
||
fpEl.textContent = fingerprint ? 'Key fingerprint: ' + fingerprint : 'This key is password-protected.';
|
||
}
|
||
dialog.showModal();
|
||
input.focus();
|
||
|
||
function cleanup() {
|
||
okBtn.removeEventListener('click', onOk);
|
||
dialog.removeEventListener('close', onClose);
|
||
}
|
||
function onOk() {
|
||
var pw = input.value;
|
||
cleanup();
|
||
dialog.close();
|
||
resolve(pw);
|
||
}
|
||
function onClose() {
|
||
cleanup();
|
||
resolve(null);
|
||
}
|
||
okBtn.addEventListener('click', onOk);
|
||
dialog.addEventListener('close', onClose);
|
||
|
||
// Enter key submits
|
||
input.addEventListener('keydown', function handler(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
input.removeEventListener('keydown', handler);
|
||
onOk();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Key dialog ──────────────────────────────────────
|
||
function openKeyDialog() {
|
||
return new Promise(function (resolve) {
|
||
var dialog = qs('#key-dialog');
|
||
var selectBtn = qs('#key-select-file');
|
||
var generateBtn = qs('#key-generate');
|
||
var generatePanel = qs('#key-generate-panel');
|
||
var genConfirmBtn = qs('#key-generate-confirm');
|
||
var genCancelBtn = qs('#key-generate-cancel');
|
||
var pwInput = qs('#key-password');
|
||
var pwConfirm = qs('#key-password-confirm');
|
||
var feedback = qs('#key-dialog-feedback');
|
||
if (!dialog) { resolve(null); return; }
|
||
|
||
// Reset state
|
||
if (generatePanel) { generatePanel.hidden = true; }
|
||
if (pwInput) { pwInput.value = ''; }
|
||
if (pwConfirm) { pwConfirm.value = ''; }
|
||
showFeedback(feedback, '', '');
|
||
dialog.showModal();
|
||
|
||
var resolved = false;
|
||
function done(jwk) {
|
||
if (resolved) { return; }
|
||
resolved = true;
|
||
cleanup();
|
||
if (dialog.open) { dialog.close(); }
|
||
resolve(jwk);
|
||
}
|
||
function cleanup() {
|
||
if (selectBtn) { selectBtn.removeEventListener('click', onSelect); }
|
||
if (generateBtn) { generateBtn.removeEventListener('click', onGenerate); }
|
||
if (genConfirmBtn) { genConfirmBtn.removeEventListener('click', onGenConfirm); }
|
||
if (genCancelBtn) { genCancelBtn.removeEventListener('click', onGenCancel); }
|
||
dialog.removeEventListener('close', onDialogClose);
|
||
}
|
||
|
||
function onDialogClose() { done(null); }
|
||
|
||
async function onSelect() {
|
||
try {
|
||
var security = app.modules.security;
|
||
var jwk = await pickKeyFileWithPassword();
|
||
if (jwk) { done(jwk); }
|
||
} catch (err) {
|
||
showFeedback(feedback, 'Failed to load key: ' + (err.message || err), 'error');
|
||
}
|
||
}
|
||
|
||
function onGenerate() {
|
||
if (generatePanel) { generatePanel.hidden = false; }
|
||
if (generateBtn) { generateBtn.hidden = true; }
|
||
if (selectBtn) { selectBtn.hidden = true; }
|
||
}
|
||
|
||
function onGenCancel() {
|
||
if (generatePanel) { generatePanel.hidden = true; }
|
||
if (generateBtn) { generateBtn.hidden = false; }
|
||
if (selectBtn) { selectBtn.hidden = false; }
|
||
if (pwInput) { pwInput.value = ''; }
|
||
if (pwConfirm) { pwConfirm.value = ''; }
|
||
}
|
||
|
||
async function onGenConfirm() {
|
||
var pw = pwInput ? pwInput.value : '';
|
||
var pwc = pwConfirm ? pwConfirm.value : '';
|
||
if (pw && pw !== pwc) {
|
||
showFeedback(feedback, 'Passwords do not match.', 'error');
|
||
return;
|
||
}
|
||
try {
|
||
showFeedback(feedback, 'Generating key\u2026', '');
|
||
var security = app.modules.security;
|
||
var keys = await window.crypto.subtle.generateKey(
|
||
{ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']
|
||
);
|
||
var privateJwk = await window.crypto.subtle.exportKey('jwk', keys.privateKey);
|
||
var publicJwk = security.derivePublicFromPrivate(privateJwk);
|
||
var fingerprint = await util.publicKeyFingerprint(publicJwk);
|
||
|
||
var keyFileData;
|
||
if (pw) {
|
||
keyFileData = await util.encryptPrivateKey(privateJwk, pw, fingerprint);
|
||
} else {
|
||
keyFileData = util.wrapKeyFile(privateJwk, fingerprint);
|
||
}
|
||
util.downloadBlob('signing-key.zddc-key', JSON.stringify(keyFileData, null, 2), 'application/json');
|
||
showFeedback(feedback, 'Key downloaded. Fingerprint: ' + (fingerprint || '(unknown)'), 'success');
|
||
done(privateJwk);
|
||
} catch (err) {
|
||
showFeedback(feedback, 'Failed to generate key: ' + (err.message || err), 'error');
|
||
}
|
||
}
|
||
|
||
if (selectBtn) { selectBtn.addEventListener('click', onSelect); }
|
||
if (generateBtn) { generateBtn.addEventListener('click', onGenerate); }
|
||
if (genConfirmBtn) { genConfirmBtn.addEventListener('click', onGenConfirm); }
|
||
if (genCancelBtn) { genCancelBtn.addEventListener('click', onGenCancel); }
|
||
dialog.addEventListener('close', onDialogClose);
|
||
});
|
||
}
|
||
|
||
// ── Pick key file with automatic password prompt ────
|
||
function pickKeyFileWithPassword() {
|
||
return new Promise(function (resolve, reject) {
|
||
var input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.zddc-key';
|
||
input.addEventListener('change', async function () {
|
||
try {
|
||
var file = input.files && input.files[0];
|
||
if (!file) { resolve(null); return; }
|
||
var text = await file.text();
|
||
var jwk = await util.loadKeyFile(text, promptForPassword);
|
||
resolve(jwk);
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
});
|
||
input.click();
|
||
});
|
||
}
|
||
|
||
// ── Output checkbox helpers ───────────────────────────
|
||
function readOutputOptions() {
|
||
var saveCb = qs('#publish-save-folder');
|
||
var dlCb = qs('#publish-download-html');
|
||
return {
|
||
saveToFolder: saveCb && saveCb.checked,
|
||
download: dlCb && dlCb.checked
|
||
};
|
||
}
|
||
|
||
function syncOutputCheckboxes() {
|
||
var saveCb = qs('#publish-save-folder');
|
||
var dlCb = qs('#publish-download-html');
|
||
var hasDir = !!app.data.selectedDirHandle;
|
||
if (saveCb) { saveCb.checked = hasDir; }
|
||
if (dlCb) { dlCb.checked = !hasDir; }
|
||
}
|
||
|
||
function restoreOutputOptions(opts) {
|
||
var saveCb = qs('#publish-save-folder');
|
||
var dlCb = qs('#publish-download-html');
|
||
if (saveCb) { saveCb.checked = !!opts.saveToFolder; }
|
||
if (dlCb) { dlCb.checked = !!opts.download; }
|
||
}
|
||
|
||
function feedbackLabel(mode) {
|
||
if (mode === 'draft') { return 'Saving draft'; }
|
||
if (mode === 'signed') { return 'Signing and publishing'; }
|
||
return 'Publishing';
|
||
}
|
||
|
||
function setPublishBusy(busy) {
|
||
var btns = [qs('#publish-confirm'), qs('#publish-signed-btn'), qs('#publish-draft-btn')];
|
||
for (var i = 0; i < btns.length; i++) {
|
||
if (!btns[i]) { continue; }
|
||
btns[i].disabled = busy;
|
||
if (busy) { btns[i].classList.add('opacity-60'); }
|
||
else { btns[i].classList.remove('opacity-60'); }
|
||
}
|
||
}
|
||
|
||
// ── Core publish execution ──────────────────────────
|
||
async function executePublishFlow(mode, signingKey) {
|
||
var feedback = qs('#publish-modal-feedback');
|
||
var opts = readOutputOptions();
|
||
|
||
if (!opts.saveToFolder && !opts.download) {
|
||
throw new Error('Select at least one output option.');
|
||
}
|
||
|
||
// Prompt for directory if Save in directory is checked but none selected
|
||
if (opts.saveToFolder && !app.data.selectedDirHandle) {
|
||
try {
|
||
var dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||
app.data.selectedDirHandle = dirHandle;
|
||
if (app.modules.files && app.modules.files.updateDirectoryIndicator) {
|
||
app.modules.files.updateDirectoryIndicator();
|
||
}
|
||
} catch (dirErr) {
|
||
throw new Error('A directory is required to save. Please select a directory.');
|
||
}
|
||
}
|
||
|
||
// Sync date from modal back to form
|
||
var dateInput = qs('#publish-date-input');
|
||
var formDate = qs('#date');
|
||
if (dateInput && formDate && dateInput.value) {
|
||
formDate.value = dateInput.value;
|
||
}
|
||
|
||
var publishModule = app.modules.publish;
|
||
if (!publishModule || typeof publishModule.executePublish !== 'function') {
|
||
throw new Error('Publish module not ready');
|
||
}
|
||
|
||
showFeedback(feedback, feedbackLabel(mode) + '\u2026', '');
|
||
|
||
var result = await publishModule.executePublish({
|
||
mode: mode,
|
||
saveToFolder: opts.saveToFolder,
|
||
download: opts.download,
|
||
signingKey: signingKey || null
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// ── Publish modal ───────────────────────────────────
|
||
function initPublishModal() {
|
||
var modal = qs('#publish-modal');
|
||
if (!modal) { return; }
|
||
|
||
var feedback = qs('#publish-modal-feedback');
|
||
var confirmBtn = qs('#publish-confirm');
|
||
var signedBtn = qs('#publish-signed-btn');
|
||
var draftBtn = qs('#publish-draft-btn');
|
||
var dateInput = qs('#publish-date-input');
|
||
|
||
// Close buttons
|
||
modal.querySelectorAll('[data-modal-close]').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
if (modal.open) { modal.close(); }
|
||
});
|
||
});
|
||
|
||
// Backdrop click
|
||
modal.addEventListener('click', function (e) {
|
||
if (e.target === modal) { modal.close(); }
|
||
});
|
||
|
||
// Date warning
|
||
if (dateInput) { dateInput.addEventListener('input', syncDateWarning); }
|
||
|
||
function openModal() {
|
||
showFeedback(feedback, '', '');
|
||
syncOutputCheckboxes();
|
||
// Populate date
|
||
var formDate = qs('#date');
|
||
if (formDate && dateInput) {
|
||
var val = (formDate.value || '').trim();
|
||
dateInput.value = val ? toIsoDate(val) : getTodayIso();
|
||
}
|
||
syncDateWarning();
|
||
// Warn about missing hashes before user commits
|
||
var missingHash = 0;
|
||
(app.data.files || []).forEach(function (f) {
|
||
if (!f.sha256) { missingHash++; }
|
||
});
|
||
if (missingHash > 0) {
|
||
showFeedback(feedback, missingHash + ' file(s) have no SHA-256 hash.', 'error');
|
||
}
|
||
if (!modal.open) { modal.showModal(); }
|
||
}
|
||
|
||
function closeAndNotify(result) {
|
||
if (modal.open) { modal.close(); }
|
||
var msg = result.zipFilename ? result.filename + ' + ' + result.zipFilename : result.filename;
|
||
setTimeout(function () { showSuccessNotification(msg); }, 100);
|
||
}
|
||
|
||
function handleError(err) {
|
||
showFeedback(feedback, err && err.message ? err.message : 'Publish failed.', 'error');
|
||
if (err && err.focusEl && typeof err.focusEl.focus === 'function') {
|
||
if (modal.open) { modal.close(); }
|
||
err.focusEl.focus();
|
||
if (err.focusEl.scrollIntoView) { err.focusEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
|
||
}
|
||
}
|
||
|
||
// Publish (unsigned with digest) — primary action
|
||
if (confirmBtn) {
|
||
confirmBtn.addEventListener('click', async function () {
|
||
setPublishBusy(true);
|
||
try {
|
||
var result = await executePublishFlow('digest');
|
||
setPublishBusy(false);
|
||
closeAndNotify(result);
|
||
} catch (err) {
|
||
setPublishBusy(false);
|
||
handleError(err);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Publish Signed — opens key dialog first
|
||
if (signedBtn) {
|
||
signedBtn.addEventListener('click', async function () {
|
||
// Save user's checkbox selections before closing
|
||
var savedOpts = readOutputOptions();
|
||
if (modal.open) { modal.close(); }
|
||
|
||
try {
|
||
var jwk = await openKeyDialog();
|
||
if (!jwk) { openModal(); restoreOutputOptions(savedOpts); return; }
|
||
|
||
openModal();
|
||
restoreOutputOptions(savedOpts);
|
||
setPublishBusy(true);
|
||
var result = await executePublishFlow('signed', jwk);
|
||
setPublishBusy(false);
|
||
closeAndNotify(result);
|
||
} catch (err) {
|
||
setPublishBusy(false);
|
||
openModal();
|
||
restoreOutputOptions(savedOpts);
|
||
handleError(err);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Save Draft
|
||
if (draftBtn) {
|
||
draftBtn.addEventListener('click', async function () {
|
||
setPublishBusy(true);
|
||
try {
|
||
var result = await executePublishFlow('draft');
|
||
setPublishBusy(false);
|
||
closeAndNotify(result);
|
||
} catch (err) {
|
||
setPublishBusy(false);
|
||
handleError(err);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Listen for publish requests from the primary action button
|
||
document.addEventListener('transmittal:open-publish', function () {
|
||
openModal();
|
||
});
|
||
}
|
||
|
||
// ── Password dialog close buttons ───────────────────
|
||
function initPasswordDialog() {
|
||
var dialog = qs('#password-dialog');
|
||
if (!dialog) { return; }
|
||
dialog.querySelectorAll('[data-modal-close]').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
if (dialog.open) { dialog.close(); }
|
||
});
|
||
});
|
||
dialog.addEventListener('click', function (e) {
|
||
if (e.target === dialog) { dialog.close(); }
|
||
});
|
||
}
|
||
|
||
// ── Key dialog close buttons ────────────────────────
|
||
function initKeyDialog() {
|
||
var dialog = qs('#key-dialog');
|
||
if (!dialog) { return; }
|
||
dialog.querySelectorAll('[data-modal-close]').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
if (dialog.open) { dialog.close(); }
|
||
});
|
||
});
|
||
dialog.addEventListener('click', function (e) {
|
||
if (e.target === dialog) { dialog.close(); }
|
||
});
|
||
}
|
||
|
||
app.registerInit(function () {
|
||
initPublishModal();
|
||
initPasswordDialog();
|
||
initKeyDialog();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
const dom = app.dom;
|
||
const logosModule = app.modules.logos = {};
|
||
|
||
// Read an image file and apply it as a logo.
|
||
// imgId: 'left-logo' or 'right-logo'
|
||
async function applyLogoFile(imgId, file) {
|
||
if (!file) { return; }
|
||
var img = dom.qs('#' + imgId);
|
||
if (!img) { return; }
|
||
try {
|
||
var dataUrl = await new Promise(function (resolve, reject) {
|
||
var reader = new FileReader();
|
||
reader.onload = function () { resolve(reader.result); };
|
||
reader.onerror = function () { reject(reader.error); };
|
||
reader.readAsDataURL(file);
|
||
});
|
||
img.src = dataUrl;
|
||
// Mark logo-cell as having a logo so placeholder is hidden
|
||
var cell = img.closest('[data-drop-zone]');
|
||
if (cell) { cell.classList.add('has-logo'); }
|
||
app.state.apply();
|
||
} catch (err) {
|
||
console.error('[transmittal] logo apply failed', err);
|
||
alert('Failed to load logo: ' + (err && err.message ? err.message : err));
|
||
}
|
||
}
|
||
|
||
logosModule.applyLogoFile = applyLogoFile;
|
||
|
||
app.registerInit(function () {
|
||
// Nothing to wire here — drag-and-drop is handled by drop-zones.js.
|
||
// Keep the has-logo class in sync with existing logo src on load.
|
||
['left-logo', 'right-logo'].forEach(function (imgId) {
|
||
var img = dom.qs('#' + imgId);
|
||
if (!img) { return; }
|
||
var cell = img.closest('[data-drop-zone]');
|
||
if (cell && img.src && img.src !== window.location.href) {
|
||
cell.classList.add('has-logo');
|
||
}
|
||
});
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
// ── Targeted drop zones ─────────────────────────────────────────────────
|
||
// Shows contextual zone outlines when anything is dragged over the window.
|
||
// Emphasises zones that can accept the dragged data type.
|
||
// Hands off actual processing to existing module helpers.
|
||
|
||
var dropZonesModule = app.modules.dropZones = {};
|
||
|
||
// ── Data-transfer classification ────────────────────────────────────────
|
||
// Returns { hasImage, hasHtmlJson, hasPossibleDir }
|
||
// NOTE: MIME types ARE accessible on items during dragenter/dragover,
|
||
// but filenames are not. Directories have kind === 'file' and type === ''.
|
||
function classifyTransfer(dt) {
|
||
var hasImage = false;
|
||
var hasHtmlJson = false;
|
||
var hasPossibleDir = false;
|
||
|
||
if (!dt || !dt.items) {
|
||
return { hasImage: hasImage, hasHtmlJson: hasHtmlJson, hasPossibleDir: hasPossibleDir };
|
||
}
|
||
|
||
var items = dt.items;
|
||
for (var i = 0; i < items.length; i++) {
|
||
var item = items[i];
|
||
if (item.kind !== 'file') { continue; }
|
||
var t = (item.type || '').toLowerCase();
|
||
if (t.indexOf('image/') === 0) {
|
||
hasImage = true;
|
||
} else if (t === 'text/html' || t === 'application/json') {
|
||
hasHtmlJson = true;
|
||
} else if (t === '') {
|
||
// Could be a directory or a file with no MIME (e.g. .json on some OSes)
|
||
hasPossibleDir = true;
|
||
}
|
||
}
|
||
return { hasImage: hasImage, hasHtmlJson: hasHtmlJson, hasPossibleDir: hasPossibleDir };
|
||
}
|
||
|
||
// ── Eligibility per zone type ───────────────────────────────────────────
|
||
function zoneIsEligible(zoneType, cls) {
|
||
var inEditMode = (app.state && app.state.mode === 'edit');
|
||
switch (zoneType) {
|
||
case 'logo-left':
|
||
case 'logo-right':
|
||
return cls.hasImage && inEditMode;
|
||
case 'header':
|
||
// Accept HTML/JSON imports; also allow unknown-type files (hasPossibleDir)
|
||
// so that dragging a JSON with no MIME still lights up the header zone.
|
||
return (cls.hasHtmlJson || cls.hasPossibleDir) && inEditMode;
|
||
case 'file-table':
|
||
// Directories and HTML/JSON imports are always eligible here.
|
||
// (Verify works in both edit and published mode.)
|
||
// Pure image-only drops are not accepted here.
|
||
return cls.hasHtmlJson || cls.hasPossibleDir;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ── Zone visibility helpers ─────────────────────────────────────────────
|
||
function getAllZones() {
|
||
return document.querySelectorAll('[data-drop-zone]');
|
||
}
|
||
|
||
function showZones(cls) {
|
||
getAllZones().forEach(function (el) {
|
||
var zoneType = el.getAttribute('data-drop-zone');
|
||
var eligible = zoneIsEligible(zoneType, cls);
|
||
el.classList.add('dz-visible');
|
||
el.classList.toggle('dz-eligible', eligible);
|
||
el.classList.toggle('dz-ineligible', !eligible);
|
||
});
|
||
}
|
||
|
||
function hideZones() {
|
||
getAllZones().forEach(function (el) {
|
||
el.classList.remove('dz-visible', 'dz-eligible', 'dz-ineligible', 'dz-hover');
|
||
});
|
||
}
|
||
|
||
// ── Window-level drag tracking ──────────────────────────────────────────
|
||
// Use relatedTarget to distinguish real enter/leave from child-element noise.
|
||
// relatedTarget is null when the drag crosses the window boundary.
|
||
// Note: this approach is reliable in Chromium-based browsers. Firefox has
|
||
// a known quirk where relatedTarget may not be null at true window boundary
|
||
// in all cases; zones may not appear in Firefox. Acceptable given the app's
|
||
// primary target is Chromium.
|
||
|
||
document.addEventListener('dragenter', function (e) {
|
||
e.preventDefault();
|
||
if (e.relatedTarget === null) {
|
||
showZones(classifyTransfer(e.dataTransfer));
|
||
}
|
||
});
|
||
|
||
document.addEventListener('dragleave', function (e) {
|
||
if (e.relatedTarget === null) {
|
||
hideZones();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('dragover', function (e) { e.preventDefault(); });
|
||
|
||
document.addEventListener('drop', function (e) {
|
||
// Default handler: prevent browser navigation. Specific zones handle drops.
|
||
e.preventDefault();
|
||
hideZones();
|
||
});
|
||
|
||
// ── Per-zone drop handler: HTML / JSON import ───────────────────────────
|
||
async function handleDataFileDrop(file, sourceName) {
|
||
var dataModule = app.modules.data;
|
||
if (!dataModule) { return; }
|
||
var name = (sourceName || '').toLowerCase();
|
||
try {
|
||
var text = await file.text();
|
||
var data = null;
|
||
if (name.endsWith('.json')) {
|
||
data = JSON.parse(text);
|
||
} else {
|
||
// HTML: extract embedded JSON using the shared helper from data.js
|
||
if (dataModule.extractJsonFromHtml) {
|
||
data = dataModule.extractJsonFromHtml(text);
|
||
}
|
||
}
|
||
if (!data) {
|
||
if (dataModule.setStatus) { dataModule.setStatus('Dropped file does not contain transmittal data', 'error'); }
|
||
return;
|
||
}
|
||
await dataModule.applyLoadedData(data, sourceName || 'dropped');
|
||
} catch (err) {
|
||
console.error('[transmittal] drop-zones: file drop failed', err);
|
||
if (dataModule.setStatus) {
|
||
dataModule.setStatus('Failed to import: ' + (err && err.message ? err.message : err), 'error');
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Per-zone drop handler: directory ────────────────────────────────────
|
||
async function handleDirectoryDrop(item) {
|
||
var dataModule = app.modules.data;
|
||
var filesModule = app.modules.files;
|
||
if (!item || typeof item.getAsFileSystemHandle !== 'function') { return false; }
|
||
try {
|
||
var handle = await item.getAsFileSystemHandle();
|
||
if (!handle || handle.kind !== 'directory') { return false; }
|
||
if (typeof handle.requestPermission === 'function') {
|
||
await handle.requestPermission({ mode: 'readwrite' });
|
||
}
|
||
app.data.selectedDirHandle = handle;
|
||
if (filesModule && filesModule.updateDirectoryIndicator) {
|
||
filesModule.updateDirectoryIndicator();
|
||
}
|
||
if (dataModule && dataModule.setStatus) {
|
||
dataModule.setStatus('Directory: ' + handle.name, 'info');
|
||
}
|
||
var isPublished = !!app.state.published;
|
||
var hasFiles = app.data.files && app.data.files.length > 0;
|
||
if (isPublished || hasFiles) {
|
||
document.dispatchEvent(new CustomEvent('transmittal:verify-directory'));
|
||
} else {
|
||
document.dispatchEvent(new CustomEvent('transmittal:scan-directory'));
|
||
}
|
||
return true;
|
||
} catch (err) {
|
||
console.warn('[transmittal] drop-zones: getAsFileSystemHandle failed', err);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ── Wire a single zone element ──────────────────────────────────────────
|
||
function wireZone(el) {
|
||
var zoneType = el.getAttribute('data-drop-zone');
|
||
|
||
el.addEventListener('dragover', function (e) {
|
||
var cls = classifyTransfer(e.dataTransfer);
|
||
if (zoneIsEligible(zoneType, cls)) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
el.classList.add('dz-hover');
|
||
}
|
||
});
|
||
|
||
el.addEventListener('dragleave', function (e) {
|
||
// Only remove hover if we're actually leaving this element
|
||
// (not just moving to a child element within it)
|
||
if (!el.contains(e.relatedTarget)) {
|
||
el.classList.remove('dz-hover');
|
||
}
|
||
});
|
||
|
||
el.addEventListener('drop', async function (e) {
|
||
var cls = classifyTransfer(e.dataTransfer);
|
||
if (!zoneIsEligible(zoneType, cls)) { return; }
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
hideZones();
|
||
|
||
// ── Logo zones ──────────────────────────────────────────
|
||
if (zoneType === 'logo-left' || zoneType === 'logo-right') {
|
||
var file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
||
if (!file) { return; }
|
||
var imgId = zoneType === 'logo-left' ? 'left-logo' : 'right-logo';
|
||
if (app.modules.logos && app.modules.logos.applyLogoFile) {
|
||
await app.modules.logos.applyLogoFile(imgId, file);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// ── Header zone: HTML or JSON import only ───────────────
|
||
if (zoneType === 'header') {
|
||
var file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
||
if (!file) { return; }
|
||
var name = (file.name || '').toLowerCase();
|
||
if (!name.endsWith('.html') && !name.endsWith('.htm') && !name.endsWith('.json')) {
|
||
if (app.modules.data && app.modules.data.setStatus) {
|
||
app.modules.data.setStatus('Drop an HTML or JSON transmittal file here', 'error');
|
||
}
|
||
return;
|
||
}
|
||
await handleDataFileDrop(file, file.name);
|
||
return;
|
||
}
|
||
|
||
// ── File-table zone: directory or HTML/JSON ─────────────
|
||
if (zoneType === 'file-table') {
|
||
var ftItems = e.dataTransfer && e.dataTransfer.items;
|
||
var ftFirstItem = ftItems && ftItems.length > 0 ? ftItems[0] : null;
|
||
// Grab file synchronously before async work
|
||
var ftFile = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
||
|
||
// Try directory first
|
||
if (ftFirstItem) {
|
||
var wasDir = await handleDirectoryDrop(ftFirstItem);
|
||
if (wasDir) { return; }
|
||
}
|
||
|
||
// Fall back to file-based import
|
||
if (!ftFile) { return; }
|
||
var ftName = (ftFile.name || '').toLowerCase();
|
||
if (!ftName.endsWith('.html') && !ftName.endsWith('.htm') && !ftName.endsWith('.json')) {
|
||
if (app.modules.data && app.modules.data.setStatus) {
|
||
app.modules.data.setStatus('Drop a folder, HTML, or JSON file here', 'error');
|
||
}
|
||
return;
|
||
}
|
||
await handleDataFileDrop(ftFile, ftFile.name);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Initialization ──────────────────────────────────────────────────────
|
||
app.registerInit(function () {
|
||
document.querySelectorAll('[data-drop-zone]').forEach(wireZone);
|
||
});
|
||
|
||
dropZonesModule.classifyTransfer = classifyTransfer;
|
||
dropZonesModule.zoneIsEligible = zoneIsEligible;
|
||
|
||
})(window.transmittalApp);
|
||
|
||
(function (app) {
|
||
'use strict';
|
||
|
||
app.registerInit(function () {
|
||
let tabbing = false;
|
||
|
||
document.addEventListener('keydown', function (event) {
|
||
if (event.key === 'Tab') {
|
||
tabbing = true;
|
||
}
|
||
}, true);
|
||
|
||
document.addEventListener('mousedown', function () {
|
||
tabbing = false;
|
||
}, true);
|
||
|
||
document.addEventListener('focusin', function (event) {
|
||
if (!tabbing) {
|
||
return;
|
||
}
|
||
tabbing = false;
|
||
const target = event.target;
|
||
if (!target) {
|
||
return;
|
||
}
|
||
if (target.id === 'remarks') {
|
||
return;
|
||
}
|
||
if (target instanceof HTMLInputElement) {
|
||
try {
|
||
target.select();
|
||
} catch (_) {
|
||
// ignore selection issues
|
||
}
|
||
return;
|
||
}
|
||
if (target instanceof HTMLTextAreaElement) {
|
||
return;
|
||
}
|
||
if (target.isContentEditable) {
|
||
try {
|
||
const selection = window.getSelection && window.getSelection();
|
||
if (selection && document.createRange) {
|
||
const range = document.createRange();
|
||
range.selectNodeContents(target);
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
}
|
||
} catch (_) {
|
||
// ignore
|
||
}
|
||
}
|
||
}, true);
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
/**
|
||
* 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 (app) {
|
||
'use strict';
|
||
|
||
const filesModule = app.modules.files;
|
||
const security = app.modules.security;
|
||
const mode = app.modules.mode;
|
||
|
||
// Global handler: close parent <dialog> when any [data-modal-close] button is clicked
|
||
document.addEventListener('click', function (e) {
|
||
var btn = e.target.closest('[data-modal-close]');
|
||
if (!btn) { return; }
|
||
var dlg = btn.closest('dialog');
|
||
if (dlg && dlg.open) { dlg.close(); }
|
||
});
|
||
|
||
// ── Date field + calendar picker wiring ────────────
|
||
function toIso(str) {
|
||
if (!str || !str.trim()) { return ''; }
|
||
var t = str.trim();
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(t)) { return t; }
|
||
var d = new Date(t);
|
||
if (!isNaN(d.getTime())) {
|
||
var y = d.getFullYear();
|
||
var m = String(d.getMonth() + 1).padStart(2, '0');
|
||
var day = String(d.getDate()).padStart(2, '0');
|
||
return y + '-' + m + '-' + day;
|
||
}
|
||
return t;
|
||
}
|
||
|
||
function wireDatePicker(textId, pickerId) {
|
||
var dateText = app.dom.qs(textId);
|
||
var datePicker = app.dom.qs(pickerId);
|
||
if (!dateText || !datePicker) { return; }
|
||
|
||
dateText.addEventListener('click', function () {
|
||
var iso = toIso(dateText.value);
|
||
datePicker.value = (iso && /^\d{4}-\d{2}-\d{2}$/.test(iso)) ? iso : '';
|
||
datePicker.showPicker();
|
||
});
|
||
|
||
datePicker.addEventListener('change', function () {
|
||
if (datePicker.value) {
|
||
dateText.value = datePicker.value;
|
||
dateText.dispatchEvent(new Event('input', { bubbles: true }));
|
||
}
|
||
});
|
||
|
||
dateText.addEventListener('blur', function () {
|
||
var iso = toIso(dateText.value);
|
||
if (iso !== dateText.value) {
|
||
dateText.value = iso;
|
||
dateText.dispatchEvent(new Event('input', { bubbles: true }));
|
||
}
|
||
});
|
||
}
|
||
|
||
wireDatePicker('#date', '#date-picker-hidden');
|
||
wireDatePicker('#response-due', '#response-due-picker-hidden');
|
||
|
||
app.registerInit(async function () {
|
||
// ── Type combo dropdown wiring ───────────────────────
|
||
(function () {
|
||
var typeHidden = app.dom.qs('#type');
|
||
var typeDisplay = app.dom.qs('#type-display');
|
||
var menu = app.dom.qs('#type-menu');
|
||
if (!typeHidden || !typeDisplay || !menu) { return; }
|
||
|
||
function syncToHidden() {
|
||
var text = (typeDisplay.textContent || '').trim();
|
||
typeHidden.value = text;
|
||
typeHidden.dispatchEvent(new Event('input', { bubbles: true }));
|
||
if (app.state && app.state.apply) { app.state.apply(); }
|
||
}
|
||
|
||
function isOpen() { return !menu.classList.contains('hidden'); }
|
||
function closeMenu() { menu.classList.add('hidden'); }
|
||
|
||
typeDisplay.addEventListener('input', syncToHidden);
|
||
|
||
typeDisplay.addEventListener('blur', syncToHidden);
|
||
|
||
typeDisplay.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
typeDisplay.blur();
|
||
}
|
||
});
|
||
|
||
typeDisplay.addEventListener('paste', function (e) {
|
||
e.preventDefault();
|
||
var text = (e.clipboardData || window.clipboardData).getData('text/plain');
|
||
document.execCommand('insertText', false, text.replace(/\n/g, ' '));
|
||
});
|
||
|
||
typeDisplay.addEventListener('click', function () {
|
||
if (app.state.mode !== 'edit') { return; }
|
||
if (isOpen()) { closeMenu(); } else { menu.classList.remove('hidden'); }
|
||
});
|
||
|
||
menu.addEventListener('click', function (e) {
|
||
var opt = e.target.closest('[data-value]');
|
||
if (!opt) { return; }
|
||
var val = opt.getAttribute('data-value');
|
||
typeDisplay.textContent = val;
|
||
typeHidden.value = val;
|
||
typeHidden.dispatchEvent(new Event('input', { bubbles: true }));
|
||
closeMenu();
|
||
if (app.state && app.state.apply) { app.state.apply(); }
|
||
});
|
||
|
||
document.addEventListener('click', function (e) {
|
||
if (isOpen() && !menu.contains(e.target) && e.target !== typeDisplay) {
|
||
closeMenu();
|
||
}
|
||
});
|
||
})();
|
||
filesModule.loadFromJson();
|
||
app._initialized = true;
|
||
app.state.updateHiddenFields();
|
||
app.state.apply();
|
||
await security.verifySignatureIfPresent();
|
||
mode.refresh();
|
||
});
|
||
|
||
app.ready(function () {
|
||
app.start();
|
||
});
|
||
})(window.transmittalApp);
|
||
|
||
</script>
|
||
<script id="help-markdown" type="application/markdown">
|
||
# Transmittal Creator
|
||
|
||
[← Back to ZDDC](../README.md)
|
||
|
||
Create professional document transmittals that are impossible to forge or tamper with. Each transmittal is a self-contained HTML file with built-in integrity checking and optional digital signatures. Email it, archive it, trust it.
|
||
|
||
**[🔗 Open Transmittal Creator](dist/transmittal.html)** - Click to use online, or right-click → "Save Link As" to keep your own copy.
|
||
|
||
## The "Record Player with the Record" Concept
|
||
|
||
This tool embodies true data portability - each transmittal contains both the data AND the viewer. Recipients don't need special software, accounts, or training. They just open the HTML file in any browser to see a professional transmittal with full verification capabilities. In 20 years, it will still work exactly the same.
|
||
|
||
## What Makes Transmittals Special?
|
||
|
||
✅ **Tamper-Proof**
|
||
Every file gets a SHA-256 fingerprint. If even one character changes, it's detected instantly.
|
||
|
||
🔒 **Digitally Signable**
|
||
Optionally sign with cryptographic keys. Proves WHO sent it and WHEN, forever.
|
||
|
||
📧 **Self-Contained**
|
||
The entire transmittal is one HTML file. Email it, archive it, open it anywhere.
|
||
|
||
📊 **Machine-Readable**
|
||
Embedded JSON data means your systems can parse and process automatically.
|
||
|
||
🔍 **Independently Verifiable**
|
||
Anyone can verify signatures and checksums - but verification MUST be done using a trusted copy of this tool, not the transmittal itself. See [Verification Security](#verification-security) below.
|
||
|
||
## Quick Start
|
||
|
||
### Workflow A: Paste file list first (typical for new transmittals)
|
||
|
||
1. **Fill in the header** — Tracking Number, Date, To, From, Subject, Purpose, Remarks
|
||
2. **Create Folder** — Menu → Create Folder → select staging directory → folder is created and selected
|
||
3. **Paste file list** — Copy 3-5 adjacent columns from Excel (Tracking, Title, Revision, [Status], [Extension]) → Menu → Paste New Rows
|
||
4. **Drop files onto rows** — Drag individual files from your OS onto matching rows to copy with ZDDC names and compute hashes
|
||
5. **Save Draft** — Menu → Save Draft → save into the created folder
|
||
6. **Publish** — When ready, Menu → Publish → choose Unsigned or Signed
|
||
|
||
### Workflow B: Scan an existing directory
|
||
|
||
1. **Click "Select Directory"** — Choose folder with files to transmit
|
||
2. **Files auto-populate** — With tracking numbers, revisions, and checksums parsed from ZDDC filenames
|
||
3. **Fill in the form** — To, From, Subject, and any remarks
|
||
4. **Click Publish** — Choose Draft, Unsigned, or Signed
|
||
5. **Send the HTML file** — Email it or save to your archive
|
||
|
||
## Verification Security
|
||
|
||
⚠️ **CRITICAL: Self-Verification is NOT Secure**
|
||
|
||
When you open a transmittal, it may display "✓ Signature Valid" - but **this display can be faked**. A malicious actor could create a transmittal that shows valid signatures while containing altered data.
|
||
|
||
### How to Verify Securely
|
||
|
||
**For Document Controllers / Official Verification:**
|
||
|
||
1. **Use a trusted tool instance** - Download the official transmittal tool from a trusted source (e.g., your organization's approved version or https://zddc.varasys.io/releases/transmittal_stable.html)
|
||
2. **Export JSON from the transmittal** - Open the transmittal → Click "Download Data"
|
||
3. **Import JSON into trusted tool** - Open your trusted tool → Click "Load JSON" → Paste the exported data
|
||
4. **Verify file hashes** - Click "Select Directory" and point to the actual files
|
||
5. **Check the verification display** - Only trust the verification shown in YOUR trusted tool, not the transmittal itself
|
||
|
||
**Why This Works:**
|
||
- The trusted tool's code is verified (by you or your organization)
|
||
- The JSON data is extracted and re-verified independently
|
||
- File hashes are computed fresh from the actual files
|
||
- The verification logic cannot be tampered with
|
||
|
||
**For Casual Review:**
|
||
- The self-verification display is fine for informal checks
|
||
- Useful for catching accidental modifications
|
||
- NOT sufficient for legal/contractual verification
|
||
|
||
### Verification Workflow
|
||
|
||
```
|
||
Received Transmittal (untrusted)
|
||
↓
|
||
Export JSON Data
|
||
↓
|
||
Trusted Tool Instance (verified by your organization)
|
||
↓
|
||
Import JSON → Verify Signatures → Verify File Hashes
|
||
↓
|
||
Official Verification Result
|
||
```
|
||
|
||
**Best Practice:** Organizations should maintain a verified copy of the transmittal tool at a known URL or network location for all document controllers to use for official verification.
|
||
|
||
## Technical Architecture
|
||
|
||
### Data-First Design
|
||
- **Single source of truth**: `<script id="transmittal-data" type="application/json">` block
|
||
- All UI state derived from this embedded JSON
|
||
- No duplicate data storage in the HTML
|
||
|
||
### Build System
|
||
- **Build script** that:
|
||
- Concatenates modular CSS and JavaScript files
|
||
- Inlines everything into template.html
|
||
- Embeds this README as help documentation
|
||
- Outputs single `dist/transmittal.html` file
|
||
|
||
### Security Model
|
||
- All cryptographic operations happen **client-side** (Web Crypto API)
|
||
- Private keys never stored or uploaded
|
||
- **Self-verification is informational only** - a malicious transmittal can fake verification displays
|
||
- **Secure verification requires external validation** - see [Verification Security](#verification-security)
|
||
- Organizations should maintain a trusted tool instance for official verification
|
||
|
||
## Key Features
|
||
|
||
✅ **Portable**: Single HTML file works offline
|
||
✅ **Cryptographic**: SHA-256 hashing + ECDSA signatures
|
||
✅ **Parseable**: Embedded JSON for machine automation
|
||
✅ **Human-readable**: Clean UI with print optimization
|
||
✅ **Filterable**: Boolean logic filtering on all columns
|
||
✅ **Verifiable**: Independent signature verification
|
||
✅ **Standards-based**: ZDDC filename convention compliance
|
||
|
||
## Use Case
|
||
|
||
This tool is designed for **engineering/construction document transmittals** where:
|
||
- Multiple files need to be transmitted with metadata
|
||
- Cryptographic integrity verification is required
|
||
- Non-repudiation via digital signatures is valuable
|
||
- Recipients need human-readable + machine-parseable format
|
||
- Offline/portable distribution is essential
|
||
|
||
The project achieves a **self-contained, verifiable document package** that bridges human workflow needs with automation requirements.
|
||
|
||
## How It Works
|
||
|
||
The transmittal operates on a simple principle: all data lives in a single JSON block inside the HTML. When you save or publish, it updates this JSON and downloads a new HTML file. This ensures:
|
||
|
||
- Complete data portability
|
||
- No hidden state or complexity
|
||
- Easy integration with other systems
|
||
- Full transparency of what's stored
|
||
|
||
|
||
## Field reference (canonical payload)
|
||
|
||
Form values map to a stable canonical payload used for hashing/signing and JSON export. This payload lives inside the `<script type="application/json" id="transmittal-data">` block.
|
||
|
||
**JSON Schema**: See [`transmittal.schema.json`](./transmittal/transmittal.schema.json) for the complete, machine-readable schema definition.
|
||
|
||
### Key Fields
|
||
|
||
**Payload** (core document data):
|
||
- `version` — Payload schema version (currently 1)
|
||
- `type` — Document type: "Transmittal", "Submittal", or custom text (e.g., "Contract Attachment 1")
|
||
- `title` — Optional descriptive title
|
||
- `client` — The Owner Name
|
||
- `project` — The Project Name
|
||
- `projectNumber` — The Project Number
|
||
- `date` — Date in YYYY-MM-DD format (canonicalized from friendly text)
|
||
- `trackingNumber` — Must not contain spaces or underscores
|
||
- `from` — Who issued the document (hidden when type is not Transmittal or Submittal)
|
||
- `to` — Recipient (hidden when type is not Transmittal or Submittal)
|
||
- `purpose` — Purpose of document (hidden when type is not Transmittal or Submittal)
|
||
- `responseDue` — Response deadline (shown only when type is Submittal)
|
||
- `subject` — Subject line
|
||
- `remarks` — Free-form Markdown content
|
||
- `files[]` — Array of file objects, each containing:
|
||
- `trackingNumber`, `revision`, `status`, `title` (parsed from ZDDC filename)
|
||
- `path` — Directory path relative to transmittal root (empty string for root-level files)
|
||
- `filename` — Filename only (without path)
|
||
- `sha256` — SHA-256 hash of file contents (hex)
|
||
- `fileSize` — File size in bytes
|
||
|
||
**Envelope** (cryptographic metadata):
|
||
- `version` — Envelope schema version (currently 1)
|
||
- `digestAlgorithm` — Always "SHA-256"
|
||
- `digest` — SHA-256 hash of canonical payload string (hex)
|
||
- `digestedAt` — ISO 8601 timestamp when digest was computed
|
||
- `signatureAlgorithm` — "ECDSA-P256-SHA256"
|
||
- `signatures[]` — Array of signature objects, each containing:
|
||
- `signature` — ECDSA signature over canonical envelope (base64)
|
||
- `signedAt` — ISO 8601 timestamp when this signature was created
|
||
- `publicKeyJwk` — EC P-256 public key (JWK) for verification
|
||
|
||
**Security Model**: Signatures sign the envelope (which contains the digest), not the payload directly. This prevents tampering with the digest field while maintaining a two-layer verification:
|
||
1. **Digest verification**: Confirms payload hasn't been modified (fast, no crypto)
|
||
2. **Signature verification**: Confirms envelope (including digest) is authentic (cryptographic proof)
|
||
|
||
**Presentation** (optional display assets - not signed):
|
||
- `leftLogo` — Left logo as data URL
|
||
- `rightLogo` — Right logo as data URL
|
||
- `theme` — Visual theme identifier
|
||
- `customCss` — Optional custom CSS for styling
|
||
|
||
|
||
## Files table
|
||
|
||
**Columns**: Tracking Number, Title, Revision, Status, EXT, Size, SHA256
|
||
|
||
**Column visibility**:
|
||
- **Default view**: Shows Tracking Number, Title, Revision, Status (essential columns)
|
||
- **Show Details**: Toggle to reveal EXT, Size, SHA256 (technical details)
|
||
- **Fullscreen**: Expand table to full browser window for better viewing of large file lists
|
||
|
||
**Pasting file lists**:
|
||
- Copy 3-5 **adjacent** tab-separated columns from Excel or any spreadsheet
|
||
- Expected column order: Tracking, Title, Revision, [Status], [Extension]
|
||
- 3-column paste: third column can be combined "A (IFR)" → splits into Revision "A" and Status "IFR"
|
||
- Pastes with more than 5 columns are rejected with an error — condition your data into adjacent columns first
|
||
- **Paste New Rows**: Replaces entire file list (confirms first)
|
||
- **Paste Append Rows**: Adds to existing list, updates matching rows by tracking+revision key
|
||
|
||
**Drop file onto row**:
|
||
- Drag a file from your OS onto any table row in edit mode
|
||
- The file is copied to the selected directory with the proper ZDDC filename
|
||
- SHA-256 hash is computed and stored
|
||
- If no directory is selected, you will be prompted to pick one
|
||
|
||
**File scanning**:
|
||
- "Select Directory" / "Scan Directory" scans recursively using the File System Access API, hashing files locally and parsing names
|
||
- Filename parser expects: `trackingNumber_revision (status) - title.ext`
|
||
- Example: `A101-203_revA (Issued) - Site Plan.pdf`
|
||
- If a filename doesn't match, it still appears; parsed fields default to empty and `title` becomes the base name
|
||
- Imports are merged on key `(trackingNumber|revision)`; newer scans update/replace matching rows
|
||
- For known viewable types (`pdf,png,jpg,jpeg,gif,svg,webp,txt,html,htm,md`), links open in a new tab; others download
|
||
|
||
|
||
## Filtering
|
||
|
||
Each column has a filter input. Supported expression syntax:
|
||
|
||
| Expression | Meaning |
|
||
|---|---|
|
||
| `term` | Contains "term" (case-insensitive) |
|
||
| `!term` | Does not contain "term" |
|
||
| `^term` | Starts with "term" |
|
||
| `term$` | Ends with "term" |
|
||
| `a b` | Matches both (AND) |
|
||
| `a \| b` | Matches either (OR) |
|
||
| `^IFA \| ^IFB` | Starts with IFA or IFB |
|
||
| `pdf !draft` | Contains "pdf" and not "draft" |
|
||
| `!^~` | Does not start with ~ (excludes drafts) |
|
||
| `el.*spc` | Regex: "el" followed by "spc" |
|
||
| `[ei]fa` | Regex character class: "efa" or "ifa" |
|
||
|
||
Examples:
|
||
|
||
- Only PDFs: in EXT filter, type `pdf$`.
|
||
- Exclude drafts: in Revision, type `!^~`.
|
||
- Status IFA or IFB: type `^IFA | ^IFB`.
|
||
- Title contains both "floor" and "plan": type `floor plan`.
|
||
- Tracking contains "el" then "spc": type `el.*spc`.
|
||
|
||
|
||
## Form validation
|
||
|
||
The transmittal provides real-time validation to catch errors early:
|
||
|
||
**Inline validation** (as you type):
|
||
- **Tracking Number**: Highlights if contains spaces or underscores (invalid characters)
|
||
- **Date fields**: Auto-formats to YYYY-MM-DD, shows format hint on focus
|
||
- **File table**: Shows parsing warnings for filenames that don't match ZDDC convention
|
||
|
||
**Publish-time validation**:
|
||
- All required fields must be filled
|
||
- Tracking numbers must not contain spaces or underscores
|
||
- File revisions must not contain spaces
|
||
- At least one file must be included
|
||
|
||
**Smart defaults**:
|
||
- **Date**: Pre-filled with today's date in YYYY-MM-DD format
|
||
|
||
|
||
## Signature verification
|
||
|
||
⚠️ **WARNING: For official verification, see [Verification Security](#verification-security) above. The verification display shown here can be faked by a malicious transmittal.**
|
||
|
||
When viewing a published transmittal, signature status is displayed above the files table:
|
||
|
||
**For each signature**:
|
||
- ✓ **Valid** — Signature verified successfully (in this document's context)
|
||
- ⚠ **Warning** — Signature valid but transmittal has been edited
|
||
- ✗ **Invalid** — Signature verification failed
|
||
|
||
**Verification Actions**:
|
||
- **Verify Externally** — Opens a trusted tool instance for independent verification
|
||
- Copies JSON data to clipboard
|
||
- Opens https://zddc.varasys.io/releases/transmittal_stable.html in validation mode
|
||
- Paste JSON and select directory to verify file hashes independently
|
||
- Only trust verification results from the trusted tool, not this document
|
||
|
||
**Signature details** (expandable):
|
||
- Public key fingerprint (click to copy)
|
||
- Signature timestamp (from `signedAt` field)
|
||
- Signature index (e.g., "Signature 1 of 3")
|
||
|
||
**Other Actions**:
|
||
- **Delete signature** — Remove invalid or outdated signatures
|
||
- **Add signature** — Co-sign the document with your key
|
||
- Opens signing dialog (select existing key or generate new key)
|
||
- New signature appended to `signatures[]` array with current timestamp
|
||
- Downloads updated document (cannot save in place)
|
||
|
||
|
||
## Saving and loading transmittals
|
||
|
||
**Save Draft** (Menu → Save Draft):
|
||
- Saves current state as an HTML file via the system file picker
|
||
- All header info, file list, and remarks are preserved
|
||
- Open the saved HTML in a browser to resume editing
|
||
- Drafts have no digest or signature
|
||
|
||
**Create Folder** (Menu → Create Folder):
|
||
- Builds a folder name from the form: `YYYY-MM-DD_TRACKING (PURPOSE) - SUBJECT`
|
||
- Prompts you to select a staging directory, then creates the subfolder
|
||
- The new folder becomes the selected directory for file drops and Save Draft
|
||
- Requires at least a tracking number
|
||
|
||
**Remove Files** (Menu → Remove Files):
|
||
- Clears the file list while keeping all header info and remarks
|
||
- Useful when reusing a transmittal template with a new set of files
|
||
|
||
**Save as JSON**:
|
||
- Click **Download Data** to export the current transmittal as JSON
|
||
- This file can be reloaded later as either:
|
||
- A finished transmittal (view/verify)
|
||
- A starting point for a new transmittal (edit/modify)
|
||
|
||
**Load from JSON**:
|
||
- Click **Load JSON** to import a previously saved transmittal
|
||
- All form fields, files, and presentation assets (logos) are restored
|
||
|
||
**Import HTML** (Menu → Import HTML):
|
||
- Load a previously saved draft or published transmittal HTML file
|
||
- Extracts the embedded JSON and restores all fields
|
||
- You can also drag-and-drop an HTML or JSON file directly onto the page — see **Drag-and-Drop Zones** below
|
||
|
||
## Drag-and-Drop Zones
|
||
|
||
Whenever you drag any file or folder over the browser window, labelled drop zones appear on the transmittal to show you exactly where data can land:
|
||
|
||
| Zone | Location | Accepts | Notes |
|
||
|------|----------|---------|-------|
|
||
| **Sender logo** | Top-left logo area | Image files (png, jpg, svg, …) | Edit mode only |
|
||
| **Receiver logo** | Top-right logo area | Image files | Edit mode only |
|
||
| **Header** | Header info block | Transmittal HTML or JSON file | Imports all header fields and file list; edit mode only |
|
||
| **File table** | Document table area | Folder (scan or verify) · Transmittal HTML or JSON | Works in both edit and published mode |
|
||
|
||
**Visual feedback during drag:**
|
||
- Zones that can accept the dragged data are highlighted with a blue dashed outline and a descriptive label.
|
||
- Zones that cannot accept the data are shown with a grey outline and dimmed.
|
||
- When you hover over an eligible zone, it brightens further to confirm where the drop will land.
|
||
|
||
**Logos and presentation assets**:
|
||
- Logos ARE included in JSON exports (in the optional `presentation` object)
|
||
- Logos are NOT part of the signed `payload` (separate top-level object)
|
||
- When you drag-drop logos, they're converted to data URLs
|
||
- The `presentation` object includes:
|
||
- `leftLogo` and `rightLogo` (data URLs)
|
||
- `theme` (visual theme identifier)
|
||
- `customCss` (optional custom styling)
|
||
- This keeps the cryptographic signature focused on document data, not visual styling
|
||
|
||
|
||
## JSON export format
|
||
|
||
Download Data exports the exact contents of the embedded JSON block.
|
||
|
||
**Format**: See [`transmittal.schema.json`](./transmittal/transmittal.schema.json) for the complete structure.
|
||
|
||
**Key Points**:
|
||
- The `payload` contains all transmittal data (metadata + files)
|
||
- The `envelope` contains cryptographic metadata (digest, timestamp, signatures)
|
||
- The `presentation` (optional) contains display assets (logos, styling) - **not signed**
|
||
- The digest is computed over the canonical JSON string of `payload` only (sorted keys, stable formatting)
|
||
- Each signature in the `signatures[]` array signs the same canonical payload string
|
||
- If unsigned, the `signatures` array is empty `[]`
|
||
- Presentation assets travel with the JSON but are excluded from cryptographic operations
|
||
|
||
|
||
## Browser compatibility and printing
|
||
|
||
- Directory selection requires the File System Access API (Chromium‑based browsers). On unsupported browsers, file scanning will not work.
|
||
- Print styles are optimized for US Letter (8.5in×11in). Table header "sticky" behavior is disabled in print.
|
||
|
||
|
||
## Publishing & Signing
|
||
|
||
When you click **Publish**, a modal opens with these options:
|
||
|
||
1. **Save Draft** — Saves HTML with current data (no digest, no signature)
|
||
- Also available directly from Menu → Save Draft (without opening the publish modal)
|
||
- Use for work-in-progress transmittals
|
||
- Can be reopened and edited later
|
||
|
||
2. **Publish Unsigned** — Creates transmittal with digest only (no signature)
|
||
- Provides integrity verification via SHA-256 digest
|
||
- Proves content hasn't been modified
|
||
- No identity authentication
|
||
|
||
3. **Publish Signed** — Creates transmittal with digest + signature(s)
|
||
- Choose one of:
|
||
- **Sign with Existing Key** — Select your saved private key JWK file
|
||
- **Sign with New Key** — Generate new EC P-256 key pair (downloads private key for you to save)
|
||
- Provides both integrity and authentication
|
||
- Supports multiple signatures (co-signing)
|
||
|
||
**Security notes**:
|
||
- All cryptographic operations happen locally in the browser (Web Crypto API)
|
||
- Private keys are never stored or uploaded
|
||
- Only public keys are embedded in the published transmittal for verification
|
||
- If you generate a new key, **save the downloaded private key file** — you'll need it to sign future transmittals with the same identity
|
||
- Editing a published transmittal invalidates existing signatures (they can be deleted)
|
||
|
||
## Workflow
|
||
|
||
The transmittal operates in different modes that control what actions are available:
|
||
|
||
### Edit Mode (Default)
|
||
|
||
**When active**: New transmittals or when explicitly toggled to Edit mode.
|
||
|
||
**Document type selector**:
|
||
|
||
The `type` field is a text input with autocomplete suggestions. Type to select or enter custom text.
|
||
|
||
| Field | Transmittal | Submittal | File List (custom text) |
|
||
|-------|-------------|-----------|-------------------------|
|
||
| From | ✓ | ✓ | — |
|
||
| To | ✓ | ✓ | — |
|
||
| Purpose | ✓ | ✓ | — |
|
||
| Response Due | — | ✓ | — |
|
||
| Subject | ✓ | ✓ | ✓ |
|
||
| Remarks | ✓ | ✓ | ✓ |
|
||
|
||
**Usage**:
|
||
- **Transmittal** — General document transmittals (no response required)
|
||
- **Submittal** — Submittals requiring response (shop drawings, product data, samples)
|
||
- **Custom text** — Type any text (e.g., "Contract Attachment 1", "Document Register") for file lists without workflow fields
|
||
|
||
**Primary actions** (toolbar buttons):
|
||
- **Select Directory** / **Scan Directory** — Scan filesystem to populate files table
|
||
- **Verify Directory** — Re-hash files and compare against stored hashes
|
||
- **Publish** — Opens publish modal (Draft / Unsigned / Signed)
|
||
|
||
**Menu actions** (☰ dropdown):
|
||
- **Scan Directory** / **Verify Directory** — Same as toolbar buttons
|
||
- **Publish** / **Save Draft** / **Create Folder** — Publishing and folder management
|
||
- **Paste New Rows** / **Paste Append Rows** — Paste file lists from clipboard
|
||
- **Copy Table** — Copy file table to clipboard as tab-separated text
|
||
- **Remove Files** — Clear file list, keep header and remarks
|
||
- **Import HTML** — Load a saved draft or published transmittal
|
||
- **Copy JSON** / **Paste JSON** — JSON clipboard operations
|
||
- **Reset** — Clear all form data and files (start over)
|
||
- **Create Index** — Generate archive redirect index in selected directory
|
||
|
||
**Header bar actions** (always available):
|
||
- **View/Edit toggle** — Switch between edit and read-only mode
|
||
- **Load JSON** — Import a previously saved document
|
||
- **Download Data** — Export JSON for signature verification or system integration
|
||
- **Help** — View this documentation
|
||
|
||
**UI behavior**: All form fields and table cells are editable. Filter inputs remain active for searching.
|
||
|
||
### View Mode
|
||
|
||
**When active**: Toggle from Edit mode or when opening a published transmittal.
|
||
|
||
**Available actions**:
|
||
- **View/Edit** — Switch to Edit mode (allows modifications)
|
||
- **Download Data** — Export JSON for signature verification or system integration
|
||
- **Help** — View this documentation
|
||
|
||
**UI behavior**: Form fields and table cells are read-only. Filter inputs remain active for searching. All editing controls hidden (Select Directory, Publish, Reset).
|
||
|
||
### Published State
|
||
|
||
**When active**: Automatically detected when transmittal contains a valid digest.
|
||
|
||
**Additional display**: Signature verification status shown above files table with public key fingerprint(s).
|
||
|
||
**Behavior**: Defaults to View mode on load. Can toggle to Edit mode, but signature status indicates the transmittal has been published.
|
||
|
||
### Mode Transitions
|
||
|
||
```
|
||
New Transmittal → Edit Mode
|
||
↓
|
||
Save Draft → Edit Mode (unsigned)
|
||
↓
|
||
Sign & Publish → Published State (View Mode)
|
||
↓
|
||
Toggle Edit → Published State (Edit Mode)
|
||
```
|
||
|
||
|
||
---
|
||
|
||
Authoring notes:
|
||
|
||
- UI copy uses “Tracking Number” consistently. The underlying key remains `trackingNumber`.
|
||
- This README documents only the transmittal folder assets and does not change any code outside this directory.
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|