Skip to content

Commit 69da8e1

Browse files
authored
Merge pull request #74 from ReScienceLab/canvas/inspector-sprite-use
canvas: a sprite's <use> is inlined into the inspector's copy, and its host is no asset
2 parents ce4a5fb + 9beabad commit 69da8e1

2 files changed

Lines changed: 37 additions & 6 deletions

File tree

‎canvas/src/inspectorAgent.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ describe("AGENT", () => {
1919
// every backslash doubled and would silently turn `\s` into `s`.
2020
expect(AGENT).toContain("replace(/^\\s+|\\s+$/g,'')");
2121
});
22+
23+
it("inlines a same-document <use> and passes over a sprite host's own definitions", () => {
24+
// Issue #73: a <use href="#id"> cloned alone dangles, and a <defs> of <symbol>s draws nothing.
25+
expect(AGENT).toContain("document.getElementById(h.slice(1))");
26+
expect(AGENT).toContain(":not(defs *,symbol *)");
27+
// The ceiling is a count of resolutions, not a depth: two self-references would double per level.
28+
expect(AGENT).toContain("left=64");
29+
});
2230
});
2331

2432
describe("injectAgent", () => {

‎canvas/src/inspectorAgent.ts‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export interface SpAsset {
5252
mime: string;
5353
/** Payload length in characters; for a vector, the standalone markup's. */
5454
chars: number;
55-
/** For a vector, the viewBox size in board units. */
55+
/** For a vector, the viewBox size in board units, else its width and height attributes. */
5656
w: number;
5757
h: number;
5858
/** The `alt`; for a vector, its accessible name, class or the caption beside it. */
@@ -318,9 +318,9 @@ function addAsset(i,uri,via,el){var c=uri.indexOf(','),payload=uri.slice(c+1),k=
318318
cascade supplied (currentColor, var(), a fill from a stylesheet) are written in, and xmlns is
319319
added where a literal icon had none, so an <img> in the parent draws it and Figma accepts it. */
320320
function addSvg(i,el){
321-
if(!el.querySelector('path,rect,circle,ellipse,line,polygon,polyline,text,image,use'))return;
322-
var c=el.cloneNode(true),cs=getComputedStyle(el),inner=c.querySelectorAll('[data-sp]'),n;
323-
for(n=0;n<inner.length;n++)inner[n].removeAttribute('data-sp');
321+
/* Geometry that only defines, a sprite host's <defs> of <symbol>s, draws nothing and is no asset. */
322+
if(!el.querySelector(':is(path,rect,circle,ellipse,line,polygon,polyline,text,image,use):not(defs *,symbol *)'))return;
323+
var c=el.cloneNode(true),cs=getComputedStyle(el),inner,n;
324324
c.removeAttribute('data-sp');c.removeAttribute('style');c.removeAttribute('class');c.removeAttribute('preserveAspectRatio');
325325
if(!c.hasAttribute('xmlns'))c.setAttribute('xmlns','http://www.w3.org/2000/svg');
326326
if(!c.hasAttribute('fill'))c.setAttribute('fill',cs.fill);
@@ -332,15 +332,38 @@ function addSvg(i,el){
332332
for(j=0;j<src.length;j++){s=getComputedStyle(src[j]);ps=getComputedStyle(src[j].parentNode);
333333
if(!dst[j].hasAttribute('fill')&&s.fill!==ps.fill)dst[j].setAttribute('fill',s.fill);
334334
if(!dst[j].hasAttribute('stroke')&&s.stroke!==ps.stroke)dst[j].setAttribute('stroke',s.stroke);}
335+
/* A same-document <use> is inlined here and not before the pass above, which walks el and c in
336+
lockstep by index: a node inlined into c has no counterpart in el. A <symbol> becomes an inner
337+
<svg> carrying the use's own attributes (x, y, width, height, a fill) and the symbol's viewBox;
338+
any other target is cloned in place. The inlined geometry takes the root's fill and stroke
339+
written in above, not the sprite's own cascade. An external reference (file.svg#id) stays as
340+
it is, and data-sp goes last because the inlined nodes bring the document's in. The root
341+
adopts the symbol's viewBox only when that one use, unoffset and unsized, is the whole root: a
342+
root composing two symbols side by side keeps its own coordinate system, and its size then
343+
reads from its width and height.
344+
ponytail: 64 resolutions in all, then whatever <use> is still queued stays dangling; a symbol
345+
holding two references to itself ends as 64 wrappers, not two to the power of the depth. */
346+
var q=Array.prototype.slice.call(c.querySelectorAll('use')),left=64,seen=0,svb='',u,t,g,h;
347+
while(q.length&&left){u=q.shift();h=u.getAttribute('href')||u.getAttribute('xlink:href')||'';
348+
t=h.charAt(0)==='#'?document.getElementById(h.slice(1)):null;if(!t)continue;left--;seen++;
349+
g=document.createElementNS('http://www.w3.org/2000/svg','svg');
350+
for(n=0;n<u.attributes.length;n++)if(!/href$/.test(u.attributes[n].name))g.setAttribute(u.attributes[n].name,u.attributes[n].value);
351+
if(t.tagName==='symbol'){if(t.hasAttribute('viewBox'))g.setAttribute('viewBox',t.getAttribute('viewBox'));
352+
if(!(u.getAttribute('x')||u.getAttribute('y')||u.getAttribute('width')||u.getAttribute('height')))svb=t.getAttribute('viewBox')||'';
353+
t=t.cloneNode(true);while(t.firstChild)g.appendChild(t.firstChild);}
354+
else g.appendChild(t.cloneNode(true));
355+
u.parentNode.replaceChild(g,u);q.push.apply(q,g.querySelectorAll('use'));}
356+
if(seen===1&&svb&&!c.hasAttribute('viewBox'))c.setAttribute('viewBox',svb);
357+
inner=c.querySelectorAll('[data-sp]');for(n=0;n<inner.length;n++)inner[n].removeAttribute('data-sp');
335358
var svg=c.outerHTML.replace(/currentColor/gi,cs.color)
336359
.replace(/var\(\s*(--[\w-]+)[^)]*\)/g,function(m,p){return cs.getPropertyValue(p).replace(/^\s+|\s+$/g,'')||m;});
337360
/* One row per glyph and colour: the geometry key joins the file, and the colours after it keep
338361
a red and a black instance of the same glyph apart, so what a row shows is what it copies.
339362
ponytail: the root's computed colours; a colour that differs only in a child's var() collapses. */
340363
var k='svg:'+fnv(svgSignature(svg))+':'+fnv(cs.fill+'|'+cs.stroke+'|'+cs.color),a=byKey[k];
341-
if(!a){var vb=(el.getAttribute('viewBox')||'').split(/[\s,]+/);
364+
if(!a){var vb=(c.getAttribute('viewBox')||'').split(/[\s,]+/);
342365
a=byKey[k]={key:k,uri:'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg),via:'svg',mime:'image/svg+xml',
343-
chars:svg.length,w:+vb[2]||0,h:+vb[3]||0,alt:svgLabel(el),svg:svg,uses:[]};assets.push(a);}
366+
chars:svg.length,w:+vb[2]||+c.getAttribute('width')||0,h:+vb[3]||+c.getAttribute('height')||0,alt:svgLabel(el),svg:svg,uses:[]};assets.push(a);}
344367
if(a.uses.indexOf(i)<0)a.uses.push(i);nodes[i].img=true;}
345368
/* No board writes a title on its icons, so with no file to name one the name is a guess from
346369
context: the accessible name if there is one; else a class, when it is a word (logo, aiface)

0 commit comments

Comments
 (0)