mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 08:36:25 +03:00
fix(live): CSP-meta patch+revert preserves space before self-closing /
Sanity-check on the live-inject unwrap path turned up a real round-trip
bug on HTML files that ship a `<meta http-equiv="Content-Security-Policy"
content="..." />` tag (the leading space before `/>` is the canonical
self-closing form).
Trace:
- The tag-finder regex (`<meta\s+([^>]*?)\/?>`) captures any whitespace
between the last attribute and the closing `/>` as part of `attrs`.
- patchCspMeta did `attrs.replace(content, newContent) + ' ' + marker`,
appending the marker AFTER that captured trailing whitespace. Result:
`...content="..." data-...="..."` — a double space inside attrs and
the original space-before-slash gone.
- revertCspMeta then strips the marker via `\s*${origAttr.full}`, which
greedily eats both spaces — so the round trip leaves `"/>` with no
space, even though the original was `" />`.
Fix: split off the trailing whitespace from `attrs` before patching,
splice the marker into the attribute body with a single leading space,
and re-append the original trailing whitespace. The marker-removal
regex then consumes exactly one space and the trailing space rides
through unchanged.
Test coverage:
- New `round-trips through CSP-meta patch and revert` test in
live-inject.test.mjs covers the canonical Vite shape (CSP meta with
` />`).
- Plus a `round-trips with insertAfter` test for symmetry — the existing
suite only covered insertBefore.
- Existing 4 round-trip tests (HTML, JSX layout, multi-file, column-0)
all still pass byte-for-byte.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a701ee613a
commit
11dfad81da
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -388,7 +388,16 @@ export function patchCspMeta(content, port) {
|
|||||||
|
|
||||||
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
|
||||||
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
|
||||||
const newAttrs = attrs.replace(contentAttr.full, newContentAttr) + ' ' + marker;
|
// The tagRe captures any whitespace between the last attribute and the
|
||||||
|
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
|
||||||
|
// a replace would land it BEFORE that trailing space, leaving a double
|
||||||
|
// space inside attrs and clobbering the space before `/>`. Split off
|
||||||
|
// the trailing whitespace, splice the marker into the attribute body,
|
||||||
|
// and re-append the original trailing whitespace so a self-closing
|
||||||
|
// `<meta … />` round-trips byte-for-byte.
|
||||||
|
const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
|
||||||
|
const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
|
||||||
|
const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
|
||||||
const newTag = tag.full.replace(attrs, newAttrs);
|
const newTag = tag.full.replace(attrs, newAttrs);
|
||||||
|
|
||||||
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
|
||||||
|
|||||||
@@ -128,6 +128,71 @@ describe('live-inject — insert/remove round-trip preserves file bytes', () =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('round-trips with insertAfter — preserves indented opener line below it', () => {
|
||||||
|
const original = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
const file = join(tmp, 'index.html');
|
||||||
|
writeFileSync(file, original);
|
||||||
|
|
||||||
|
const cfgPath = join(tmp, 'config.json');
|
||||||
|
writeFileSync(cfgPath, JSON.stringify({
|
||||||
|
files: ['index.html'],
|
||||||
|
insertAfter: '<head>',
|
||||||
|
commentSyntax: 'html',
|
||||||
|
}));
|
||||||
|
|
||||||
|
runInject(tmp, cfgPath, ['--port', '8400']);
|
||||||
|
runInject(tmp, cfgPath, ['--remove']);
|
||||||
|
|
||||||
|
const after = readFileSync(file, 'utf-8');
|
||||||
|
assert.equal(after, original, 'insertAfter round-trip must restore original byte-for-byte');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('round-trips through CSP-meta patch and revert (insert mutates the meta tag, remove restores it)', () => {
|
||||||
|
// Mirrors a Vite app that ships a CSP meta tag in index.html. live-inject
|
||||||
|
// appends `http://localhost:PORT` to script-src / connect-src on insert
|
||||||
|
// and stashes the original directives in `data-impeccable-csp-original`.
|
||||||
|
// --remove must restore the meta tag's original `content` exactly.
|
||||||
|
const original = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; connect-src 'self';" />
|
||||||
|
<title>CSP test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
const file = join(tmp, 'index.html');
|
||||||
|
writeFileSync(file, original);
|
||||||
|
|
||||||
|
const cfgPath = join(tmp, 'config.json');
|
||||||
|
writeFileSync(cfgPath, JSON.stringify({
|
||||||
|
files: ['index.html'],
|
||||||
|
insertBefore: '</body>',
|
||||||
|
commentSyntax: 'html',
|
||||||
|
}));
|
||||||
|
|
||||||
|
runInject(tmp, cfgPath, ['--port', '8400']);
|
||||||
|
runInject(tmp, cfgPath, ['--remove']);
|
||||||
|
|
||||||
|
const after = readFileSync(file, 'utf-8');
|
||||||
|
assert.equal(after, original, 'CSP meta tag must round-trip exactly through insert+remove');
|
||||||
|
});
|
||||||
|
|
||||||
it('round-trips when the insert anchor has no leading indent (column-0 </body>)', () => {
|
it('round-trips when the insert anchor has no leading indent (column-0 </body>)', () => {
|
||||||
const original = `<html>
|
const original = `<html>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user