Skip to content

Commit e79bb78

Browse files
authored
Merge pull request #1561 from tanem/review
Tidy and improve comments
2 parents 8e27f5d + b49992b commit e79bb78

10 files changed

Lines changed: 52 additions & 55 deletions

.github/copilot-instructions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ This project follows strict versioning conventions for dependencies:
9797
- **No arrow function class methods**: this is a functional codebase with no classes.
9898
- **Strict TypeScript and ESLint**: `tsconfig.base.json` and `eslint.config.mjs` enforce strict type safety. Never use `any` types; use `unknown` when type is truly dynamic. Use non-null assertions (`!`) only with runtime guarantees (e.g., array access within bounds-checked loops).
9999
- **Formatting**: Prettier handles all JS/TS formatting. Run `npm run format` or check with `npm run check:format`.
100+
- **Comment style**: Use `//` comments, not `/* */` (except for istanbul/eslint directives). Comments are wrapped to 80 columns.
100101

101102
## IRI Renumeration
102103

@@ -121,6 +122,7 @@ When `renumerateIRIElements` is `true` (the default), the injector rewrites `id`
121122
- Keep `.github/copilot-instructions.md`, `README.md`, and `MIGRATION.md` up to date when making changes that affect the public API, build pipeline, testing patterns, or code conventions.
122123
- Use NZ English in documentation (e.g. "serialise", "normalise", "colour", "behaviour").
123124
- **Copilot instructions should only contain information that cannot be readily inferred from the source code.** Do not duplicate implementation details (e.g. function names, processing order, data structures) that an agent can discover by reading the relevant files. Focus on conventions, design decisions, known limitations, and non-obvious constraints.
125+
- **README structure follows [standard-readme](https://github.com/RichardLitt/standard-readme).** Keep the main `README.md` concise and scannable. Detailed feature documentation (usage examples, caveats, limitations) belongs in a `README.md` within the relevant `examples/` subdirectory, linked from the main README.
124126

125127
## Writing Style
126128

src/clone-svg.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Deep clone to avoid mutating cached SVG originals. Each injection receives
2+
// its own copy that can be safely modified (attribute transfer, IRI
3+
// renumeration, script removal, etc.).
14
const cloneSvg = (sourceSvg: SVGSVGElement) =>
25
sourceSvg.cloneNode(true) as SVGSVGElement
36

src/inject-element.ts

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import uniqueId from './unique-id'
66

77
type ElementType = Element | HTMLElement | null
88

9+
// Tracks elements currently being injected. Prevents duplicate injection if
10+
// SVGInjector is called with the same element twice before the first injection
11+
// completes.
912
const injectedElements: ElementType[] = []
1013
const ranScripts: Record<string, boolean> = {}
1114
const svgNamespace = 'http://www.w3.org/2000/svg'
@@ -27,22 +30,17 @@ const injectElement = (
2730
return
2831
}
2932

30-
// Make sure we aren't already in the process of injecting this element to
31-
// avoid a race condition if multiple injections for the same element are run.
32-
// :NOTE: Using indexOf() only _after_ we check for SVG support and bail, so
33-
// no need for IE8 indexOf() polyfill.
3433
if (injectedElements.indexOf(el) !== -1) {
35-
// TODO: Extract.
3634
injectedElements.splice(injectedElements.indexOf(el), 1)
35+
// Release the DOM reference to allow GC. The cast is needed because the
36+
// parameter is typed as NonNullable.
3737
;(el as ElementType) = null
3838
return
3939
}
4040

41-
// Remember the request to inject this element, in case other injection calls
42-
// are also trying to replace this element before we finish.
4341
injectedElements.push(el)
44-
45-
// Try to avoid loading the orginal image src if possible.
42+
// Clear src to prevent the browser from fetching the original image URL while
43+
// the SVG load is in progress.
4644
el.setAttribute('src', '')
4745

4846
// Strip fragment identifier for sprite support. The base URL is used for
@@ -55,7 +53,6 @@ const injectElement = (
5553

5654
loadSvg(baseUrl, httpRequestWithCredentials, (error, loadedSvg) => {
5755
if (!loadedSvg) {
58-
// TODO: Extract.
5956
injectedElements.splice(injectedElements.indexOf(el), 1)
6057
;(el as ElementType) = null
6158
callback(error)
@@ -115,7 +112,6 @@ const injectElement = (
115112

116113
svg.setAttribute('data-src', elUrl)
117114

118-
// Copy all the data elements to the svg.
119115
const elData: Attr[] = [].filter.call(el.attributes, (at: Attr) => {
120116
return /^data-\w[\w-]*$/.test(at.name)
121117
})
@@ -128,20 +124,13 @@ const injectElement = (
128124
})
129125

130126
if (renumerateIRIElements) {
131-
// Make sure any internally referenced clipPath ids and their clip-path
132-
// references are unique.
133-
//
134-
// This addresses the issue of having multiple instances of the same SVG
135-
// on a page and only the first clipPath id is referenced.
127+
// Rewrite IRI element ids to be unique across injection instances.
128+
// Browsers skip clipPaths in hidden parent elements, so duplicate ids
129+
// cause all but the first instance to lose clipping. Reference:
130+
// https://bugzilla.mozilla.org/show_bug.cgi?id=376027.
136131
//
137-
// Browsers often shortcut the SVG Spec and don't use clipPaths contained
138-
// in parent elements that are hidden, so if you hide the first SVG
139-
// instance on the page, then all other instances lose their clipping.
140-
// Reference: https://bugzilla.mozilla.org/show_bug.cgi?id=376027
141-
142-
// Handle all defs elements that have iri capable attributes as defined by
143-
// w3c: http://www.w3.org/TR/SVG/linking.html#processingIRI. Mapping IRI
144-
// addressable elements to the properties that can reference them.
132+
// IRI-addressable elements mapped to referencing properties per the SVG
133+
// spec: http://www.w3.org/TR/SVG/linking.html#processingIRI.
145134
const iriElementsAndProperties: Record<string, string[]> = {
146135
clipPath: ['clip-path'],
147136
'color-profile': ['color-profile'],
@@ -214,7 +203,6 @@ const injectElement = (
214203
Object.keys(iriElementsAndProperties).forEach((key) => {
215204
properties = iriElementsAndProperties[key]!
216205

217-
// All of the properties that can reference this element type.
218206
let referencingElements: NodeListOf<Element>
219207
Array.prototype.forEach.call(properties, (property: string) => {
220208
referencingElements = svg.querySelectorAll('[' + property + ']')
@@ -300,14 +288,12 @@ const injectElement = (
300288
}
301289
}
302290

303-
// Remove any unwanted/invalid namespaces that might have been added by SVG
304-
// editing tools.
291+
// Remove invalid namespaces that SVG editing tools may have added.
305292
svg.removeAttribute('xmlns:a')
306293

307-
// Post page load injected SVGs don't automatically have their script
308-
// elements run, so we'll need to make that happen, if requested.
294+
// Injected SVGs don't automatically run their script elements, so extract
295+
// and evaluate them manually if requested.
309296

310-
// Find then prune the scripts.
311297
const scripts = svg.querySelectorAll('script')
312298
const scriptsToEval: string[] = []
313299
let script: string | null
@@ -317,7 +303,7 @@ const injectElement = (
317303
const scriptElement = scripts[i]!
318304
scriptType = scriptElement.getAttribute('type')
319305

320-
// Only process javascript types. SVG defaults to 'application/ecmascript'
306+
// Only process JavaScript types. SVG defaults to 'application/ecmascript'
321307
// for unset types.
322308
/* istanbul ignore else */
323309
if (
@@ -326,21 +312,17 @@ const injectElement = (
326312
scriptType === 'application/javascript' ||
327313
scriptType === 'text/javascript'
328314
) {
329-
// innerText for IE, textContent for other browsers.
330315
script = scriptElement.innerText || scriptElement.textContent
331316

332-
// Stash.
333317
/* istanbul ignore else */
334318
if (script) {
335319
scriptsToEval.push(script)
336320
}
337321

338-
// Tidy up and remove the script element since we don't need it anymore.
339322
svg.removeChild(scriptElement)
340323
}
341324
}
342325

343-
// Run/Eval the scripts if needed.
344326
if (
345327
scriptsToEval.length > 0 &&
346328
(evalScripts === 'always' ||
@@ -351,24 +333,18 @@ const injectElement = (
351333
l < scriptsToEvalLen;
352334
l++
353335
) {
354-
// :NOTE: Yup, this is a form of eval, but it is being used to eval code
355-
// the caller has explictely asked to be loaded, and the code is in a
356-
// caller defined SVG file... not raw user input.
357-
//
358-
// Also, the code is evaluated in a closure and not in the global scope.
359-
// If you need to put something in global scope, use 'window'.
336+
// This is a form of eval, but only for code the caller has explicitly
337+
// asked to load from their own SVG files. The code runs in a closure,
338+
// not the global scope.
360339
new Function(scriptsToEval[l]!)(window)
361340
}
362341

363-
// Remember we already ran scripts for this svg.
364342
ranScripts[elUrl] = true
365343
}
366344

367-
// :WORKAROUND: IE doesn't evaluate <style> tags in SVGs that are
368-
// dynamically added to the page. This trick will trigger IE to read and use
369-
// any existing SVG <style> tags.
370-
//
371-
// Reference: https://github.com/iconic/SVGInjector/issues/23.
345+
// Some browsers don't evaluate <style> tags in SVGs that are dynamically
346+
// added to the page. This triggers a re-read. Reference:
347+
// https://github.com/iconic/SVGInjector/issues/23.
372348
const styleTags = svg.querySelectorAll('style')
373349
Array.prototype.forEach.call(styleTags, (styleTag: HTMLStyleElement) => {
374350
styleTag.textContent += ''
@@ -386,12 +362,7 @@ const injectElement = (
386362
return
387363
}
388364

389-
// Replace the image with the svg.
390365
el.parentNode.replaceChild(svg, el)
391-
392-
// Now that we no longer need it, drop references to the original element so
393-
// it can be GC'd.
394-
// TODO: Extract
395366
injectedElements.splice(injectedElements.indexOf(el), 1)
396367
;(el as ElementType) = null
397368

src/load-svg-cached.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const loadSvgCached = (
2626
// Errors are always refetched.
2727
}
2828

29-
// Seed the cache to indicate we are loading this URL.
29+
// Seed the cache to indicate this URL is loading.
3030
cache.set(url, undefined)
3131
queueRequest(url, callback)
3232

src/make-ajax-request.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ const makeAjaxRequest = (
1010

1111
httpRequest.onreadystatechange = () => {
1212
try {
13+
// URLs with a .svg extension skip content-type validation. This avoids
14+
// failures on the file:// protocol where browsers don't send Content-Type
15+
// headers, and is unnecessary when the extension already indicates SVG
16+
// content.
1317
if (!/\.svg/i.test(url) && httpRequest.readyState === 2) {
1418
const contentType = httpRequest.getResponseHeader('Content-Type')
1519
if (!contentType) {
@@ -33,6 +37,7 @@ const makeAjaxRequest = (
3337
)
3438
}
3539

40+
// Browsers return status 0 (not 200) for successful file:// loads.
3641
if (
3742
httpRequest.status === 200 ||
3843
(isLocal() && httpRequest.status === 0)
@@ -61,7 +66,6 @@ const makeAjaxRequest = (
6166

6267
httpRequest.withCredentials = httpRequestWithCredentials
6368

64-
// Defensive check for old browsers that might not have overrideMimeType
6569
/* istanbul ignore else */
6670
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6771
if (httpRequest.overrideMimeType) {

src/request-queue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const processRequestQueue = (url: string) => {
1616
}
1717

1818
for (let i = 0, len = callbacks.length; i < len; i++) {
19-
// Make these calls async so we avoid blocking the page/renderer.
19+
// Async to avoid blocking the renderer.
2020
setTimeout(() => {
2121
if (Array.isArray(requestQueue[url])) {
2222
const cacheValue = cache.get(url)

src/svg-injector.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const SVGInjector = (
6262
(error, svg) => {
6363
afterEach(error, svg)
6464
afterAll(1)
65+
// Release the DOM reference to allow GC.
6566
elements = null
6667
},
6768
)

test/eval-scripts.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ test.describe('eval scripts', () => {
2222
const expectedDefault =
2323
'<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 100" class="injected-svg inject-me" data-src="/fixtures/script.svg" xmlns:xlink="http://www.w3.org/1999/xlink"><circle cx="50" cy="50" r="15" fill="green"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 100" class="injected-svg inject-me" data-src="/fixtures/script.svg" xmlns:xlink="http://www.w3.org/1999/xlink"><circle cx="50" cy="50" r="15" fill="green"></circle></svg>'
2424

25+
// Replace window.alert with a counter. Playwright's default dialog handling
26+
// would auto-dismiss alerts without tracking them, so we need a controlled
27+
// way to verify how many times scripts in the injected SVGs call alert().
2528
const setupAlerts = async (page: Page) => {
2629
await page.evaluate(() => {
2730
;(window as unknown as AlertWindow).__alertCount = 0

test/local.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ interface LocalWindow extends Window {
1717
}
1818

1919
test.describe('local', () => {
20+
// Lets the real XHR fail naturally on a file:// page. The browser's
21+
// cross-origin restrictions cause the request to fail, triggering the
22+
// local-specific error message via isLocal() in make-ajax-request.ts.
2023
test('not found', async ({ page }) => {
2124
await addSvgInjector(page)
2225
await page.goto(blankFileUrl)
@@ -37,6 +40,11 @@ test.describe('local', () => {
3740
)
3841
})
3942

43+
// Playwright browsers enforce cross-origin restrictions on file:// pages, so
44+
// a real XHR to a local SVG file would fail. The mock below simulates what a
45+
// successful local XHR looks like: status 0 (not 200), no Content-Type
46+
// header, and a populated responseXML. This exercises the `isLocal() &&
47+
// httpRequest.status === 0` success path in make-ajax-request.ts.
4048
test('ok', async ({ page }) => {
4149
await addSvgInjector(page)
4250
await page.goto(blankFileUrl)

test/svg-injector.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ test.describe('SVGInjector', () => {
160160
})
161161

162162
const actual = formatHtml(result.html)
163+
// Firefox serialises SVG attributes in a different order than Chromium and
164+
// WebKit, so we need browser-specific expected strings.
163165
const expectedFirefox =
164166
'<svg width="150" height="150" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" class="injected-svg inject-me" data-src="/fixtures/style-tag.svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style>circle {fill: orange;stroke: black;stroke-width: 10px;}</style><circle cx="50" cy="50" r="40"></circle></svg>'
165167
const expectedDefault =
@@ -559,6 +561,9 @@ test.describe('SVGInjector', () => {
559561
expect(result.error).toBe('Parent node is null')
560562
})
561563

564+
// Some older libraries (e.g. MooTools) replace window.Document with a plain
565+
// function. This verifies injection still works when Document is not the
566+
// native constructor.
562567
test('handles Document wrangling via old libs', async ({ page }) => {
563568
await setupPage(page)
564569

0 commit comments

Comments
 (0)