cleanup
Hunter
Monday, February 2nd, 2026 at 3:15 PM
5 const previewPane = document.querySelector('.preview-pane');
6 const storageKey = 'zine-editor-content';
7
8 +// Insert content right after <head> tag in HTML string
9 +function insertAfterHead(html, content) {
10 + const headMatch = html.match(/<head[^>]*>/i);
11 + if (headMatch) {
12 + const insertPos = html.indexOf(headMatch[0]) + headMatch[0].length;
13 + return html.slice(0, insertPos) + content + html.slice(insertPos);
14 + }
15 + return html;
16 +}
17 +
18 +// Insert content right before </head> tag in HTML string
19 +function insertBeforeHeadClose(html, content) {
20 + const headEndMatch = html.match(/<\/head>/i);
21 + if (headEndMatch) {
22 + const insertPos = html.indexOf(headEndMatch[0]);
23 + return html.slice(0, insertPos) + content + html.slice(insertPos);
24 + }
25 + return html;
26 +}
27 +
28 // Extract title and favicon from user's HTML
29 function extractTitleAndFavicon(htmlCode) {
30 const parser = new DOMParser();
108 return startPos + ZINE_BOILERPLATE.indexOf('<div class="page" id="front-cover">') + 39;
109 }
110
111 +// Zine page dimensions in inches
112 +const PAGE_WIDTH_IN = 2.75;
113 +const PAGE_HEIGHT_IN = 4.25;
114 +const PAGE_PADDING_IN = 0.2;
115 +
116 +// All page IDs for selectors
117 +const PAGE_IDS = ['front-cover', 'page1', 'page2', 'page3', 'page4', 'page5', 'page6', 'back-cover'];
118 +const ALL_PAGES_SELECTOR = PAGE_IDS.map(id => `#${id}`).join(', ');
119 +const RIGHT_PAGES_SELECTOR = '#front-cover, #page2, #page4, #page6';
120 +const LEFT_PAGES_SELECTOR = '#back-cover, #page1, #page3, #page5';
121 +
122 +// Create an empty placeholder div for single-page spreads
123 +function createEmptyPlaceholder(doc) {
124 + const empty = doc.createElement('div');
125 + empty.className = 'zine-empty';
126 + return empty;
127 +}
128 +
129 +// Spread definitions
130 +const SPREADS = [
131 + { left: null, right: 'front-cover', label: 'Front Cover' },
132 + { left: 'page1', right: 'page2', label: 'Pages 1-2' },
133 + { left: 'page3', right: 'page4', label: 'Pages 3-4' },
134 + { left: 'page5', right: 'page6', label: 'Pages 5-6' },
135 + { left: 'back-cover', right: null, label: 'Back Cover' }
136 +];
137 +
138 +let currentSpread = 0;
139 +let isFullscreen = false;
140 +let showLineNumbers = false;
141 +let enableLineWrapping = false;
142 +let lineNumbersCompartment;
143 +let lineWrappingCompartment;
144 +let isEditorFocused = false;
145 +let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
146 +
147 // Shared print CSS for zine layout (used by both editor preview and exported viewer)
148 const ZINE_PRINT_CSS = `
149 @page { size: 8.5in 11in portrait; margin: 0; }
150 html, body { width: 8.5in; height: 11in; }
151 body {
152 display: grid !important;
- grid-template-columns: repeat(4, 2.75in);
- grid-template-rows: repeat(2, 4.25in);
153 + grid-template-columns: repeat(4, ${PAGE_WIDTH_IN}in);
154 + grid-template-rows: repeat(2, ${PAGE_HEIGHT_IN}in);
155 transform: rotate(90deg);
156 transform-origin: center center;
157 position: absolute;
- top: calc(50% - 4.25in);
158 + top: calc(50% - ${PAGE_HEIGHT_IN}in);
159 left: calc(50% - 5.5in);
160 width: 11in;
161 height: 8.5in;
162 }
- #front-cover, #page1, #page2, #page3, #page4, #page5, #page6, #back-cover {
163 + ${ALL_PAGES_SELECTOR} {
164 display: block !important;
- width: 2.75in !important;
- height: 4.25in !important;
- padding: 0.2in;
165 + width: ${PAGE_WIDTH_IN}in !important;
166 + height: ${PAGE_HEIGHT_IN}in !important;
167 + padding: ${PAGE_PADDING_IN}in;
168 background: white;
169 overflow: hidden;
170 overflow-wrap: break-word;
185 a { color: black; }
186 `;
187
-// Spread definitions
-const SPREADS = [
- { left: null, right: 'front-cover', label: 'Front Cover' },
- { left: 'page1', right: 'page2', label: 'Pages 1-2' },
- { left: 'page3', right: 'page4', label: 'Pages 3-4' },
- { left: 'page5', right: 'page6', label: 'Pages 5-6' },
- { left: 'back-cover', right: null, label: 'Back Cover' }
-];
-
-let currentSpread = 0;
-let isFullscreen = false;
-let showLineNumbers = false;
-let enableLineWrapping = false;
-let lineNumbersCompartment;
-let lineWrappingCompartment;
-let isEditorFocused = false;
-let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
-
-// Zine page dimensions in inches
-const PAGE_WIDTH_IN = 2.75;
-const PAGE_HEIGHT_IN = 4.25;
-
188 // Shared CSS for zine pages (used by both editor preview and exported viewer)
189 const ZINE_PAGE_CSS = `
- #front-cover, #page1, #page2, #page3, #page4, #page5, #page6, #back-cover {
190 + ${ALL_PAGES_SELECTOR} {
191 display: none;
192 width: ${PAGE_WIDTH_IN}in;
193 height: ${PAGE_HEIGHT_IN}in;
194 flex-shrink: 0;
- padding: 0.2in;
195 + padding: ${PAGE_PADDING_IN}in;
196 overflow: hidden;
197 overflow-wrap: break-word;
198 }
203 display: flex;
204 transform-origin: center center;
205 }
- #front-cover, #page2, #page4, #page6 {
206 + ${RIGHT_PAGES_SELECTOR} {
207 box-shadow: inset 4px 0 1.3px -3px rgba(0, 0, 0, 0.09), inset 8px 0 6px -6px rgba(0, 0, 0, 0.15);
208 }
- #back-cover, #page1, #page3, #page5 {
209 + ${LEFT_PAGES_SELECTOR} {
210 box-shadow: inset -4px 0 1.5px -3px rgba(0, 0, 0, 0.09), inset -8px 0 6px -6px rgba(0, 0, 0, 0.15);
211 }
212 .zine-empty {
256 position: relative;
257 }
258
- #front-cover, #page1, #page2, #page3, #page4, #page5, #page6, #back-cover {
259 + ${ALL_PAGES_SELECTOR} {
260 display: none !important;
261 width: ${PAGE_WIDTH_IN}in !important;
262 height: ${PAGE_HEIGHT_IN}in !important;
362
363 // Inject spread CSS right after <head> so user styles come last and take precedence
364 const spreadCSS = generateSpreadCSS(currentSpread);
- let processedCode = code;
- const headStartMatch = processedCode.match(/<head[^>]*>/i);
- if (headStartMatch) {
- const insertPos = processedCode.indexOf(headStartMatch[0]) + headStartMatch[0].length;
- processedCode = processedCode.slice(0, insertPos) +
- `<style id="zine-editor-spread-css">${spreadCSS}</style>` +
- processedCode.slice(insertPos);
- }
365 + const processedCode = insertAfterHead(code, `<style id="zine-editor-spread-css">${spreadCSS}</style>`);
366
367 preview.srcdoc = processedCode || '<!DOCTYPE html><html><head></head><body></body></html>';
368
399
400 // Add empty placeholder before right page if no left page
401 if (!spread.left && spread.right) {
- const empty = doc.createElement('div');
- empty.className = 'zine-empty';
- container.appendChild(empty);
402 + container.appendChild(createEmptyPlaceholder(doc));
403 }
404
405 // Move visible pages into container
411
412 // Add empty placeholder after left page if no right page
413 if (spread.left && !spread.right) {
- const empty = doc.createElement('div');
- empty.className = 'zine-empty';
- container.appendChild(empty);
414 + container.appendChild(createEmptyPlaceholder(doc));
415 }
416
417 doc.body.appendChild(container);
549 });
550 }
551
552 +// Shared navigation button CSS (used by both editor spread-nav and exported viewer zine-nav)
553 +const NAV_BUTTON_CSS = `
554 + display: flex;
555 + align-items: center;
556 + justify-content: center;
557 + gap: 16px;
558 + padding: 12px 20px;
559 +`;
560 +
561 +const NAV_BUTTON_STYLES = `
562 + background: rgba(255,255,255,0.15);
563 + color: white;
564 + border: none;
565 + border-radius: 50%;
566 + width: 36px;
567 + height: 36px;
568 + cursor: pointer;
569 + font-size: 18px;
570 + display: flex;
571 + align-items: center;
572 + justify-content: center;
573 +`;
574 +
575 // Generate standalone viewer CSS and JS (matches preview pane rendering)
576 function generateViewerCode() {
577 const css = `
591 body > *:not(.zine-spread-container):not(.zine-nav) {
592 display: none !important;
593 }
- #front-cover, #page1, #page2, #page3, #page4, #page5, #page6, #back-cover {
- display: none;
- width: ${PAGE_WIDTH_IN}in !important;
- height: ${PAGE_HEIGHT_IN}in !important;
- flex-shrink: 0;
- padding: 0.2in;
- overflow: hidden;
- overflow-wrap: break-word;
- }
- .page {
- background: white;
- }
594 + ${ZINE_PAGE_CSS}
595 .zine-spread-container {
596 display: flex !important;
- transform-origin: center center;
597 position: relative;
598 }
- #front-cover, #page2, #page4, #page6 {
- box-shadow: inset 4px 0 1.3px -3px rgba(0, 0, 0, 0.09), inset 8px 0 6px -6px rgba(0, 0, 0, 0.15);
- }
- #back-cover, #page1, #page3, #page5 {
- box-shadow: inset -4px 0 1.5px -3px rgba(0, 0, 0, 0.09), inset -8px 0 6px -6px rgba(0, 0, 0, 0.15);
- }
- .zine-empty {
- width: ${PAGE_WIDTH_IN}in;
- height: ${PAGE_HEIGHT_IN}in;
- flex-shrink: 0;
- border: 1px dashed #666;
- }
- .zine-empty:first-child {
- border-right: none;
- }
- .zine-empty:last-child {
- border-left: none;
- }
599 .zine-nav {
600 position: fixed;
601 bottom: 0;
602 left: 0;
603 right: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 16px;
- padding: 12px 20px;
604 + ${NAV_BUTTON_CSS}
605 background: transparent;
606 }
607 .zine-nav button {
- background: rgba(255,255,255,0.15);
- color: white;
- border: none;
- border-radius: 50%;
- width: 36px;
- height: 36px;
- cursor: pointer;
- font-size: 18px;
- display: flex;
- align-items: center;
- justify-content: center;
608 + ${NAV_BUTTON_STYLES}
609 }
610 .zine-nav button:hover:not(:disabled) { background: rgba(255,255,255,0.25); }
611 .zine-nav button:disabled { opacity: 0.3; cursor: default; }
617 }
618 `;
619
620 + // Serialize SPREADS array for the viewer
621 + const spreadsJson = JSON.stringify(SPREADS);
622 + const pageIdsJson = JSON.stringify(PAGE_IDS);
623 +
624 const js = `
- const SPREADS = [
- { left: null, right: 'front-cover', label: 'Front Cover' },
- { left: 'page1', right: 'page2', label: 'Pages 1–2' },
- { left: 'page3', right: 'page4', label: 'Pages 3–4' },
- { left: 'page5', right: 'page6', label: 'Pages 5–6' },
- { left: 'back-cover', right: null, label: 'Back Cover' }
- ];
625 + const SPREADS = ${spreadsJson};
626 + const PAGE_IDS = ${pageIdsJson};
627 let currentSpread = 0;
628 let container;
629
645 const visible = [spread.left, spread.right].filter(Boolean);
646
647 // Move existing pages back to body before clearing container
- const existingPages = container.querySelectorAll('#front-cover, #page1, #page2, #page3, #page4, #page5, #page6, #back-cover');
- existingPages.forEach(page => {
- page.style.display = 'none';
- document.body.appendChild(page);
648 + PAGE_IDS.forEach(id => {
649 + const page = document.getElementById(id);
650 + if (page) {
651 + page.style.display = 'none';
652 + document.body.appendChild(page);
653 + }
654 });
655
656 container.innerHTML = '';
721 }
722
723 // File operations
-window.saveFile = function() {
- const blob = new Blob([editorView.state.doc.toString()], { type: 'text/html' });
724 +function downloadHtmlFile(content, filename = 'my-zine.html') {
725 + const blob = new Blob([content], { type: 'text/html' });
726 const url = URL.createObjectURL(blob);
727 const a = document.createElement('a');
728 a.href = url;
- a.download = 'my-zine.html';
729 + a.download = filename;
730 a.click();
731 URL.revokeObjectURL(url);
732 +}
733 +
734 +window.saveFile = function() {
735 + downloadHtmlFile(editorView.state.doc.toString());
736 };
737
738 window.saveFileWithViewer = function() {
739 const code = editorView.state.doc.toString();
740 const { css, js } = generateViewerCode();
741
- // Inject viewer CSS at START of head (so user styles take precedence for page content)
- // Inject viewer JS at END of head (needs to run after DOM is ready)
- let processedCode = code;
- const headStartMatch = processedCode.match(/<head[^>]*>/i);
- const headEndMatch = processedCode.match(/<\/head>/i);
-
- if (headStartMatch && headEndMatch) {
- // Insert CSS right after <head>
- const cssInsertPos = processedCode.indexOf(headStartMatch[0]) + headStartMatch[0].length;
- processedCode = processedCode.slice(0, cssInsertPos) +
- `\n<style id="zine-viewer-css">${css}</style>` +
- processedCode.slice(cssInsertPos);
-
- // Insert JS before </head> (recalculate position after CSS insertion)
- const newHeadEndMatch = processedCode.match(/<\/head>/i);
- const jsInsertPos = processedCode.indexOf(newHeadEndMatch[0]);
- processedCode = processedCode.slice(0, jsInsertPos) +
- `<script id="zine-viewer-js">${js}<\/script>\n` +
- processedCode.slice(jsInsertPos);
742 + // Check if code has proper head tags
743 + const hasHead = /<head[^>]*>/i.test(code) && /<\/head>/i.test(code);
744 +
745 + let processedCode;
746 + if (hasHead) {
747 + // Inject viewer CSS at START of head (so user styles take precedence for page content)
748 + // Inject viewer JS at END of head (needs to run after DOM is ready)
749 + processedCode = insertAfterHead(code, `\n<style id="zine-viewer-css">${css}</style>`);
750 + processedCode = insertBeforeHeadClose(processedCode, `<script id="zine-viewer-js">${js}<\/script>\n`);
751 } else {
752 // No proper head found, wrap content
753 processedCode = `<!DOCTYPE html>\n<html>\n<head>\n<style id="zine-viewer-css">${css}</style>\n<script id="zine-viewer-js">${js}<\/script>\n</head>\n<body>\n${code}\n</body>\n</html>`;
754 }
755
- const blob = new Blob([processedCode], { type: 'text/html' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'my-zine.html';
- a.click();
- URL.revokeObjectURL(url);
756 + downloadHtmlFile(processedCode);
757 };
758
759 window.loadFile = function() {