301 lines
8.5 KiB
JavaScript
301 lines
8.5 KiB
JavaScript
import { parseSVG } from './parse.mjs';
|
|
import { tagSpecificNonPresentationalAttributes, urlPresentationalAttributes, commonColorPresentationalAttributes, markerAttributes } from './data/attributes.mjs';
|
|
import { styleTag, maskTags, reusableElementsWithPalette, defsTag, useTag } from './data/tags.mjs';
|
|
import { analyseTagError } from './analyse/error.mjs';
|
|
|
|
function analyseSVGStructure(svg, options = {}) {
|
|
const fixErrors = options.fixErrors;
|
|
let root = svg.$svg(":root").get(0);
|
|
if (root._parsed) {
|
|
svg.load(svg.toString());
|
|
root = svg.$svg(":root").get(0);
|
|
}
|
|
root._parsed = true;
|
|
const cheerio = svg.$svg;
|
|
const elements = /* @__PURE__ */ new Map();
|
|
const ids = /* @__PURE__ */ Object.create(null);
|
|
let links = [];
|
|
function addID(element, id) {
|
|
if (ids[id]) {
|
|
throw new Error(`Duplicate id "${id}"`);
|
|
}
|
|
element._id = id;
|
|
ids[id] = element._index;
|
|
return true;
|
|
}
|
|
function gotElementWithID(element, id, isMask) {
|
|
addID(element, id);
|
|
if (!element._belongsTo) {
|
|
element._belongsTo = [];
|
|
}
|
|
element._belongsTo.push({
|
|
id,
|
|
isMask,
|
|
indexes: /* @__PURE__ */ new Set([element._index])
|
|
});
|
|
return;
|
|
}
|
|
function gotReusableElement(item, isMask) {
|
|
const element = item.element;
|
|
const attribs = element.attribs;
|
|
const index2 = element._index;
|
|
const id = attribs["id"];
|
|
if (typeof id !== "string") {
|
|
const message = `Definition element ${analyseTagError(
|
|
element
|
|
)} does not have id`;
|
|
if (fixErrors) {
|
|
item.removeNode = true;
|
|
item.testChildren = false;
|
|
console.warn(message);
|
|
return false;
|
|
}
|
|
throw new Error(message);
|
|
}
|
|
if (ids[id] && fixErrors) {
|
|
console.warn(`Duplicate id "${id}"`);
|
|
item.removeNode = true;
|
|
item.testChildren = false;
|
|
return false;
|
|
}
|
|
element._reusableElement = {
|
|
id,
|
|
isMask,
|
|
index: index2
|
|
};
|
|
gotElementWithID(element, id, isMask);
|
|
return true;
|
|
}
|
|
function gotElementReference(item, id, usedAsMask) {
|
|
const element = item.element;
|
|
const usedByIndex = element._index;
|
|
const link = {
|
|
id,
|
|
usedByIndex,
|
|
usedAsMask
|
|
};
|
|
links.push(link);
|
|
if (!element._linksTo) {
|
|
element._linksTo = [];
|
|
}
|
|
element._linksTo.push(link);
|
|
}
|
|
let index = 0;
|
|
parseSVG(svg, (item) => {
|
|
const { tagName, parents } = item;
|
|
if (styleTag.has(tagName)) {
|
|
item.testChildren = false;
|
|
return;
|
|
}
|
|
const element = item.element;
|
|
const attribs = element.attribs;
|
|
index++;
|
|
element._index = index;
|
|
elements.set(index, element);
|
|
if (!parents.length) {
|
|
element._usedAsMask = false;
|
|
element._usedAsPaint = true;
|
|
return;
|
|
}
|
|
element._usedAsMask = false;
|
|
element._usedAsPaint = false;
|
|
const parentItem = parents[0];
|
|
const parentElement = parentItem.element;
|
|
if (maskTags.has(tagName)) {
|
|
if (!gotReusableElement(item, true)) {
|
|
return;
|
|
}
|
|
} else if (reusableElementsWithPalette.has(tagName)) {
|
|
if (!gotReusableElement(item, false)) {
|
|
return;
|
|
}
|
|
} else if (defsTag.has(parentItem.tagName)) {
|
|
if (!gotReusableElement(item, false)) {
|
|
return;
|
|
}
|
|
} else if (!defsTag.has(tagName)) {
|
|
element._usedAsMask = parentElement._usedAsMask;
|
|
element._usedAsPaint = parentElement._usedAsPaint;
|
|
element._parentElement = parentElement._index;
|
|
if (!parentElement._childElements) {
|
|
parentElement._childElements = [];
|
|
}
|
|
parentElement._childElements.push(index);
|
|
const parentReusableElement = parentElement._reusableElement;
|
|
if (parentReusableElement) {
|
|
if (element._reusableElement) {
|
|
throw new Error(
|
|
`Reusable element ${analyseTagError(
|
|
element
|
|
)} is inside another reusable element id="${parentReusableElement.id}"`
|
|
);
|
|
}
|
|
element._reusableElement = parentReusableElement;
|
|
}
|
|
const parentBelongsTo = parentElement._belongsTo;
|
|
if (parentBelongsTo) {
|
|
const list = element._belongsTo || (element._belongsTo = []);
|
|
parentBelongsTo.forEach((item2) => {
|
|
item2.indexes.add(index);
|
|
list.push(item2);
|
|
});
|
|
}
|
|
if (element._id === void 0) {
|
|
const id = attribs["id"];
|
|
if (typeof id === "string") {
|
|
if (ids[id] && fixErrors) {
|
|
console.warn(`Duplicate id "${id}"`);
|
|
cheerio(element).removeAttr("id");
|
|
} else {
|
|
gotElementWithID(element, id, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (tagSpecificNonPresentationalAttributes[tagName]?.has("href")) {
|
|
const href = attribs["href"] || attribs["xlink:href"];
|
|
if (typeof href === "string") {
|
|
if (href.slice(0, 1) !== "#") {
|
|
throw new Error(
|
|
`Invalid link in ${analyseTagError(element)}`
|
|
);
|
|
}
|
|
const id = href.slice(1);
|
|
gotElementReference(item, id, false);
|
|
}
|
|
}
|
|
Object.keys(attribs).forEach((attr) => {
|
|
let value = attribs[attr];
|
|
if (value.slice(0, 5).toLowerCase() !== "url(#") {
|
|
return;
|
|
}
|
|
value = value.slice(5);
|
|
if (value.slice(-1) !== ")") {
|
|
return;
|
|
}
|
|
const id = value.slice(0, value.length - 1).trim();
|
|
if (urlPresentationalAttributes.has(attr)) {
|
|
gotElementReference(item, id, attr !== "filter");
|
|
return;
|
|
}
|
|
if (commonColorPresentationalAttributes.has(attr) || markerAttributes.has(attr)) {
|
|
gotElementReference(item, id, false);
|
|
return;
|
|
}
|
|
});
|
|
});
|
|
links = links.filter((item) => {
|
|
const id = item.id;
|
|
if (ids[id]) {
|
|
return true;
|
|
}
|
|
function fix() {
|
|
const index2 = item.usedByIndex;
|
|
const element = elements.get(index2);
|
|
const tagName = element.tagName;
|
|
function remove() {
|
|
const $element = cheerio(element);
|
|
const parent = element.parent;
|
|
if (parent) {
|
|
if (parent._childElements) {
|
|
parent._childElements = parent._childElements.filter(
|
|
(num) => num !== index2
|
|
);
|
|
}
|
|
parent._belongsTo?.forEach((list) => {
|
|
list.indexes.delete(index2);
|
|
});
|
|
}
|
|
$element.remove();
|
|
}
|
|
if (element._linksTo) {
|
|
element._linksTo = element._linksTo.filter(
|
|
(item2) => item2.id !== id
|
|
);
|
|
}
|
|
if (!element.children.length) {
|
|
if (useTag.has(tagName)) {
|
|
remove();
|
|
return;
|
|
}
|
|
}
|
|
const matches = /* @__PURE__ */ new Set(["#" + id, "url(#" + id + ")"]);
|
|
const attribs = element.attribs;
|
|
for (const attr in attribs) {
|
|
if (matches.has(attribs[attr])) {
|
|
cheerio(element).removeAttr(attr);
|
|
}
|
|
}
|
|
}
|
|
const message = `Missing element with id="${id}"`;
|
|
if (fixErrors) {
|
|
fix();
|
|
console.warn(message);
|
|
} else {
|
|
throw new Error(message);
|
|
}
|
|
return false;
|
|
});
|
|
function hasChildItem(tree2, child, canThrow) {
|
|
const item = tree2.children.find(
|
|
(item2) => item2.index === child.index && item2.usedAsMask === child.usedAsMask
|
|
);
|
|
if (item && canThrow) {
|
|
throw new Error("Recursion");
|
|
}
|
|
return !!item;
|
|
}
|
|
const tree = {
|
|
index: 1,
|
|
usedAsMask: false,
|
|
children: []
|
|
};
|
|
function parseTreeItem(tree2, usedItems, inMask) {
|
|
const element = elements.get(tree2.index);
|
|
if (tree2.usedAsMask || inMask) {
|
|
element._usedAsMask = true;
|
|
inMask = true;
|
|
} else {
|
|
element._usedAsPaint = true;
|
|
}
|
|
usedItems = usedItems.slice(0);
|
|
usedItems.push(element._index);
|
|
element._childElements?.forEach((childIndex) => {
|
|
if (usedItems.includes(childIndex)) {
|
|
throw new Error("Recursion");
|
|
}
|
|
const childItem = {
|
|
index: childIndex,
|
|
usedAsMask: false,
|
|
children: [],
|
|
parent: tree2
|
|
};
|
|
tree2.children.push(childItem);
|
|
parseTreeItem(childItem, usedItems, inMask);
|
|
});
|
|
element._linksTo?.forEach((link) => {
|
|
const linkIndex = ids[link.id];
|
|
const usedAsMask = link.usedAsMask;
|
|
const childItem = {
|
|
index: linkIndex,
|
|
usedAsMask,
|
|
children: [],
|
|
parent: tree2
|
|
};
|
|
if (hasChildItem(tree2, childItem, false)) {
|
|
return;
|
|
}
|
|
tree2.children.push(childItem);
|
|
parseTreeItem(childItem, usedItems, inMask || usedAsMask);
|
|
});
|
|
}
|
|
parseTreeItem(tree, [0], false);
|
|
return {
|
|
elements,
|
|
ids,
|
|
links,
|
|
tree
|
|
};
|
|
}
|
|
|
|
export { analyseSVGStructure };
|