- commit
- 86690f4c2938d25ecdeb3e3db8844ffac858dffa
- parent
- 3d73b73f677f95d08c37c76d04306582efda7876
- author
- Dami <dami.etoile@gmail.com>
| M | diagramzip-api/ | 4 | +- |
| M | diagramzip-api/ | 1 | + |
| M | diagramzip-render/ | 8 | ++- |
| M | diagramzip-render/ | 2 | + |
| M | diagramzip-render/ | 2 | + |
| M | diagramzip-render/ | 2 | +- |
| M | diagramzip/ | 77 | ++++++++++++++++++++------- |
| M | diagramzip/ | 12 | ++++- |
| M | diagramzip/ | 86 | +++++++++++++++--------------- |
| M | diagramzip/ | 16 | ++++++ |
@@ -521,7 +521,9 @@ async function getRender(
521521 object.writeHttpMetadata(headers)522522 headers.set('ETag', object.httpEtag)523523 headers.set('X-Content-Type-Options', 'nosniff')524- headers.set('Content-Security-Policy', "default-src 'none'; sandbox")524+ headers.set('Content-Security-Policy', format === 'svg'525+ ? "default-src 'none'; style-src 'unsafe-inline'; sandbox"526+ : "default-src 'none'; sandbox")525527 headers.set('X-Renderer-Unit', head.unit)526528 headers.set('X-Renderer-Build', head.build)527529 headers.set('X-Renderer-Pipeline', head.pipeline.join(','))@@ -309,6 +309,7 @@ describe('open aliases', () => {
309309 expect(render.headers.get('X-Renderer-Unit')).toBe('d2')310310 expect(render.headers.get('X-Renderer-Build')).toBe('d2-compat-unit-1')311311 expect(render.headers.get('X-Renderer-Pipeline')).toBe('d2')312+ expect(render.headers.get('Content-Security-Policy')).toBe("default-src 'none'; style-src 'unsafe-inline'; sandbox")312313 expect(await render.text()).toBe(svg)313314 314315 const head = await env.CONTENT.get(`render-heads/open/${renderId}.svg.json`)@@ -30,11 +30,13 @@ export function imageCompatibleAnimations(svg: string): string {
3030 const animation = styleDeclaration(style, 'animation')3131 const duration = animation?.match(/^dashdraw\s+(\d+(?:\.\d+)?)s\s+linear\s+infinite$/)?.[1]3232 if (!offset || !/^-?\d+(?:\.\d+)?(?:px)?$/.test(offset) || !duration) return tag33- const withoutCssAnimation = style33+ const imageCompatibleStyle = style3434 .split(';')35- .filter(declaration => declaration.trim() && !/^\s*animation\s*:/i.test(declaration))35+ .filter(declaration => declaration.trim() && !/^\s*(?:animation|stroke-dashoffset)\s*:/i.test(declaration))3636 .join(';')37- const normalizedTag = tag.replace(/\bstyle="[^"]*"/, `style="${withoutCssAnimation};"`)37+ const normalizedTag = tag38+ .replace(/\s+mask="[^"]*"/, '')39+ .replace(/\bstyle="[^"]*"/, `style="${imageCompatibleStyle};"`)3840 const animate = `<animate attributeName="stroke-dashoffset" from="0" to="${offset}" dur="${duration}s" calcMode="linear" repeatCount="indefinite"></animate>`3941 return /\/\s*>$/.test(normalizedTag)4042 ? `${normalizedTag.replace(/\/\s*>$/, '>')}${animate}</path>`@@ -24,6 +24,8 @@ describe('D2 dedicated Worker adapter', () => {
2424 expect(result.body).toContain('repeatCount="indefinite"')2525 expect(result.body).toMatch(/<path[^>]*animated-connection[^>]*><animate[^>]*><\/animate><\/path>/)2626 expect(result.body).not.toContain('animation: dashdraw')27+ expect(result.body).not.toMatch(/<path[^>]*animated-connection[^>]*style="[^"]*stroke-dashoffset/)28+ expect(result.body).not.toMatch(/<path[^>]*animated-connection[^>]*mask=/)2729 expect(result.body).toContain('next')2830 })2931 })@@ -41,6 +41,8 @@ describe('renderer unit protocol', () => {
4141 expect(response.headers.get('X-Diagram-Pipeline')).toBe('vegalite,vega')4242 expect(response.headers.get('Access-Control-Expose-Headers')).toContain('X-Diagram-Unit')4343 expect(response.headers.get('Access-Control-Expose-Headers')).toContain('X-Renderer-Build')44+ expect(response.headers.get('Content-Type')).toContain('image/svg+xml')45+ expect(response.headers.get('Content-Security-Policy')).toBe("default-src 'none'; style-src 'unsafe-inline'; sandbox")4446 expect(await response.text()).toContain('<text x="1" y="8">ok</text>')4547 })4648 @@ -1 +1 @@
1-{"$schema":"../node_modules/wrangler/config-schema.json","name":"diagramzip-render-d2","main":"../src/units/d2.ts","compatibility_date":"2026-08-20","compatibility_flags":["nodejs_compat"],"observability":{"enabled":true},"vars":{"RENDERER_BUILD":"d2-0.7.1-custom-grid-2"},"rules":[{"type":"CompiledWasm","globs":["**/*.wasm"],"fallthrough":false}],"routes":[{"pattern":"d2.render.diagram.zip","custom_domain":true}]}1+{"$schema":"../node_modules/wrangler/config-schema.json","name":"diagramzip-render-d2","main":"../src/units/d2.ts","compatibility_date":"2026-08-20","compatibility_flags":["nodejs_compat"],"observability":{"enabled":true},"vars":{"RENDERER_BUILD":"d2-0.7.1-custom-grid-4"},"rules":[{"type":"CompiledWasm","globs":["**/*.wasm"],"fallthrough":false}],"routes":[{"pattern":"d2.render.diagram.zip","custom_domain":true}]}@@ -69,15 +69,47 @@ document.querySelector('#app').innerHTML = `
6969 <span class="sr-only">Diagram type</span>7070 <select id="diagram-type"></select>7171 </label>72- <a class="docs-link" href="https://docs.diagram.zip/" aria-label="Documentation" title="Documentation">73- <svg class="docs-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">74- <path d="M5 4.75A2.75 2.75 0 0 1 7.75 2H19v16.25A2.75 2.75 0 0 0 16.25 21H5a2 2 0 0 1-2-2V6.75A2 2 0 0 1 5 4.75Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>75- <path d="M19 18.25A2.75 2.75 0 0 0 16.25 21H7.75A2.75 2.75 0 0 1 5 18.25V4.75M8 7h7M8 10.5h7M8 14h4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>76- </svg>77- </a>78- <button class="secondary-action" id="details" type="button" aria-label="Details">Details</button>79- <button class="secondary-action" id="save" type="button">Save</button>80- <button class="primary-action" id="share" type="button">Share</button>72+ <div class="header-actions" role="group" aria-label="Diagram actions">73+ <a class="header-icon-action" href="https://docs.diagram.zip/" aria-label="Documentation" title="Documentation">74+ <svg class="header-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">75+ <path d="M3.5 5.5c3.25-.75 6-.1 8.5 2v12c-2.5-2.1-5.25-2.75-8.5-2V5.5Zm17 0c-3.25-.75-6-.1-8.5 2v12c2.5-2.1 5.25-2.75 8.5-2V5.5Z"/>76+ <path d="M12 7.5v12"/>77+ </svg>78+ </a>79+ <button class="header-icon-action" id="details" type="button" aria-label="Details" title="Details">80+ <svg class="header-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">81+ <path d="M6 3.5h8l4 4v13H6v-17Z"/>82+ <path d="M14 3.5V8h4M9 12h6M9 15.5h6"/>83+ </svg>84+ </button>85+ <button class="header-icon-action" id="save" type="button" data-save-state="save" aria-label="Save" title="Save">86+ <svg class="header-action-icon save-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">87+ <g data-save-icon="save">88+ <path d="M5 3.5h11.5L19 6v14.5H5v-17Z"/>89+ <path d="M8 3.5V9h8V3.5M8 20.5v-7h8v7"/>90+ </g>91+ <g data-save-icon="saving">92+ <path d="M19.5 8.5A8 8 0 1 0 20 14"/>93+ <path d="M19.5 4.5v4h-4"/>94+ </g>95+ <g data-save-icon="saved">96+ <path d="m5.5 12 4 4 9-9"/>97+ </g>98+ <g data-save-icon="copy">99+ <path d="M8 7.5h11v13H8v-13Z"/>100+ <path d="M5 16.5H4v-13h11v1M13.5 11v6M10.5 14h6"/>101+ </g>102+ </svg>103+ </button>104+ <button class="header-icon-action" id="share" type="button" aria-label="Share" title="Share">105+ <svg class="header-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">106+ <circle cx="18" cy="5" r="2.5"/>107+ <circle cx="6" cy="12" r="2.5"/>108+ <circle cx="18" cy="19" r="2.5"/>109+ <path d="m8.2 10.8 7.6-4.5M8.2 13.2l7.6 4.5"/>110+ </svg>111+ </button>112+ </div>81113 </div>82114 </header>83115 @@ -902,18 +934,25 @@ function setRemoteAlias(alias, writeCapability, state, bundleKey = null) {
902934 function updateSaveButton() {903935 const button = document.querySelector('#save')904936 updateDraftControls()937+ let state = 'save'938+ let label = 'Save'939+ let disabled = false905940 if (saving) {906- button.textContent = 'Saving…'907- button.disabled = true908- return909- }910- if (remote.aliasId && !remote.dirty) {911- button.textContent = 'Saved'912- button.disabled = true913- return941+ state = 'saving'942+ label = 'Saving'943+ disabled = true944+ } else if (remote.aliasId && !remote.dirty) {945+ state = 'saved'946+ label = 'Saved'947+ disabled = true948+ } else if (remote.aliasId && !remote.writeCapability) {949+ state = 'copy'950+ label = 'Save a copy'914951 }915- button.textContent = remote.aliasId && !remote.writeCapability ? 'Save copy' : 'Save'916- button.disabled = false952+ button.dataset.saveState = state953+ button.setAttribute('aria-label', label)954+ button.title = label955+ button.disabled = disabled917956 }918957 919958 function updateDraftControls() {@@ -2,6 +2,13 @@ import { clientAdapterFor } from './client-renderers.js'
22 import { sanitizeAndDecorateSvg } from './client-svg.js'33 import { httpRendererUnitFor } from './renderer-units.js'44 5+const STATUS_LABELS = {6+ idle: 'Ready',7+ loading: 'Rendering',8+ ready: 'Rendered',9+ error: 'Error',10+}11+512 function clamp(value, minimum, maximum) {613 return Math.min(Math.max(value, minimum), maximum)714 }@@ -227,7 +234,10 @@ export class PreviewController {
227234 }228235 229236 setStatus(message, state) {230- this.status.textContent = message237+ const label = STATUS_LABELS[state] ?? 'Ready'238+ this.status.textContent = label239+ this.status.title = message === label ? '' : message240+ this.status.setAttribute('aria-label', message === label ? label : `${label}: ${message}`)231241 this.status.dataset.state = state232242 }233243 @@ -140,7 +140,7 @@ button { cursor: pointer; }
140140 .header-meta {141141 display: flex;142142 align-items: center;143- gap: 9px;143+ gap: 0;144144 min-width: 0;145145 }146146 @@ -163,21 +163,18 @@ button { cursor: pointer; }
163163 .workspace { grid-row: 3; }164164 165165 .render-status {166- max-width: min(38vw, 420px);167- overflow: hidden;166+ flex: 0 0 auto;168167 color: var(--muted);169168 font-size: 13px;170- text-overflow: ellipsis;171169 white-space: nowrap;172170 }173171 174172 .render-status[data-state="error"] { color: var(--danger); }175-.render-status[data-state="loading"]::before { content: "◌ "; }176173 177174 .type-picker select,178175 .primary-action,179176 .secondary-action,180-.docs-link,177+.header-icon-action,181178 .preview-toolbar button,182179 .mobile-tabs button,183180 .icon-button,@@ -193,6 +190,8 @@ button { cursor: pointer; }
193190 padding: 7px 30px 7px 11px;194191 }195192 193+.type-picker { margin: 0 10px; }194+196195 .primary-action {197196 padding: 7px 15px;198197 border-color: var(--action-bg);@@ -212,24 +211,46 @@ button { cursor: pointer; }
212211 213212 .secondary-action:hover { background: var(--soft); }214213 215-.docs-link {214+.header-actions {215+ display: flex;216+ align-items: center;217+ gap: 6px;218+}219+220+.header-icon-action {216221 display: inline-grid;217- width: 36px;218- min-width: 36px;219- height: 36px;222+ width: 40px;223+ min-width: 40px;224+ height: 40px;225+ padding: 0;220226 place-items: center;221227 color: var(--ink);222228 text-decoration: none;223229 }224230 225-.docs-link:hover { background: var(--soft); }231+.header-icon-action:hover { background: var(--soft); }232+.header-icon-action:disabled { cursor: default; opacity: .58; }226233 227-.docs-link-icon {228- width: 19px;229- height: 19px;234+.header-action-icon {235+ width: 20px;236+ height: 20px;230237 display: block;238+ fill: none;239+ stroke: currentColor;240+ stroke-width: 1.75;241+ stroke-linecap: round;242+ stroke-linejoin: round;231243 }232244 245+.save-action-icon [data-save-icon] { display: none; }246+#save[data-save-state="save"] [data-save-icon="save"],247+#save[data-save-state="saving"] [data-save-icon="saving"],248+#save[data-save-state="saved"] [data-save-icon="saved"],249+#save[data-save-state="copy"] [data-save-icon="copy"] { display: block; }250+#save[data-save-state="saving"] .save-action-icon { animation: save-spin 900ms linear infinite; }251+252+@keyframes save-spin { to { transform: rotate(360deg); } }253+233254 .workspace {234255 display: grid;235256 grid-template-columns: minmax(340px, .82fr) minmax(440px, 1.18fr);@@ -531,28 +552,15 @@ dialog::backdrop {
531552 @media (max-width: 760px) {532553 .app-shell { grid-template-rows: 64px auto 42px minmax(0, 1fr); }533554 .render-status { display: none; }534- .render-status[data-state="error"] {535- position: fixed;536- top: 108px;537- right: 10px;538- left: 10px;539- z-index: 20;540- display: block;541- max-width: none;542- max-height: 84px;543- overflow: auto;544- padding: 10px 12px;545- border: 1px solid color-mix(in srgb, var(--danger), transparent 55%);546- border-radius: 10px;547- background: var(--surface-raised);548- box-shadow: var(--shadow);549- white-space: normal;550- }551555 .app-header { padding-left: 14px; }552556 .brand-mark { width: 36px; height: 36px; }553557 .document-identity { flex-basis: 140px; gap: 8px; }554558 .document-title { width: 100%; font-size: 18px; }555- .header-meta { flex: 1; justify-content: flex-end; gap: 6px; }559+ .header-meta { flex: 1; justify-content: flex-end; }560+ .type-picker { margin: 0 8px; }561+ .header-actions { gap: 4px; }562+ .header-icon-action { width: 36px; min-width: 36px; height: 36px; }563+ .header-action-icon { width: 18px; height: 18px; }556564 .draft-bar { align-items: stretch; flex-direction: column; gap: 7px; padding: 9px 14px; }557565 .draft-bar div { display: grid; grid-template-columns: 1fr 1fr; }558566 .mobile-tabs { grid-row: 3; }@@ -595,21 +603,13 @@ dialog::backdrop {
595603 596604 @media (max-width: 390px) {597605 .app-header { gap: 8px; }598- .header-meta { gap: 6px; }599606 .type-picker { flex-basis: 108px; }600607 .type-picker select { max-width: 108px; }608+ .type-picker { margin: 0 5px; }601609 .document-identity { flex-basis: 76px; }602610 .document-title { min-width: 40px; padding-inline: 4px; font-size: 16px; }603- #details {604- width: 36px;605- padding-inline: 0;606- font-size: 0;607- }608- #details::before {609- content: "…";610- font-size: 18px;611- line-height: 1;612- }611+ .header-actions { gap: 3px; }612+ .header-icon-action { width: 34px; min-width: 34px; height: 34px; }613613 .primary-action, .secondary-action { padding-inline: 9px; }614614 .preview-toolbar { bottom: 10px; left: 10px; }615615 .minimap { right: 10px; bottom: 62px; }@@ -108,3 +108,19 @@ test('does not send a failed diagrams.net renderer to an HTTP unit', async () =>
108108 assert.equal(unitCalls, 0)109109 assert.equal(statuses.at(-1).state, 'error')110110 })111+112+test('keeps render status text to a single state word', () => {113+ const attributes = new Map()114+ const status = {115+ dataset: {},116+ setAttribute(name, value) { attributes.set(name, value) },117+ }118+ const controller = { status }119+120+ PreviewController.prototype.setStatus.call(controller, 'The renderer rejected this source.', 'error')121+122+ assert.equal(status.textContent, 'Error')123+ assert.equal(status.title, 'The renderer rejected this source.')124+ assert.equal(status.dataset.state, 'error')125+ assert.equal(attributes.get('aria-label'), 'Error: The renderer rejected this source.')126+})