From ff711f080bb60c409661a5bc1a461836b6e42b4f Mon Sep 17 00:00:00 2001 From: jfh Date: Sun, 13 Sep 2020 12:08:05 +0200 Subject: [PATCH] remove externals in favor of npm packages --- package-lock.json | 8 +- package.json | 1 + src/editor/extensions/ext-imagelib/index.html | 4 - .../extensions/ext-imagelib/openclipart.html | 7 - .../extensions/ext-imagelib/openclipart.js | 3 +- .../ext-server_moinsave.js | 2 +- .../ext-server_opensave.js | 2 +- src/editor/svgedit.js | 4 +- src/external/canvg/canvg.js | 3055 ----------------- src/external/canvg/rgbcolor.js | 282 -- src/external/deparam/deparam.esm.js | 102 - src/external/dom-polyfill/dom-polyfill.js | 121 - src/external/jamilih/jml-es.js | 1932 ----------- src/external/load-stylesheets/index-es.js | 162 - .../stackblur-canvas/dist/stackblur-es.js | 580 ---- src/svgcanvas/svgcanvas.js | 2 +- 16 files changed, 11 insertions(+), 6256 deletions(-) delete mode 100644 src/external/canvg/canvg.js delete mode 100644 src/external/canvg/rgbcolor.js delete mode 100644 src/external/deparam/deparam.esm.js delete mode 100644 src/external/dom-polyfill/dom-polyfill.js delete mode 100644 src/external/jamilih/jml-es.js delete mode 100644 src/external/load-stylesheets/index-es.js delete mode 100644 src/external/stackblur-canvas/dist/stackblur-es.js diff --git a/package-lock.json b/package-lock.json index 6f3aa371..12691e92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3482,8 +3482,7 @@ "@types/raf": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz", - "integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==", - "optional": true + "integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==" }, "@types/range-parser": { "version": "1.2.3", @@ -6075,7 +6074,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.6.tgz", "integrity": "sha512-eFUy8R/4DgocR93LF8lr+YUxW4PYblUe/Q1gz2osk/cI5n8AsYdassvln0D9QPhLXQ6Lx7l8hwtT8FLvOn2Ihg==", - "optional": true, "requires": { "@babel/runtime": "^7.6.3", "@types/raf": "^3.4.0", @@ -17141,7 +17139,6 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "optional": true, "requires": { "performance-now": "^2.1.0" } @@ -18052,8 +18049,7 @@ "rgbcolor": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", - "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=", - "optional": true + "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=" }, "rimraf": { "version": "3.0.2", diff --git a/package.json b/package.json index c91c7dfc..48584a38 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ ], "dependencies": { "@babel/polyfill": "^7.11.5", + "canvg": "^3.0.6", "core-js": "^3.6.5", "jspdf": "^2.1.1", "regenerator-runtime": "^0.13.7", diff --git a/src/editor/extensions/ext-imagelib/index.html b/src/editor/extensions/ext-imagelib/index.html index 3e782dee..c3da0a8c 100644 --- a/src/editor/extensions/ext-imagelib/index.html +++ b/src/editor/extensions/ext-imagelib/index.html @@ -12,10 +12,6 @@ - - - - diff --git a/src/editor/extensions/ext-imagelib/openclipart.html b/src/editor/extensions/ext-imagelib/openclipart.html index 604a84e9..31b4a1a0 100644 --- a/src/editor/extensions/ext-imagelib/openclipart.html +++ b/src/editor/extensions/ext-imagelib/openclipart.html @@ -9,13 +9,6 @@ - - - - - - - diff --git a/src/editor/extensions/ext-imagelib/openclipart.js b/src/editor/extensions/ext-imagelib/openclipart.js index d033525c..ff686ab5 100644 --- a/src/editor/extensions/ext-imagelib/openclipart.js +++ b/src/editor/extensions/ext-imagelib/openclipart.js @@ -1,4 +1,5 @@ -import {jml, body, nbsp} from '../../../external/jamilih/jml-es.js'; +// eslint-disable-next-line node/no-unpublished-import +import {jml, body, nbsp} from 'jamilih'; import $ from '../../../external/query-result/esm/index.js'; import {manipulation} from '../../../external/qr-manipulation/dist/index-es.js'; diff --git a/src/editor/extensions/ext-server_moinsave/ext-server_moinsave.js b/src/editor/extensions/ext-server_moinsave/ext-server_moinsave.js index e2587cee..5fce99e8 100644 --- a/src/editor/extensions/ext-server_moinsave/ext-server_moinsave.js +++ b/src/editor/extensions/ext-server_moinsave/ext-server_moinsave.js @@ -7,7 +7,7 @@ * adopted for moinmoins item storage. It sends in one post png and svg data * (I agree to dual license my work to additional GPLv2 or later) */ -import {canvg} from '../../../external/canvg/canvg.js'; +import {Canvg as canvg} from 'canvg'; const loadExtensionTranslation = async function (lang) { let translationModule; diff --git a/src/editor/extensions/ext-server_opensave/ext-server_opensave.js b/src/editor/extensions/ext-server_opensave/ext-server_opensave.js index 42a7a27e..89e27862 100644 --- a/src/editor/extensions/ext-server_opensave/ext-server_opensave.js +++ b/src/editor/extensions/ext-server_opensave/ext-server_opensave.js @@ -6,7 +6,7 @@ * @copyright 2010 Alexis Deveria * */ -import {canvg} from '../../../external/canvg/canvg.js'; +import {Canvg as canvg} from 'canvg'; const loadExtensionTranslation = async function (lang) { let translationModule; diff --git a/src/editor/svgedit.js b/src/editor/svgedit.js index ce9532ff..a1e3e0e8 100644 --- a/src/editor/svgedit.js +++ b/src/editor/svgedit.js @@ -15,6 +15,9 @@ * @borrows module:locale.setStrings as setStrings */ +// eslint-disable-next-line node/no-unpublished-import +import deparam from 'deparam'; + import './touch.js'; import {NS} from '../common/namespaces.js'; import {isWebkit, isChrome, isGecko, isIE, isMac, isTouch} from '../common/browser.js'; @@ -26,7 +29,6 @@ import {getTypeMap, convertUnit, isValidUnit} from '../common/units.js'; import { hasCustomHandler, getCustomHandler, injectExtendedContextMenuItemsIntoDom } from './contextmenu.js'; -import deparam from '../external/deparam/deparam.esm.js'; import SvgCanvas from '../svgcanvas/svgcanvas.js'; import Layer from '../common/layer.js'; diff --git a/src/external/canvg/canvg.js b/src/external/canvg/canvg.js deleted file mode 100644 index b1117328..00000000 --- a/src/external/canvg/canvg.js +++ /dev/null @@ -1,3055 +0,0 @@ -/* eslint-disable new-cap, class-methods-use-this, jsdoc/require-jsdoc */ -// Todo: Compare with latest canvg (add any improvements of ours) and add full JSDocs (denoting links to standard APIs and which are custom): https://github.com/canvg/canvg -/** - * Javascript SVG parser and renderer on Canvas. - * @file canvg.js - * @module canvg - * @license MIT - * @author Gabe Lerner - * @see https://github.com/canvg/canvg - */ - -import RGBColor from './rgbcolor.js'; -import {canvasRGBA} from '../../external/stackblur-canvas/dist/stackblur-es.js'; - -/** - * Whether a value is `null` or `undefined`. - * @param {any} val - * @returns {boolean} - */ -const isNullish = (val) => { - return val === null || val === undefined; -}; - -/** -* @callback module:canvg.ForceRedraw -* @returns {boolean} -*/ - -/** -* @typedef {PlainObject} module:canvg.CanvgOptions -* @property {boolean} ignoreMouse true => ignore mouse events -* @property {boolean} ignoreAnimation true => ignore animations -* @property {boolean} ignoreDimensions true => does not try to resize canvas -* @property {boolean} ignoreClear true => does not clear canvas -* @property {Integer} offsetX int => draws at a x offset -* @property {Integer} offsetY int => draws at a y offset -* @property {Integer} scaleWidth int => scales horizontally to width -* @property {Integer} scaleHeight int => scales vertically to height -* @property {module:canvg.ForceRedraw} forceRedraw function => will call the function on every frame, if it returns true, will redraw -* @property {boolean} log Adds log function -* @property {boolean} useCORS Whether to set CORS `crossOrigin` for the image to `Anonymous` -*/ - -/** -* If called with no arguments, it will replace all `` elements on the page -* with `` elements. -* @function module:canvg.canvg -* @param {HTMLCanvasElement|string} target canvas element or the id of a canvas element -* @param {string|XMLDocument} s - svg string, url to svg file, or xml document -* @param {module:canvg.CanvgOptions} [opts] Optional hash of options -* @returns {Promise} All the function after the first render is completed with dom -*/ -export const canvg = function (target, s, opts) { - // no parameters - if (isNullish(target) && isNullish(s) && isNullish(opts)) { - const svgTags = document.querySelectorAll('svg'); - return Promise.all([...svgTags].map((svgTag) => { - const c = document.createElement('canvas'); - c.width = svgTag.clientWidth; - c.height = svgTag.clientHeight; - svgTag.before(c); - svgTag.remove(); - const div = document.createElement('div'); - div.append(svgTag); - return canvg(c, div.innerHTML); - })); - } - - if (typeof target === 'string') { - target = document.getElementById(target); - } - - // store class on canvas - if (!isNullish(target.svg)) target.svg.stop(); - const svg = build(opts || {}); - // on i.e. 8 for flash canvas, we can't assign the property so check for it - if (!(target.childNodes.length === 1 && target.childNodes[0].nodeName === 'OBJECT')) { - target.svg = svg; - } - - const ctx = target.getContext('2d'); - if (typeof s.documentElement !== 'undefined') { - // load from xml doc - return svg.loadXmlDoc(ctx, s); - } - if (s.substr(0, 1) === '<') { - // load from xml string - return svg.loadXml(ctx, s); - } - // load from url - return svg.load(ctx, s); -}; - -/* eslint-disable jsdoc/check-types */ -/** -* @param {module:canvg.CanvgOptions} opts -* @returns {object} -* @todo Flesh out exactly what object is returned here (after updating to latest and reincluding our changes here and those of StackBlur) -*/ -function build (opts) { - /* eslint-enable jsdoc/check-types */ - const svg = {opts}; - - svg.FRAMERATE = 30; - svg.MAX_VIRTUAL_PIXELS = 30000; - - svg.log = function (msg) { /* */ }; - if (svg.opts.log === true && typeof console !== 'undefined') { - svg.log = function (msg) { console.log(msg); }; // eslint-disable-line no-console - } - - // globals - svg.init = function (ctx) { - let uniqueId = 0; - svg.UniqueId = function () { - uniqueId++; - return 'canvg' + uniqueId; - }; - svg.Definitions = {}; - svg.Styles = {}; - svg.Animations = []; - svg.Images = []; - svg.ctx = ctx; - svg.ViewPort = { - viewPorts: [], - Clear () { this.viewPorts = []; }, - SetCurrent (width, height) { this.viewPorts.push({width, height}); }, - RemoveCurrent () { this.viewPorts.pop(); }, - Current () { return this.viewPorts[this.viewPorts.length - 1]; }, - width () { return this.Current().width; }, - height () { return this.Current().height; }, - ComputeSize (d) { - if (!isNullish(d) && typeof d === 'number') return d; - if (d === 'x') return this.width(); - if (d === 'y') return this.height(); - return Math.sqrt( - (this.width() ** 2) + (this.height() ** 2) - ) / Math.sqrt(2); - } - }; - }; - svg.init(); - - // images loaded - svg.ImagesLoaded = function () { - return svg.Images.every((img) => img.loaded); - }; - - // trim - svg.trim = function (s) { - return s.replace(/^\s+|\s+$/g, ''); - }; - - // compress spaces - svg.compressSpaces = function (s) { - return s.replace(/\s+/gm, ' '); - }; - - // ajax - // Todo: Replace with `fetch` and polyfill - svg.ajax = function (url, asynch) { - const AJAX = window.XMLHttpRequest - ? new XMLHttpRequest() - : new window.ActiveXObject('Microsoft.XMLHTTP'); - if (asynch) { - return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new - const req = AJAX.open('GET', url, true); - req.addEventListener('load', () => { - resolve(AJAX.responseText); - }); - AJAX.send(null); - }); - } - - AJAX.open('GET', url, false); - AJAX.send(null); - return AJAX.responseText; - }; - - // parse xml - svg.parseXml = function (xml) { - if (window.DOMParser) { - const parser = new DOMParser(); - return parser.parseFromString(xml, 'text/xml'); - } - xml = xml.replace(/]*>/, ''); - const xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM'); - xmlDoc.async = 'false'; - xmlDoc.loadXML(xml); - return xmlDoc; - }; - - // text extensions - // get the text baseline - const textBaselineMapping = { - baseline: 'alphabetic', - 'before-edge': 'top', - 'text-before-edge': 'top', - middle: 'middle', - central: 'middle', - 'after-edge': 'bottom', - 'text-after-edge': 'bottom', - ideographic: 'ideographic', - alphabetic: 'alphabetic', - hanging: 'hanging', - mathematical: 'alphabetic' - }; - - svg.Property = class Property { - constructor (name, value) { - this.name = name; - this.value = value; - } - - getValue () { - return this.value; - } - - hasValue () { - return (!isNullish(this.value) && this.value !== ''); - } - - // return the numerical value of the property - numValue () { - if (!this.hasValue()) return 0; - - let n = Number.parseFloat(this.value); - if (String(this.value).endsWith('%')) { - n /= 100.0; - } - return n; - } - - valueOrDefault (def) { - if (this.hasValue()) return this.value; - return def; - } - - numValueOrDefault (def) { - if (this.hasValue()) return this.numValue(); - return def; - } - - // color extensions - // augment the current color value with the opacity - addOpacity (opacityProp) { - let newValue = this.value; - if (!isNullish(opacityProp.value) && opacityProp.value !== '' && typeof this.value === 'string') { // can only add opacity to colors, not patterns - const color = new RGBColor(this.value); - if (color.ok) { - newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; - } - } - return new svg.Property(this.name, newValue); - } - - // definition extensions - // get the definition from the definitions table - getDefinition () { - let name = this.value.match(/#([^)'"]+)/); - if (name) { name = name[1]; } - if (!name) { name = this.value; } - return svg.Definitions[name]; - } - - isUrlDefinition () { - return this.value.startsWith('url('); - } - - getFillStyleDefinition (e, opacityProp) { - let def = this.getDefinition(); - - // gradient - if (!isNullish(def) && def.createGradient) { - return def.createGradient(svg.ctx, e, opacityProp); - } - - // pattern - if (!isNullish(def) && def.createPattern) { - if (def.getHrefAttribute().hasValue()) { - const pt = def.attribute('patternTransform'); - def = def.getHrefAttribute().getDefinition(); - if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; } - } - return def.createPattern(svg.ctx, e); - } - - return null; - } - - // length extensions - getDPI (viewPort) { - return 96.0; // TODO: compute? - } - - getEM (viewPort) { - let em = 12; - - const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); - if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); - - return em; - } - - getUnits () { - return String(this.value).replace(/[\d.-]/g, ''); - } - - // get the length as pixels - toPixels (viewPort, processPercent) { - if (!this.hasValue()) return 0; - const s = String(this.value); - if (s.endsWith('em')) return this.numValue() * this.getEM(viewPort); - if (s.endsWith('ex')) return this.numValue() * this.getEM(viewPort) / 2.0; - if (s.endsWith('px')) return this.numValue(); - if (s.endsWith('pt')) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); - if (s.endsWith('pc')) return this.numValue() * 15; - if (s.endsWith('cm')) return this.numValue() * this.getDPI(viewPort) / 2.54; - if (s.endsWith('mm')) return this.numValue() * this.getDPI(viewPort) / 25.4; - if (s.endsWith('in')) return this.numValue() * this.getDPI(viewPort); - if (s.endsWith('%')) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); - const n = this.numValue(); - if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); - return n; - } - - // time extensions - // get the time as milliseconds - toMilliseconds () { - if (!this.hasValue()) return 0; - const s = String(this.value); - if (s.endsWith('ms')) return this.numValue(); - if (s.endsWith('s')) return this.numValue() * 1000; - return this.numValue(); - } - - // angle extensions - // get the angle as radians - toRadians () { - if (!this.hasValue()) return 0; - const s = String(this.value); - if (s.endsWith('deg')) return this.numValue() * (Math.PI / 180.0); - if (s.endsWith('grad')) return this.numValue() * (Math.PI / 200.0); - if (s.endsWith('rad')) return this.numValue(); - return this.numValue() * (Math.PI / 180.0); - } - - toTextBaseline () { - if (!this.hasValue()) return null; - return textBaselineMapping[this.value]; - } - }; - - // fonts - svg.Font = { - Styles: 'normal|italic|oblique|inherit', - Variants: 'normal|small-caps|inherit', - Weights: 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit', - - CreateFont (fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { - const f = !isNullish(inherit) ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); - return { - fontFamily: fontFamily || f.fontFamily, - fontSize: fontSize || f.fontSize, - fontStyle: fontStyle || f.fontStyle, - fontWeight: fontWeight || f.fontWeight, - fontVariant: fontVariant || f.fontVariant, - toString () { - return [ - this.fontStyle, this.fontVariant, this.fontWeight, - this.fontSize, this.fontFamily - ].join(' '); - } - }; - }, - - Parse (s) { - const f = {}; - const ds = svg.trim(svg.compressSpaces(s || '')).split(' '); - const set = { - fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false - }; - let ff = ''; - ds.forEach((d) => { - if (!set.fontStyle && this.Styles.includes(d)) { - if (d !== 'inherit') { - f.fontStyle = d; - } - set.fontStyle = true; - } else if (!set.fontVariant && this.Variants.includes(d)) { - if (d !== 'inherit') { - f.fontVariant = d; - } - set.fontStyle = set.fontVariant = true; - } else if (!set.fontWeight && this.Weights.includes(d)) { - if (d !== 'inherit') { - f.fontWeight = d; - } - set.fontStyle = set.fontVariant = set.fontWeight = true; - } else if (!set.fontSize) { - if (d !== 'inherit') { - f.fontSize = d.split('/')[0]; - } - set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true; - } else if (d !== 'inherit') { - ff += d; - } - }); - if (ff !== '') { f.fontFamily = ff; } - return f; - } - }; - - // points and paths - svg.ToNumberArray = function (s) { - const a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); - return a.map((_a) => Number.parseFloat(_a)); - }; - svg.Point = class { - constructor (x, y) { - this.x = x; - this.y = y; - } - - angleTo (p) { - return Math.atan2(p.y - this.y, p.x - this.x); - } - - applyTransform (v) { - const xp = this.x * v[0] + this.y * v[2] + v[4]; - const yp = this.x * v[1] + this.y * v[3] + v[5]; - this.x = xp; - this.y = yp; - } - }; - - svg.CreatePoint = function (s) { - const a = svg.ToNumberArray(s); - return new svg.Point(a[0], a[1]); - }; - svg.CreatePath = function (s) { - const a = svg.ToNumberArray(s); - const path = []; - for (let i = 0; i < a.length; i += 2) { - path.push(new svg.Point(a[i], a[i + 1])); - } - return path; - }; - - // bounding box - svg.BoundingBox = class { - constructor (x1, y1, x2, y2) { // pass in initial points if you want - this.x1 = Number.NaN; - this.y1 = Number.NaN; - this.x2 = Number.NaN; - this.y2 = Number.NaN; - this.addPoint(x1, y1); - this.addPoint(x2, y2); - } - - x () { return this.x1; } - y () { return this.y1; } - width () { return this.x2 - this.x1; } - height () { return this.y2 - this.y1; } - - addPoint (x, y) { - if (!isNullish(x)) { - if (isNaN(this.x1) || isNaN(this.x2)) { - this.x1 = x; - this.x2 = x; - } - if (x < this.x1) this.x1 = x; - if (x > this.x2) this.x2 = x; - } - - if (!isNullish(y)) { - if (isNaN(this.y1) || isNaN(this.y2)) { - this.y1 = y; - this.y2 = y; - } - if (y < this.y1) this.y1 = y; - if (y > this.y2) this.y2 = y; - } - } - addX (x) { this.addPoint(x, null); } - addY (y) { this.addPoint(null, y); } - - addBoundingBox (bb) { - this.addPoint(bb.x1, bb.y1); - this.addPoint(bb.x2, bb.y2); - } - - addQuadraticCurve (p0x, p0y, p1x, p1y, p2x, p2y) { - const cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) - const cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) - const cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) - const cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) - this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); - } - - addBezierCurve (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { - // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html - const p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; - this.addPoint(p0[0], p0[1]); - this.addPoint(p3[0], p3[1]); - - for (let i = 0; i <= 1; i++) { - const f = function (t) { - return ((1 - t) ** 3) * p0[i] + - 3 * ((1 - t) ** 2) * t * p1[i] + - 3 * (1 - t) * (t ** 2) * p2[i] + - (t ** 3) * p3[i]; - }; - - const b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; - const a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; - const c = 3 * p1[i] - 3 * p0[i]; - - if (a === 0) { - if (b === 0) continue; - const t = -c / b; - if (t > 0 && t < 1) { - if (i === 0) this.addX(f(t)); - if (i === 1) this.addY(f(t)); - } - continue; - } - - const b2ac = (b ** 2) - 4 * c * a; - if (b2ac < 0) continue; - const t1 = (-b + Math.sqrt(b2ac)) / (2 * a); - if (t1 > 0 && t1 < 1) { - if (i === 0) this.addX(f(t1)); - if (i === 1) this.addY(f(t1)); - } - const t2 = (-b - Math.sqrt(b2ac)) / (2 * a); - if (t2 > 0 && t2 < 1) { - if (i === 0) this.addX(f(t2)); - if (i === 1) this.addY(f(t2)); - } - } - } - - isPointInBox (x, y) { - return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); - } - }; - - // transforms - svg.Transform = class { - constructor (v) { - this.Type = { - translate: class { - constructor (s) { - this.p = svg.CreatePoint(s); - this.apply = function (ctx) { - ctx.translate(this.p.x || 0.0, this.p.y || 0.0); - }; - this.unapply = function (ctx) { - ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0); - }; - this.applyToPoint = function (p) { - p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); - }; - } - }, - rotate: class { - constructor (s) { - const a = svg.ToNumberArray(s); - this.angle = new svg.Property('angle', a[0]); - this.cx = a[1] || 0; - this.cy = a[2] || 0; - this.apply = function (ctx) { - ctx.translate(this.cx, this.cy); - ctx.rotate(this.angle.toRadians()); - ctx.translate(-this.cx, -this.cy); - }; - this.unapply = function (ctx) { - ctx.translate(this.cx, this.cy); - ctx.rotate(-1.0 * this.angle.toRadians()); - ctx.translate(-this.cx, -this.cy); - }; - this.applyToPoint = function (p) { - const _a = this.angle.toRadians(); - p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); - p.applyTransform([Math.cos(_a), Math.sin(_a), -Math.sin(_a), Math.cos(_a), 0, 0]); - p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); - }; - } - }, - scale: class { - constructor (s) { - this.p = svg.CreatePoint(s); - this.apply = function (ctx) { - ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); - }; - this.unapply = function (ctx) { - ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0); - }; - this.applyToPoint = function (p) { - p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); - }; - } - }, - matrix: class { - constructor (s) { - this.m = svg.ToNumberArray(s); - this.apply = function (ctx) { - ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); - }; - this.applyToPoint = function (p) { - p.applyTransform(this.m); - }; - } - } - }; - Object.assign(this.Type, { - SkewBase: class extends this.Type.matrix { - constructor (s) { - super(s); - this.angle = new svg.Property('angle', s); - } - } - }); - Object.assign(this.Type, { - skewX: class extends this.Type.SkewBase { - constructor (s) { - super(s); - this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0]; - } - }, - skewY: class extends this.Type.SkewBase { - constructor (s) { - super(s); - this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0]; - } - } - }); - - const data = svg.trim(svg.compressSpaces(v)).replace( - /\)([a-zA-Z])/g, ') $1' - ).replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); - this.transforms = data.map((d) => { - const type = svg.trim(d.split('(')[0]); - const s = d.split('(')[1].replace(')', ''); - const transform = new this.Type[type](s); - transform.type = type; - return transform; - }); - } - - apply (ctx) { - this.transforms.forEach((transform) => { - transform.apply(ctx); - }); - } - - unapply (ctx) { - for (let i = this.transforms.length - 1; i >= 0; i--) { - this.transforms[i].unapply(ctx); - } - } - - applyToPoint (p) { - this.transforms.forEach((transform) => { - transform.applyToPoint(p); - }); - } - }; - - // aspect ratio - svg.AspectRatio = function (ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) { - // aspect ratio - https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute - aspectRatio = svg.compressSpaces(aspectRatio); - aspectRatio = aspectRatio.replace(/^defer\s/, ''); // ignore defer - const align = aspectRatio.split(' ')[0] || 'xMidYMid'; - const meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; - - // calculate scale - const scaleX = width / desiredWidth; - const scaleY = height / desiredHeight; - const scaleMin = Math.min(scaleX, scaleY); - const scaleMax = Math.max(scaleX, scaleY); - if (meetOrSlice === 'meet') { desiredWidth *= scaleMin; desiredHeight *= scaleMin; } - if (meetOrSlice === 'slice') { desiredWidth *= scaleMax; desiredHeight *= scaleMax; } - - refX = new svg.Property('refX', refX); - refY = new svg.Property('refY', refY); - if (refX.hasValue() && refY.hasValue()) { - ctx.translate(-scaleMin * refX.toPixels('x'), -scaleMin * refY.toPixels('y')); - } else { - // align - if (align.startsWith('xMid') && - ((meetOrSlice === 'meet' && scaleMin === scaleY) || (meetOrSlice === 'slice' && scaleMax === scaleY))) { - ctx.translate(width / 2.0 - desiredWidth / 2.0, 0); - } - if (align.endsWith('YMid') && - ((meetOrSlice === 'meet' && scaleMin === scaleX) || (meetOrSlice === 'slice' && scaleMax === scaleX))) { - ctx.translate(0, height / 2.0 - desiredHeight / 2.0); - } - if (align.startsWith('xMax') && - ((meetOrSlice === 'meet' && scaleMin === scaleY) || (meetOrSlice === 'slice' && scaleMax === scaleY))) { - ctx.translate(width - desiredWidth, 0); - } - if (align.endsWith('YMax') && - ((meetOrSlice === 'meet' && scaleMin === scaleX) || - (meetOrSlice === 'slice' && scaleMax === scaleX) - ) - ) { - ctx.translate(0, height - desiredHeight); - } - } - - // scale - if (align === 'none') ctx.scale(scaleX, scaleY); - else if (meetOrSlice === 'meet') ctx.scale(scaleMin, scaleMin); - else if (meetOrSlice === 'slice') ctx.scale(scaleMax, scaleMax); - - // translate - ctx.translate(isNullish(minX) ? 0 : -minX, isNullish(minY) ? 0 : -minY); - }; - - // elements - svg.Element = {}; - - svg.EmptyProperty = new svg.Property('EMPTY', ''); - - svg.Element.ElementBase = class { - constructor (node) { - // Argument from inheriting class - this.captureTextNodes = arguments[1]; // eslint-disable-line prefer-rest-params - this.attributes = {}; - this.styles = {}; - this.children = []; - if (!isNullish(node) && node.nodeType === 1) { // ELEMENT_NODE - // add children - [...node.childNodes].forEach((childNode) => { - if (childNode.nodeType === 1) { - this.addChild(childNode, true); // ELEMENT_NODE - } - if (this.captureTextNodes && ( - childNode.nodeType === 3 || childNode.nodeType === 4 - )) { - const text = childNode.nodeValue || childNode.text || ''; - if (svg.trim(svg.compressSpaces(text)) !== '') { - this.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE - } - } - }); - - // add attributes - [...node.attributes].forEach(({nodeName, nodeValue}) => { - this.attributes[nodeName] = new svg.Property( - nodeName, - nodeValue - ); - }); - // add tag styles - let styles = svg.Styles[node.nodeName]; - if (!isNullish(styles)) { - Object.entries(styles).forEach(([name, styleValue]) => { - this.styles[name] = styleValue; - }); - } - - // add class styles - if (this.attribute('class').hasValue()) { - const classes = svg.compressSpaces(this.attribute('class').value).split(' '); - classes.forEach((clss) => { - styles = svg.Styles['.' + clss]; - if (!isNullish(styles)) { - Object.entries(styles).forEach(([name, styleValue]) => { - this.styles[name] = styleValue; - }); - } - styles = svg.Styles[node.nodeName + '.' + clss]; - if (!isNullish(styles)) { - Object.entries(styles).forEach(([name, styleValue]) => { - this.styles[name] = styleValue; - }); - } - }); - } - - // add id styles - if (this.attribute('id').hasValue()) { - const _styles = svg.Styles['#' + this.attribute('id').value]; - if (!isNullish(_styles)) { - Object.entries(_styles).forEach(([name, styleValue]) => { - this.styles[name] = styleValue; - }); - } - } - - // add inline styles - if (this.attribute('style').hasValue()) { - const _styles = this.attribute('style').value.split(';'); - _styles.forEach((style) => { - if (svg.trim(style) !== '') { - let {name, value} = style.split(':'); - name = svg.trim(name); - value = svg.trim(value); - this.styles[name] = new svg.Property(name, value); - } - }); - } - - // add id - if (this.attribute('id').hasValue()) { - if (isNullish(svg.Definitions[this.attribute('id').value])) { - svg.Definitions[this.attribute('id').value] = this; - } - } - } - } - - // get or create attribute - attribute (name, createIfNotExists) { - let a = this.attributes[name]; - if (!isNullish(a)) return a; - - if (createIfNotExists === true) { a = new svg.Property(name, ''); this.attributes[name] = a; } - return a || svg.EmptyProperty; - } - - getHrefAttribute () { - for (const a in this.attributes) { - if (a.endsWith(':href')) { - return this.attributes[a]; - } - } - return svg.EmptyProperty; - } - - // get or create style, crawls up node tree - style (name, createIfNotExists, skipAncestors) { - let s = this.styles[name]; - if (!isNullish(s)) return s; - - const a = this.attribute(name); - if (!isNullish(a) && a.hasValue()) { - this.styles[name] = a; // move up to me to cache - return a; - } - - if (skipAncestors !== true) { - const p = this.parent; - if (!isNullish(p)) { - const ps = p.style(name); - if (!isNullish(ps) && ps.hasValue()) { - return ps; - } - } - } - - if (createIfNotExists === true) { s = new svg.Property(name, ''); this.styles[name] = s; } - return s || svg.EmptyProperty; - } - - // base render - render (ctx) { - // don't render display=none - if (this.style('display').value === 'none') return; - - // don't render visibility=hidden - if (this.style('visibility').value === 'hidden') return; - - ctx.save(); - if (this.attribute('mask').hasValue()) { // mask - const mask = this.attribute('mask').getDefinition(); - if (!isNullish(mask)) mask.apply(ctx, this); - } else if (this.style('filter').hasValue()) { // filter - const filter = this.style('filter').getDefinition(); - if (!isNullish(filter)) filter.apply(ctx, this); - } else { - this.setContext(ctx); - this.renderChildren(ctx); - this.clearContext(ctx); - } - ctx.restore(); - } - - // base set context - setContext (ctx) { - // OVERRIDE ME! - } - - // base clear context - clearContext (ctx) { - // OVERRIDE ME! - } - - // base render children - renderChildren (ctx) { - this.children.forEach((child) => { - child.render(ctx); - }); - } - - addChild (childNode, create) { - const child = create - ? svg.CreateElement(childNode) - : childNode; - child.parent = this; - if (child.type !== 'title') { this.children.push(child); } - } - }; - - svg.Element.RenderedElementBase = class extends svg.Element.ElementBase { - setContext (ctx) { - // fill - if (this.style('fill').isUrlDefinition()) { - const fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity')); - if (!isNullish(fs)) ctx.fillStyle = fs; - } else if (this.style('fill').hasValue()) { - const fillStyle = this.style('fill'); - if (fillStyle.value === 'currentColor') fillStyle.value = this.style('color').value; - ctx.fillStyle = (fillStyle.value === 'none' ? 'rgba(0,0,0,0)' : fillStyle.value); - } - if (this.style('fill-opacity').hasValue()) { - let fillStyle = new svg.Property('fill', ctx.fillStyle); - fillStyle = fillStyle.addOpacity(this.style('fill-opacity')); - ctx.fillStyle = fillStyle.value; - } - - // stroke - if (this.style('stroke').isUrlDefinition()) { - const fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity')); - if (!isNullish(fs)) ctx.strokeStyle = fs; - } else if (this.style('stroke').hasValue()) { - const strokeStyle = this.style('stroke'); - if (strokeStyle.value === 'currentColor') strokeStyle.value = this.style('color').value; - ctx.strokeStyle = (strokeStyle.value === 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value); - } - if (this.style('stroke-opacity').hasValue()) { - let strokeStyle = new svg.Property('stroke', ctx.strokeStyle); - strokeStyle = strokeStyle.addOpacity(this.style('stroke-opacity')); - ctx.strokeStyle = strokeStyle.value; - } - if (this.style('stroke-width').hasValue()) { - const newLineWidth = this.style('stroke-width').toPixels(); - ctx.lineWidth = newLineWidth === 0 ? 0.001 : newLineWidth; // browsers don't respect 0 - } - if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; - if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; - if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; - if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value !== 'none') { - const gaps = svg.ToNumberArray(this.style('stroke-dasharray').value); - if (typeof ctx.setLineDash !== 'undefined') { - ctx.setLineDash(gaps); - } else if (typeof ctx.webkitLineDash !== 'undefined') { - ctx.webkitLineDash = gaps; - } else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) { - ctx.mozDash = gaps; - } - - const offset = this.style('stroke-dashoffset').numValueOrDefault(1); - if (typeof ctx.lineDashOffset !== 'undefined') { - ctx.lineDashOffset = offset; - } else if (typeof ctx.webkitLineDashOffset !== 'undefined') { - ctx.webkitLineDashOffset = offset; - } else if (typeof ctx.mozDashOffset !== 'undefined') { - ctx.mozDashOffset = offset; - } - } - - // font - if (typeof ctx.font !== 'undefined') { - ctx.font = svg.Font.CreateFont( - this.style('font-style').value, - this.style('font-variant').value, - this.style('font-weight').value, - this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '', - this.style('font-family').value - ).toString(); - } - - // transform - if (this.attribute('transform').hasValue()) { - const transform = new svg.Transform(this.attribute('transform').value); - transform.apply(ctx); - } - - // clip - if (this.style('clip-path', false, true).hasValue()) { - const clip = this.style('clip-path', false, true).getDefinition(); - if (!isNullish(clip)) clip.apply(ctx); - } - - // opacity - if (this.style('opacity').hasValue()) { - ctx.globalAlpha = this.style('opacity').numValue(); - } - } - }; - - svg.Element.PathElementBase = class extends svg.Element.RenderedElementBase { - path (ctx) { - if (!isNullish(ctx)) ctx.beginPath(); - return new svg.BoundingBox(); - } - - renderChildren (ctx) { - this.path(ctx); - svg.Mouse.checkPath(this, ctx); - if (ctx.fillStyle !== '') { - if (this.style('fill-rule').valueOrDefault('inherit') !== 'inherit') { - ctx.fill(this.style('fill-rule').value); - } else { - ctx.fill(); - } - } - if (ctx.strokeStyle !== '') ctx.stroke(); - - const markers = this.getMarkers(); - if (!isNullish(markers)) { - if (this.style('marker-start').isUrlDefinition()) { - const marker = this.style('marker-start').getDefinition(); - marker.render(ctx, markers[0][0], markers[0][1]); - } - if (this.style('marker-mid').isUrlDefinition()) { - const marker = this.style('marker-mid').getDefinition(); - for (let i = 1; i < markers.length - 1; i++) { - marker.render(ctx, markers[i][0], markers[i][1]); - } - } - if (this.style('marker-end').isUrlDefinition()) { - const marker = this.style('marker-end').getDefinition(); - marker.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]); - } - } - } - - getBoundingBox () { - return this.path(); - } - - getMarkers () { - return null; - } - }; - - // svg element - svg.Element.svg = class extends svg.Element.RenderedElementBase { - clearContext (ctx) { - super.clearContext(ctx); - svg.ViewPort.RemoveCurrent(); - } - - setContext (ctx) { - // initial values and defaults - ctx.strokeStyle = 'rgba(0,0,0,0)'; - ctx.lineCap = 'butt'; - ctx.lineJoin = 'miter'; - ctx.miterLimit = 4; - if (typeof ctx.font !== 'undefined' && typeof window.getComputedStyle !== 'undefined') { - ctx.font = window.getComputedStyle(ctx.canvas).getPropertyValue('font'); - } - - super.setContext(ctx); - - // create new view port - if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0; - if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0; - ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y')); - - let width = svg.ViewPort.width(); - let height = svg.ViewPort.height(); - - if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%'; - if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%'; - if (typeof this.root === 'undefined') { - width = this.attribute('width').toPixels('x'); - height = this.attribute('height').toPixels('y'); - - let x = 0; - let y = 0; - if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { - x = -this.attribute('refX').toPixels('x'); - y = -this.attribute('refY').toPixels('y'); - } - - if (this.attribute('overflow').valueOrDefault('hidden') !== 'visible') { - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(width, y); - ctx.lineTo(width, height); - ctx.lineTo(x, height); - ctx.closePath(); - ctx.clip(); - } - } - svg.ViewPort.SetCurrent(width, height); - - // viewbox - if (this.attribute('viewBox').hasValue()) { - const viewBox = svg.ToNumberArray(this.attribute('viewBox').value); - const minX = viewBox[0]; - const minY = viewBox[1]; - width = viewBox[2]; - height = viewBox[3]; - - svg.AspectRatio( - ctx, - this.attribute('preserveAspectRatio').value, - svg.ViewPort.width(), - width, - svg.ViewPort.height(), - height, - minX, - minY, - this.attribute('refX').value, - this.attribute('refY').value - ); - - svg.ViewPort.RemoveCurrent(); - svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); - } - } - }; - - // rect element - svg.Element.rect = class extends svg.Element.PathElementBase { - path (ctx) { - const x = this.attribute('x').toPixels('x'); - const y = this.attribute('y').toPixels('y'); - const width = this.attribute('width').toPixels('x'); - const height = this.attribute('height').toPixels('y'); - let rx = this.attribute('rx').toPixels('x'); - let ry = this.attribute('ry').toPixels('y'); - if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; - if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; - rx = Math.min(rx, width / 2.0); - ry = Math.min(ry, height / 2.0); - if (!isNullish(ctx)) { - ctx.beginPath(); - ctx.moveTo(x + rx, y); - ctx.lineTo(x + width - rx, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + ry); - ctx.lineTo(x + width, y + height - ry); - ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); - ctx.lineTo(x + rx, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - ry); - ctx.lineTo(x, y + ry); - ctx.quadraticCurveTo(x, y, x + rx, y); - ctx.closePath(); - } - - return new svg.BoundingBox(x, y, x + width, y + height); - } - }; - - // circle element - svg.Element.circle = class extends svg.Element.PathElementBase { - path (ctx) { - const cx = this.attribute('cx').toPixels('x'); - const cy = this.attribute('cy').toPixels('y'); - const r = this.attribute('r').toPixels(); - - if (!isNullish(ctx)) { - ctx.beginPath(); - ctx.arc(cx, cy, r, 0, Math.PI * 2, true); - ctx.closePath(); - } - - return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); - } - }; - - // ellipse element - const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); - svg.Element.ellipse = class extends svg.Element.PathElementBase { - path (ctx) { - const rx = this.attribute('rx').toPixels('x'); - const ry = this.attribute('ry').toPixels('y'); - const cx = this.attribute('cx').toPixels('x'); - const cy = this.attribute('cy').toPixels('y'); - - if (!isNullish(ctx)) { - ctx.beginPath(); - ctx.moveTo(cx, cy - ry); - ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy); - ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry); - ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy); - ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry); - ctx.closePath(); - } - - return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); - } - }; - - // line element - svg.Element.line = class extends svg.Element.PathElementBase { - getPoints () { - return [ - new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')), - new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y')) - ]; - } - - path (ctx) { - const points = this.getPoints(); - - if (!isNullish(ctx)) { - ctx.beginPath(); - ctx.moveTo(points[0].x, points[0].y); - ctx.lineTo(points[1].x, points[1].y); - } - - return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); - } - - getMarkers () { - const points = this.getPoints(); - const a = points[0].angleTo(points[1]); - return [[points[0], a], [points[1], a]]; - } - }; - - // polyline element - svg.Element.polyline = class extends svg.Element.PathElementBase { - constructor (node) { - super(node); - - this.points = svg.CreatePath(this.attribute('points').value); - } - path (ctx) { - const {x, y} = this.points[0]; - const bb = new svg.BoundingBox(x, y); - if (!isNullish(ctx)) { - ctx.beginPath(); - ctx.moveTo(x, y); - } - for (let i = 1; i < this.points.length; i++) { - const {x: _x, y: _y} = this.points[i]; - bb.addPoint(_x, _y); - if (!isNullish(ctx)) ctx.lineTo(_x, _y); - } - return bb; - } - - getMarkers () { - const markers = []; - for (let i = 0; i < this.points.length - 1; i++) { - markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]); - } - markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]); - return markers; - } - }; - - // polygon element - svg.Element.polygon = class extends svg.Element.polyline { - path (ctx) { - const bb = super.path(ctx); - if (!isNullish(ctx)) { - ctx.lineTo(this.points[0].x, this.points[0].y); - ctx.closePath(); - } - return bb; - } - }; - - // path element - svg.Element.path = class extends svg.Element.PathElementBase { - constructor (node) { - super(node); - - let d = this.attribute('d').value - // TODO: convert to real lexer based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF - .replace(/,/gm, ' ') // get rid of all commas - .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands - .replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from commands - .replace(/([MmZzLlHhVvCcSsQqTtAa])(\S)/gm, '$1 $2') // separate commands from points - .replace(/(\S)([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2') // separate commands from points - .replace(/(\d)([+-])/gm, '$1 $2') // separate digits when no comma - .replace(/(\.\d*)(\.)/gm, '$1 $2') // separate digits when no comma - .replace(/([Aa](\s+\d+)(\s+\d+)(\s+\d+))\s+([01])\s*([01])/gm, '$1 $5 $6 '); // shorthand elliptical arc path syntax - d = svg.compressSpaces(d); // compress multiple spaces - d = svg.trim(d); - this.PathParser = { - tokens: d.split(' '), - - reset () { - this.i = -1; - this.command = ''; - this.previousCommand = ''; - this.start = new svg.Point(0, 0); - this.control = new svg.Point(0, 0); - this.current = new svg.Point(0, 0); - this.points = []; - this.angles = []; - }, - - isEnd () { - return this.i >= this.tokens.length - 1; - }, - - isCommandOrEnd () { - if (this.isEnd()) return true; - return !isNullish(this.tokens[this.i + 1].match(/^[A-Za-z]$/)); - }, - - isRelativeCommand () { - switch (this.command) { - case 'm': - case 'l': - case 'h': - case 'v': - case 'c': - case 's': - case 'q': - case 't': - case 'a': - case 'z': - return true; - } - return false; - }, - - getToken () { - this.i++; - return this.tokens[this.i]; - }, - - getScalar () { - return Number.parseFloat(this.getToken()); - }, - - nextCommand () { - this.previousCommand = this.command; - this.command = this.getToken(); - }, - - getPoint () { - const p = new svg.Point(this.getScalar(), this.getScalar()); - return this.makeAbsolute(p); - }, - - getAsControlPoint () { - const p = this.getPoint(); - this.control = p; - return p; - }, - - getAsCurrentPoint () { - const p = this.getPoint(); - this.current = p; - return p; - }, - - getReflectedControlPoint () { - if (this.previousCommand.toLowerCase() !== 'c' && - this.previousCommand.toLowerCase() !== 's' && - this.previousCommand.toLowerCase() !== 'q' && - this.previousCommand.toLowerCase() !== 't') { - return this.current; - } - - // reflect point - const p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); - return p; - }, - - makeAbsolute (p) { - if (this.isRelativeCommand()) { - p.x += this.current.x; - p.y += this.current.y; - } - return p; - }, - - addMarker (p, from, priorTo) { - // if the last angle isn't filled in because we didn't have this point yet ... - if (!isNullish(priorTo) && this.angles.length > 0 && isNullish(this.angles[this.angles.length - 1])) { - this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo); - } - this.addMarkerAngle(p, isNullish(from) ? null : from.angleTo(p)); - }, - - addMarkerAngle (p, a) { - this.points.push(p); - this.angles.push(a); - }, - - getMarkerPoints () { return this.points; }, - getMarkerAngles () { - for (let i = 0; i < this.angles.length; i++) { - if (isNullish(this.angles[i])) { - for (let j = i + 1; j < this.angles.length; j++) { - if (!isNullish(this.angles[j])) { - this.angles[i] = this.angles[j]; - break; - } - } - } - } - return this.angles; - } - }; - } - - path (ctx) { - const pp = this.PathParser; - pp.reset(); - - const bb = new svg.BoundingBox(); - if (!isNullish(ctx)) ctx.beginPath(); - while (!pp.isEnd()) { - pp.nextCommand(); - switch (pp.command) { - case 'M': - case 'm': { - const p = pp.getAsCurrentPoint(); - pp.addMarker(p); - bb.addPoint(p.x, p.y); - if (!isNullish(ctx)) ctx.moveTo(p.x, p.y); - pp.start = pp.current; - while (!pp.isCommandOrEnd()) { - const _p = pp.getAsCurrentPoint(); - pp.addMarker(_p, pp.start); - bb.addPoint(_p.x, _p.y); - if (!isNullish(ctx)) ctx.lineTo(_p.x, _p.y); - } - break; - } case 'L': - case 'l': - while (!pp.isCommandOrEnd()) { - const c = pp.current; - const p = pp.getAsCurrentPoint(); - pp.addMarker(p, c); - bb.addPoint(p.x, p.y); - if (!isNullish(ctx)) ctx.lineTo(p.x, p.y); - } - break; - case 'H': - case 'h': - while (!pp.isCommandOrEnd()) { - const newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); - pp.addMarker(newP, pp.current); - pp.current = newP; - bb.addPoint(pp.current.x, pp.current.y); - if (!isNullish(ctx)) ctx.lineTo(pp.current.x, pp.current.y); - } - break; - case 'V': - case 'v': - while (!pp.isCommandOrEnd()) { - const newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); - pp.addMarker(newP, pp.current); - pp.current = newP; - bb.addPoint(pp.current.x, pp.current.y); - if (!isNullish(ctx)) ctx.lineTo(pp.current.x, pp.current.y); - } - break; - case 'C': - case 'c': - while (!pp.isCommandOrEnd()) { - const curr = pp.current; - const p1 = pp.getPoint(); - const cntrl = pp.getAsControlPoint(); - const cp = pp.getAsCurrentPoint(); - pp.addMarker(cp, cntrl, p1); - bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); - if (!isNullish(ctx)) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); - } - break; - case 'S': - case 's': - while (!pp.isCommandOrEnd()) { - const curr = pp.current; - const p1 = pp.getReflectedControlPoint(); - const cntrl = pp.getAsControlPoint(); - const cp = pp.getAsCurrentPoint(); - pp.addMarker(cp, cntrl, p1); - bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); - if (!isNullish(ctx)) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); - } - break; - case 'Q': - case 'q': - while (!pp.isCommandOrEnd()) { - const curr = pp.current; - const cntrl = pp.getAsControlPoint(); - const cp = pp.getAsCurrentPoint(); - pp.addMarker(cp, cntrl, cntrl); - bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); - if (!isNullish(ctx)) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); - } - break; - case 'T': - case 't': - while (!pp.isCommandOrEnd()) { - const curr = pp.current; - const cntrl = pp.getReflectedControlPoint(); - pp.control = cntrl; - const cp = pp.getAsCurrentPoint(); - pp.addMarker(cp, cntrl, cntrl); - bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); - if (!isNullish(ctx)) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); - } - break; - case 'A': - case 'a': - while (!pp.isCommandOrEnd()) { - const curr = pp.current; - let rx = pp.getScalar(); - let ry = pp.getScalar(); - const xAxisRotation = pp.getScalar() * (Math.PI / 180.0); - const largeArcFlag = pp.getScalar(); - const sweepFlag = pp.getScalar(); - const cp = pp.getAsCurrentPoint(); - - // Conversion from endpoint to center parameterization - // https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter - - // x1', y1' - const currp = new svg.Point( - Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, - -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0 - ); - // adjust radii - const l = (currp.x ** 2) / (rx ** 2) + (currp.y ** 2) / (ry ** 2); - if (l > 1) { - rx *= Math.sqrt(l); - ry *= Math.sqrt(l); - } - // cx', cy' - let s = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt( - (((rx ** 2) * (ry ** 2)) - ((rx ** 2) * (currp.y ** 2)) - ((ry ** 2) * (currp.x ** 2))) / - ((rx ** 2) * (currp.y ** 2) + (ry ** 2) * (currp.x ** 2)) - ); - if (isNaN(s)) s = 0; - const cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); - // cx, cy - const centp = new svg.Point( - (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, - (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y - ); - // vector magnitude - const m = function (v) { - return Math.sqrt((v[0] ** 2) + (v[1] ** 2)); - }; - // ratio between two vectors - const r = function (u, v) { - return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)); - }; - // angle between two vectors - const a = function (u, v) { - return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); - }; - // initial angle - const a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); - // angle delta - const u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; - const v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; - let ad = a(u, v); - if (r(u, v) <= -1) ad = Math.PI; - if (r(u, v) >= 1) ad = 0; - - // for markers - const dir = 1 - sweepFlag ? 1.0 : -1.0; - const ah = a1 + dir * (ad / 2.0); - const halfWay = new svg.Point( - centp.x + rx * Math.cos(ah), - centp.y + ry * Math.sin(ah) - ); - pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); - pp.addMarkerAngle(cp, ah - dir * Math.PI); - - bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better - if (!isNullish(ctx)) { - const _r = rx > ry ? rx : ry; - const sx = rx > ry ? 1 : rx / ry; - const sy = rx > ry ? ry / rx : 1; - - ctx.translate(centp.x, centp.y); - ctx.rotate(xAxisRotation); - ctx.scale(sx, sy); - ctx.arc(0, 0, _r, a1, a1 + ad, 1 - sweepFlag); - ctx.scale(1 / sx, 1 / sy); - ctx.rotate(-xAxisRotation); - ctx.translate(-centp.x, -centp.y); - } - } - break; - case 'Z': - case 'z': - if (!isNullish(ctx)) ctx.closePath(); - pp.current = pp.start; - } - } - - return bb; - } - - getMarkers () { - const points = this.PathParser.getMarkerPoints(); - const angles = this.PathParser.getMarkerAngles(); - - const markers = points.map((point, i) => { - return [point, angles[i]]; - }); - return markers; - } - }; - - // pattern element - svg.Element.pattern = class extends svg.Element.ElementBase { - createPattern (ctx, element) { - const width = this.attribute('width').toPixels('x', true); - const height = this.attribute('height').toPixels('y', true); - - // render me using a temporary svg element - const tempSvg = new svg.Element.svg(); - tempSvg.attributes.viewBox = new svg.Property('viewBox', this.attribute('viewBox').value); - tempSvg.attributes.width = new svg.Property('width', width + 'px'); - tempSvg.attributes.height = new svg.Property('height', height + 'px'); - tempSvg.attributes.transform = new svg.Property('transform', this.attribute('patternTransform').value); - tempSvg.children = this.children; - - const c = document.createElement('canvas'); - c.width = width; - c.height = height; - const cctx = c.getContext('2d'); - if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { - cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true)); - } - // render 3x3 grid so when we transform there's no white space on edges - for (let x = -1; x <= 1; x++) { - for (let y = -1; y <= 1; y++) { - cctx.save(); - cctx.translate(x * c.width, y * c.height); - tempSvg.render(cctx); - cctx.restore(); - } - } - const pattern = ctx.createPattern(c, 'repeat'); - return pattern; - } - }; - - // marker element - svg.Element.marker = class extends svg.Element.ElementBase { - render (ctx, point, angle) { - ctx.translate(point.x, point.y); - if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(angle); - if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); - ctx.save(); - - // render me using a temporary svg element - const tempSvg = new svg.Element.svg(); - tempSvg.attributes.viewBox = new svg.Property( - 'viewBox', this.attribute('viewBox').value - ); - tempSvg.attributes.refX = new svg.Property( - 'refX', this.attribute('refX').value - ); - tempSvg.attributes.refY = new svg.Property( - 'refY', this.attribute('refY').value - ); - tempSvg.attributes.width = new svg.Property( - 'width', this.attribute('markerWidth').value - ); - tempSvg.attributes.height = new svg.Property( - 'height', this.attribute('markerHeight').value - ); - tempSvg.attributes.fill = new svg.Property( - 'fill', this.attribute('fill').valueOrDefault('black') - ); - tempSvg.attributes.stroke = new svg.Property( - 'stroke', this.attribute('stroke').valueOrDefault('none') - ); - tempSvg.children = this.children; - tempSvg.render(ctx); - - ctx.restore(); - if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === - 'strokeWidth' - ) ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); - if (this.attribute('orient').valueOrDefault('auto') === 'auto') { - ctx.rotate(-angle); - } - ctx.translate(-point.x, -point.y); - } - }; - - // definitions element - svg.Element.defs = class extends svg.Element.ElementBase { - render (ctx) { - // NOOP - } - }; - - // base for gradients - svg.Element.GradientBase = class extends svg.Element.ElementBase { - constructor (node) { - super(node); - - this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); - - this.stops = []; - this.children.forEach((child) => { - if (child.type === 'stop') { - this.stops.push(child); - } - }); - } - - getGradient () { - // OVERRIDE ME! - } - - createGradient (ctx, element, parentOpacityProp) { - const stopsContainer = this.getHrefAttribute().hasValue() - ? this.getHrefAttribute().getDefinition() - : this; - - const addParentOpacity = function (color) { - if (parentOpacityProp.hasValue()) { - const p = new svg.Property('color', color); - return p.addOpacity(parentOpacityProp).value; - } - return color; - }; - - const g = this.getGradient(ctx, element); - if (isNullish(g)) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color); - stopsContainer.stops.forEach(({offset, color}) => { - g.addColorStop(offset, addParentOpacity(color)); - }); - - if (this.attribute('gradientTransform').hasValue()) { - // render as transformed pattern on temporary canvas - const rootView = svg.ViewPort.viewPorts[0]; - - const rect = new svg.Element.rect(); - rect.attributes.x = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0); - rect.attributes.y = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0); - rect.attributes.width = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS); - rect.attributes.height = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS); - - const group = new svg.Element.g(); - group.attributes.transform = new svg.Property('transform', this.attribute('gradientTransform').value); - group.children = [rect]; - - const tempSvg = new svg.Element.svg(); - tempSvg.attributes.x = new svg.Property('x', 0); - tempSvg.attributes.y = new svg.Property('y', 0); - tempSvg.attributes.width = new svg.Property('width', rootView.width); - tempSvg.attributes.height = new svg.Property('height', rootView.height); - tempSvg.children = [group]; - - const c = document.createElement('canvas'); - c.width = rootView.width; - c.height = rootView.height; - const tempCtx = c.getContext('2d'); - tempCtx.fillStyle = g; - tempSvg.render(tempCtx); - return tempCtx.createPattern(c, 'no-repeat'); - } - - return g; - } - }; - - // linear gradient element - svg.Element.linearGradient = class extends svg.Element.GradientBase { - getGradient (ctx, element) { - const useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; - const bb = useBB - ? element.getBoundingBox() - : null; - - if (!this.attribute('x1').hasValue() && - !this.attribute('y1').hasValue() && - !this.attribute('x2').hasValue() && - !this.attribute('y2').hasValue() - ) { - this.attribute('x1', true).value = 0; - this.attribute('y1', true).value = 0; - this.attribute('x2', true).value = 1; - this.attribute('y2', true).value = 0; - } - - const x1 = (useBB - ? bb.x() + bb.width() * this.attribute('x1').numValue() - : this.attribute('x1').toPixels('x')); - const y1 = (useBB - ? bb.y() + bb.height() * this.attribute('y1').numValue() - : this.attribute('y1').toPixels('y')); - const x2 = (useBB - ? bb.x() + bb.width() * this.attribute('x2').numValue() - : this.attribute('x2').toPixels('x')); - const y2 = (useBB - ? bb.y() + bb.height() * this.attribute('y2').numValue() - : this.attribute('y2').toPixels('y')); - - if (x1 === x2 && y1 === y2) return null; - return ctx.createLinearGradient(x1, y1, x2, y2); - } - }; - - // radial gradient element - svg.Element.radialGradient = class extends svg.Element.GradientBase { - getGradient (ctx, element) { - const useBB = this.gradientUnits === 'objectBoundingBox' && element.getBoundingBox; - const bb = useBB ? element.getBoundingBox() : null; - - if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%'; - if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%'; - if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%'; - - const cx = (useBB - ? bb.x() + bb.width() * this.attribute('cx').numValue() - : this.attribute('cx').toPixels('x')); - const cy = (useBB - ? bb.y() + bb.height() * this.attribute('cy').numValue() - : this.attribute('cy').toPixels('y')); - - let fx = cx; - let fy = cy; - if (this.attribute('fx').hasValue()) { - fx = (useBB - ? bb.x() + bb.width() * this.attribute('fx').numValue() - : this.attribute('fx').toPixels('x')); - } - if (this.attribute('fy').hasValue()) { - fy = (useBB - ? bb.y() + bb.height() * this.attribute('fy').numValue() - : this.attribute('fy').toPixels('y')); - } - - const r = (useBB - ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() - : this.attribute('r').toPixels()); - - return ctx.createRadialGradient(fx, fy, 0, cx, cy, r); - } - }; - - // gradient stop element - svg.Element.stop = class extends svg.Element.ElementBase { - constructor (node) { - super(node); - - this.offset = this.attribute('offset').numValue(); - if (this.offset < 0) this.offset = 0; - if (this.offset > 1) this.offset = 1; - - let stopColor = this.style('stop-color'); - if (this.style('stop-opacity').hasValue()) { - stopColor = stopColor.addOpacity(this.style('stop-opacity')); - } - this.color = stopColor.value; - } - }; - - // animation base element - svg.Element.AnimateBase = class extends svg.Element.ElementBase { - constructor (node) { - super(node); - - svg.Animations.push(this); - - this.duration = 0.0; - this.begin = this.attribute('begin').toMilliseconds(); - this.maxDuration = this.begin + this.attribute('dur').toMilliseconds(); - - this.initialValue = null; - this.initialUnits = ''; - this.removed = false; - - this.from = this.attribute('from'); - this.to = this.attribute('to'); - this.values = this.attribute('values'); - if (this.values.hasValue()) this.values.value = this.values.value.split(';'); - } - - getProperty () { - const attributeType = this.attribute('attributeType').value; - const attributeName = this.attribute('attributeName').value; - - if (attributeType === 'CSS') { - return this.parent.style(attributeName, true); - } - return this.parent.attribute(attributeName, true); - } - - calcValue () { - // OVERRIDE ME! - return ''; - } - - update (delta) { - // set initial value - if (isNullish(this.initialValue)) { - this.initialValue = this.getProperty().value; - this.initialUnits = this.getProperty().getUnits(); - } - - // if we're past the end time - if (this.duration > this.maxDuration) { - // loop for indefinitely repeating animations - if (this.attribute('repeatCount').value === 'indefinite' || - this.attribute('repeatDur').value === 'indefinite') { - this.duration = 0.0; - } else if (this.attribute('fill').valueOrDefault('remove') === 'freeze' && !this.frozen) { - this.frozen = true; - this.parent.animationFrozen = true; - this.parent.animationFrozenValue = this.getProperty().value; - } else if (this.attribute('fill').valueOrDefault('remove') === 'remove' && !this.removed) { - this.removed = true; - this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; - return true; - } - return false; - } - this.duration += delta; - - // if we're past the begin time - let updated = false; - if (this.begin < this.duration) { - let newValue = this.calcValue(); // tween - - if (this.attribute('type').hasValue()) { - // for transform, etc. - const type = this.attribute('type').value; - newValue = type + '(' + newValue + ')'; - } - - this.getProperty().value = newValue; - updated = true; - } - - return updated; - } - - // fraction of duration we've covered - progress () { - const ret = {progress: (this.duration - this.begin) / (this.maxDuration - this.begin)}; - if (this.values.hasValue()) { - const p = ret.progress * (this.values.value.length - 1); - const lb = Math.floor(p), ub = Math.ceil(p); - ret.from = new svg.Property('from', Number.parseFloat(this.values.value[lb])); - ret.to = new svg.Property('to', Number.parseFloat(this.values.value[ub])); - ret.progress = (p - lb) / (ub - lb); - } else { - ret.from = this.from; - ret.to = this.to; - } - return ret; - } - }; - - // animate element - svg.Element.animate = class extends svg.Element.AnimateBase { - calcValue () { - const p = this.progress(); - - // tween value linearly - const newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; - return newValue + this.initialUnits; - } - }; - - // animate color element - svg.Element.animateColor = class extends svg.Element.AnimateBase { - calcValue () { - const p = this.progress(); - const from = new RGBColor(p.from.value); - const to = new RGBColor(p.to.value); - - if (from.ok && to.ok) { - // tween color linearly - const r = from.r + (to.r - from.r) * p.progress; - const g = from.g + (to.g - from.g) * p.progress; - const b = from.b + (to.b - from.b) * p.progress; - return 'rgb(' + Number.parseInt(r) + ',' + Number.parseInt(g) + ',' + Number.parseInt(b) + ')'; - } - return this.attribute('from').value; - } - }; - - // animate transform element - svg.Element.animateTransform = class extends svg.Element.animate { - calcValue () { - const p = this.progress(); - - // tween value linearly - const from = svg.ToNumberArray(p.from.value); - const to = svg.ToNumberArray(p.to.value); - let newValue = ''; - from.forEach((fr, i) => { - newValue += fr + (to[i] - fr) * p.progress + ' '; - }); - return newValue; - } - }; - - // font element - svg.Element.font = class extends svg.Element.ElementBase { - constructor (node) { - super(node); - - this.horizAdvX = this.attribute('horiz-adv-x').numValue(); - - this.isRTL = false; - this.isArabic = false; - this.fontFace = null; - this.missingGlyph = null; - this.glyphs = []; - this.children.forEach((child) => { - if (child.type === 'font-face') { - this.fontFace = child; - if (child.style('font-family').hasValue()) { - svg.Definitions[child.style('font-family').value] = this; - } - } else if (child.type === 'missing-glyph') { - this.missingGlyph = child; - } else if (child.type === 'glyph') { - if (child.arabicForm !== '') { - this.isRTL = true; - this.isArabic = true; - if (typeof this.glyphs[child.unicode] === 'undefined') { - this.glyphs[child.unicode] = []; - } - this.glyphs[child.unicode][child.arabicForm] = child; - } else { - this.glyphs[child.unicode] = child; - } - } - }); - } - }; - - // font-face element - svg.Element.fontface = class extends svg.Element.ElementBase { - constructor (node) { - super(node); - - this.ascent = this.attribute('ascent').value; - this.descent = this.attribute('descent').value; - this.unitsPerEm = this.attribute('units-per-em').numValue(); - } - }; - - // missing-glyph element - svg.Element.missingglyph = class extends svg.Element.path { - constructor (node) { - super(node); - - this.horizAdvX = 0; - } - }; - - // glyph element - svg.Element.glyph = class extends svg.Element.path { - constructor (node) { - super(node); - - this.horizAdvX = this.attribute('horiz-adv-x').numValue(); - this.unicode = this.attribute('unicode').value; - this.arabicForm = this.attribute('arabic-form').value; - } - }; - - // text element - svg.Element.text = class extends svg.Element.RenderedElementBase { - constructor (node) { - super(node, true); - } - - setContext (ctx) { - super.setContext(ctx); - - let textBaseline = this.style('dominant-baseline').toTextBaseline(); - if (isNullish(textBaseline)) textBaseline = this.style('alignment-baseline').toTextBaseline(); - if (!isNullish(textBaseline)) ctx.textBaseline = textBaseline; - } - - getBoundingBox () { - const x = this.attribute('x').toPixels('x'); - const y = this.attribute('y').toPixels('y'); - const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); - return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y); - } - - renderChildren (ctx) { - this.x = this.attribute('x').toPixels('x'); - this.y = this.attribute('y').toPixels('y'); - this.x += this.getAnchorDelta(ctx, this, 0); - this.children.forEach((child, i) => { - this.renderChild(ctx, this, i); - }); - } - - getAnchorDelta (ctx, parent, startI) { - const textAnchor = this.style('text-anchor').valueOrDefault('start'); - if (textAnchor !== 'start') { - let width = 0; - for (let i = startI; i < parent.children.length; i++) { - const child = parent.children[i]; - if (i > startI && child.attribute('x').hasValue()) break; // new group - width += child.measureTextRecursive(ctx); - } - return -1 * (textAnchor === 'end' ? width : width / 2.0); - } - return 0; - } - - renderChild (ctx, parent, i) { - const child = parent.children[i]; - if (child.attribute('x').hasValue()) { - child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); - if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); - } else { - if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); - if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); - child.x = this.x; - } - this.x = child.x + child.measureText(ctx); - - if (child.attribute('y').hasValue()) { - child.y = child.attribute('y').toPixels('y'); - if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); - } else { - if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); - if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); - child.y = this.y; - } - this.y = child.y; - - child.render(ctx); - - for (let j = 0; j < child.children.length; j++) { - this.renderChild(ctx, child, j); - } - } - }; - - // text base - svg.Element.TextElementBase = class extends svg.Element.RenderedElementBase { - getGlyph (font, text, i) { - const c = text[i]; - let glyph = null; - if (font.isArabic) { - let arabicForm = 'isolated'; - if ((i === 0 || text[i - 1] === ' ') && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'terminal'; - if (i > 0 && text[i - 1] !== ' ' && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'medial'; - if (i > 0 && text[i - 1] !== ' ' && (i === text.length - 1 || text[i + 1] === ' ')) arabicForm = 'initial'; - if (typeof font.glyphs[c] !== 'undefined') { - glyph = font.glyphs[c][arabicForm]; - if (isNullish(glyph) && font.glyphs[c].type === 'glyph') glyph = font.glyphs[c]; - } - } else { - glyph = font.glyphs[c]; - } - if (isNullish(glyph)) glyph = font.missingGlyph; - return glyph; - } - - renderChildren (ctx) { - const customFont = this.parent.style('font-family').getDefinition(); - if (!isNullish(customFont)) { - const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); - const fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); - let text = this.getText(); - if (customFont.isRTL) text = text.split('').reverse().join(''); - - const dx = svg.ToNumberArray(this.parent.attribute('dx').value); - for (let i = 0; i < text.length; i++) { - const glyph = this.getGlyph(customFont, text, i); - const scale = fontSize / customFont.fontFace.unitsPerEm; - ctx.translate(this.x, this.y); - ctx.scale(scale, -scale); - const lw = ctx.lineWidth; - ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize; - if (fontStyle === 'italic') ctx.transform(1, 0, 0.4, 1, 0, 0); - glyph.render(ctx); - if (fontStyle === 'italic') ctx.transform(1, 0, -0.4, 1, 0, 0); - ctx.lineWidth = lw; - ctx.scale(1 / scale, -1 / scale); - ctx.translate(-this.x, -this.y); - - this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm; - if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { - this.x += dx[i]; - } - } - return; - } - - if (ctx.fillStyle !== '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); - if (ctx.strokeStyle !== '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); - } - - getText () { - // OVERRIDE ME - } - - measureTextRecursive (ctx) { - let width = this.measureText(ctx); - this.children.forEach((child) => { - width += child.measureTextRecursive(ctx); - }); - return width; - } - - measureText (ctx) { - const customFont = this.parent.style('font-family').getDefinition(); - if (!isNullish(customFont)) { - const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); - let measure = 0; - let text = this.getText(); - if (customFont.isRTL) text = text.split('').reverse().join(''); - const dx = svg.ToNumberArray(this.parent.attribute('dx').value); - for (let i = 0; i < text.length; i++) { - const glyph = this.getGlyph(customFont, text, i); - measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; - if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { - measure += dx[i]; - } - } - return measure; - } - - const textToMeasure = svg.compressSpaces(this.getText()); - if (!ctx.measureText) return textToMeasure.length * 10; - - ctx.save(); - this.setContext(ctx); - const {width} = ctx.measureText(textToMeasure); - ctx.restore(); - return width; - } - }; - - // tspan - svg.Element.tspan = class extends svg.Element.TextElementBase { - constructor (node) { - super(node, true); - - this.text = node.nodeValue || node.text || ''; - } - getText () { - return this.text; - } - }; - - // tref - svg.Element.tref = class extends svg.Element.TextElementBase { - getText () { - const element = this.getHrefAttribute().getDefinition(); - if (!isNullish(element)) return element.children[0].getText(); - return undefined; - } - }; - - // a element - svg.Element.a = class extends svg.Element.TextElementBase { - constructor (node) { - super(node); - - this.hasText = true; - [...node.childNodes].forEach((childNode) => { - if (childNode.nodeType !== 3) { - this.hasText = false; - } - }); - // this might contain text - this.text = this.hasText ? node.childNodes[0].nodeValue : ''; - } - - getText () { - return this.text; - } - - renderChildren (ctx) { - if (this.hasText) { - // render as text element - super.renderChildren(ctx); - const fontSize = new svg.Property( - 'fontSize', svg.Font.Parse(svg.ctx.font).fontSize - ); - svg.Mouse.checkBoundingBox( - this, new svg.BoundingBox( - this.x, - this.y - fontSize.toPixels('y'), - this.x + this.measureText(ctx), - this.y - ) - ); - } else { - // render as temporary group - const g = new svg.Element.g(); - g.children = this.children; - g.parent = this; - g.render(ctx); - } - } - - onclick () { - window.open(this.getHrefAttribute().value); - } - - onmousemove () { - svg.ctx.canvas.style.cursor = 'pointer'; - } - }; - - // image element - svg.Element.image = class extends svg.Element.RenderedElementBase { - constructor (node) { - super(node); - - const href = this.getHrefAttribute().value; - if (href === '') { - return; - } - this._isSvg = href.match(/\.svg$/); - - svg.Images.push(this); - this.loaded = false; - if (!this._isSvg) { - this.img = document.createElement('img'); - if (svg.opts.useCORS === true) { - this.img.crossOrigin = 'Anonymous'; - } - this.img.addEventListener('load', () => { - this.loaded = true; - }); - this.img.addEventListener('error', () => { - svg.log('ERROR: image "' + href + '" not found'); - this.loaded = true; - }); - this.img.src = href; - } else { - svg.ajax(href, true).then((img) => { // eslint-disable-line promise/prefer-await-to-then, promise/always-return - this.img = img; - this.loaded = true; - }).catch((err) => { // eslint-disable-line promise/prefer-await-to-callbacks - this.erred = true; - console.error('Ajax error for canvg', err); // eslint-disable-line no-console - }); - } - } - renderChildren (ctx) { - const x = this.attribute('x').toPixels('x'); - const y = this.attribute('y').toPixels('y'); - - const width = this.attribute('width').toPixels('x'); - const height = this.attribute('height').toPixels('y'); - if (width === 0 || height === 0) return; - - ctx.save(); - if (this._isSvg) { - ctx.drawSvg(this.img, x, y, width, height); - } else { - ctx.translate(x, y); - svg.AspectRatio( - ctx, - this.attribute('preserveAspectRatio').value, - width, - this.img.width, - height, - this.img.height, - 0, - 0 - ); - ctx.drawImage(this.img, 0, 0); - } - ctx.restore(); - } - - getBoundingBox () { - const x = this.attribute('x').toPixels('x'); - const y = this.attribute('y').toPixels('y'); - const width = this.attribute('width').toPixels('x'); - const height = this.attribute('height').toPixels('y'); - return new svg.BoundingBox(x, y, x + width, y + height); - } - }; - - // group element - svg.Element.g = class extends svg.Element.RenderedElementBase { - getBoundingBox () { - const bb = new svg.BoundingBox(); - this.children.forEach((child) => { - bb.addBoundingBox(child.getBoundingBox()); - }); - return bb; - } - }; - - // symbol element - svg.Element.symbol = class extends svg.Element.RenderedElementBase { - render (ctx) { - // NO RENDER - } - }; - - // style element - svg.Element.style = class extends svg.Element.ElementBase { - constructor (node) { - super(node); - - // text, or spaces then CDATA - let css = ''; - [...node.childNodes].forEach(({nodeValue}) => { - css += nodeValue; - }); - // remove comments - css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^\s*\/\/.*)/gm, ''); // eslint-disable-line unicorn/no-unsafe-regex - // replace whitespace - css = svg.compressSpaces(css); - const cssDefs = css.split('}'); - cssDefs.forEach((cssDef) => { - if (svg.trim(cssDef) !== '') { - let [cssClasses, cssProps] = cssDef.split('{'); - cssClasses = cssClasses.split(','); - cssProps = cssProps.split(';'); - cssClasses.forEach((cssClass) => { - cssClass = svg.trim(cssClass); - if (cssClass !== '') { - const props = {}; - cssProps.forEach((cssProp) => { - const prop = cssProp.indexOf(':'); - const name = cssProp.substr(0, prop); - const value = cssProp.substr(prop + 1, cssProp.length - prop); - if (!isNullish(name) && !isNullish(value)) { - props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value)); - } - }); - svg.Styles[cssClass] = props; - if (cssClass === '@font-face') { - const fontFamily = props['font-family'].value.replace(/"/g, ''); - const srcs = props.src.value.split(','); - srcs.forEach((src) => { - if (src.includes('format("svg")')) { - const urlStart = src.indexOf('url'); - const urlEnd = src.indexOf(')', urlStart); - const url = src.substr(urlStart + 5, urlEnd - urlStart - 6); - // Can this ajax safely be converted to async? - const doc = svg.parseXml(svg.ajax(url)); - const fonts = doc.getElementsByTagName('font'); - [...fonts].forEach((font) => { - font = svg.CreateElement(font); - svg.Definitions[fontFamily] = font; - }); - } - }); - } - } - }); - } - }); - } - }; - - // use element - svg.Element.use = class extends svg.Element.RenderedElementBase { - constructor (node) { - super(node); - - this._el = this.getHrefAttribute().getDefinition(); - } - - setContext (ctx) { - super.setContext(ctx); - if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0); - if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y')); - } - - path (ctx) { - const {_el: element} = this; - if (!isNullish(element)) element.path(ctx); - } - - getBoundingBox () { - const {_el: element} = this; - if (!isNullish(element)) return element.getBoundingBox(); - return undefined; - } - - renderChildren (ctx) { - const {_el: element} = this; - if (!isNullish(element)) { - let tempSvg = element; - if (element.type === 'symbol') { - // render me using a temporary svg element in symbol cases - // (https://www.w3.org/TR/SVG/struct.html#UseElement) - tempSvg = new svg.Element.svg(); - tempSvg.type = 'svg'; - tempSvg.attributes.viewBox = new svg.Property( - 'viewBox', element.attribute('viewBox').value - ); - tempSvg.attributes.preserveAspectRatio = new svg.Property( - 'preserveAspectRatio', element.attribute('preserveAspectRatio').value - ); - tempSvg.attributes.overflow = new svg.Property( - 'overflow', element.attribute('overflow').value - ); - tempSvg.children = element.children; - } - if (tempSvg.type === 'svg') { - // if symbol or svg, inherit width/height from me - if (this.attribute('width').hasValue()) { - tempSvg.attributes.width = new svg.Property( - 'width', this.attribute('width').value - ); - } - if (this.attribute('height').hasValue()) { - tempSvg.attributes.height = new svg.Property( - 'height', this.attribute('height').value - ); - } - } - const oldParent = tempSvg.parent; - tempSvg.parent = null; - tempSvg.render(ctx); - tempSvg.parent = oldParent; - } - } - }; - - // mask element - svg.Element.mask = class extends svg.Element.ElementBase { - apply (ctx, element) { - // render as temp svg - let x = this.attribute('x').toPixels('x'); - let y = this.attribute('y').toPixels('y'); - let width = this.attribute('width').toPixels('x'); - let height = this.attribute('height').toPixels('y'); - - if (width === 0 && height === 0) { - const bb = new svg.BoundingBox(); - this.children.forEach((child) => { - bb.addBoundingBox(child.getBoundingBox()); - }); - x = Math.floor(bb.x1); - y = Math.floor(bb.y1); - width = Math.floor(bb.width()); - height = Math.floor(bb.height()); - } - - // temporarily remove mask to avoid recursion - const mask = element.attribute('mask').value; - element.attribute('mask').value = ''; - - const cMask = document.createElement('canvas'); - cMask.width = x + width; - cMask.height = y + height; - const maskCtx = cMask.getContext('2d'); - this.renderChildren(maskCtx); - - const c = document.createElement('canvas'); - c.width = x + width; - c.height = y + height; - const tempCtx = c.getContext('2d'); - element.render(tempCtx); - tempCtx.globalCompositeOperation = 'destination-in'; - tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat'); - tempCtx.fillRect(0, 0, x + width, y + height); - - ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat'); - ctx.fillRect(0, 0, x + width, y + height); - - // reassign mask - element.attribute('mask').value = mask; - } - - render (ctx) { - // NO RENDER - } - }; - - // clip element - svg.Element.clipPath = class extends svg.Element.ElementBase { - apply (ctx) { - this.children.forEach((child) => { - if (typeof child.path !== 'undefined') { - let transform = null; - if (child.attribute('transform').hasValue()) { - transform = new svg.Transform(child.attribute('transform').value); - transform.apply(ctx); - } - child.path(ctx); - ctx.clip(); - if (transform) { transform.unapply(ctx); } - } - }); - } - render (ctx) { - // NO RENDER - } - }; - - // filters - svg.Element.filter = class extends svg.Element.ElementBase { - apply (ctx, element) { - // render as temp svg - const bb = element.getBoundingBox(); - const x = Math.floor(bb.x1); - const y = Math.floor(bb.y1); - const width = Math.floor(bb.width()); - const height = Math.floor(bb.height()); - - // temporarily remove filter to avoid recursion - const filter = element.style('filter').value; - element.style('filter').value = ''; - - let px = 0, py = 0; - this.children.forEach((child) => { - const efd = child.extraFilterDistance || 0; - px = Math.max(px, efd); - py = Math.max(py, efd); - }); - - const c = document.createElement('canvas'); - c.width = width + 2 * px; - c.height = height + 2 * py; - const tempCtx = c.getContext('2d'); - tempCtx.translate(-x + px, -y + py); - element.render(tempCtx); - - // apply filters - this.children.forEach((child) => { - child.apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py); - }); - - // render on me - ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py); - - // reassign filter - element.style('filter', true).value = filter; - } - - render (ctx) { - // NO RENDER - } - }; - - svg.Element.feMorphology = class extends svg.Element.ElementBase { - apply (ctx, x, y, width, height) { - // TODO: implement - } - }; - - svg.Element.feComposite = class extends svg.Element.ElementBase { - apply (ctx, x, y, width, height) { - // TODO: implement - } - }; - - /** - * @param {Uint8ClampedArray} img - * @param {Integer} x - * @param {Integer} y - * @param {Float} width - * @param {Float} height - * @param {Integer} rgba - * @returns {Integer} - */ - function imGet (img, x, y, width, height, rgba) { - return img[y * width * 4 + x * 4 + rgba]; - } - - /** - * @param {Uint8ClampedArray} img - * @param {Integer} x - * @param {Integer} y - * @param {Float} width - * @param {Float} height - * @param {Integer} rgba - * @param {Float} val - * @returns {void} - */ - function imSet (img, x, y, width, height, rgba, val) { - img[y * width * 4 + x * 4 + rgba] = val; - } - - svg.Element.feColorMatrix = class extends svg.Element.ElementBase { - constructor (node) { - super(node); - - let matrix = svg.ToNumberArray(this.attribute('values').value); - switch (this.attribute('type').valueOrDefault('matrix')) { // https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement - case 'saturate': { - const s = matrix[0]; - matrix = [ - 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, - 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, - 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, - 0, 0, 0, 1, 0, - 0, 0, 0, 0, 1 - ]; - break; - } case 'hueRotate': { - const a = matrix[0] * Math.PI / 180.0; - const c = function (m1, m2, m3) { - return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; - }; - matrix = [ - c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0, - c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0, - c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0, - 0, 0, 0, 1, 0, - 0, 0, 0, 0, 1 - ]; - break; - } case 'luminanceToAlpha': - matrix = [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0.2125, 0.7154, 0.0721, 0, 0, - 0, 0, 0, 0, 1 - ]; - break; - } - this.matrix = matrix; - - this._m = (i, v) => { - const mi = matrix[i]; - return mi * (mi < 0 ? v - 255 : v); - }; - } - apply (ctx, x, y, width, height) { - const {_m: m} = this; - // assuming x==0 && y==0 for now - const srcData = ctx.getImageData(0, 0, width, height); - for (let _y = 0; _y < height; _y++) { - for (let _x = 0; _x < width; _x++) { - const r = imGet(srcData.data, _x, _y, width, height, 0); - const g = imGet(srcData.data, _x, _y, width, height, 1); - const b = imGet(srcData.data, _x, _y, width, height, 2); - const a = imGet(srcData.data, _x, _y, width, height, 3); - imSet(srcData.data, _x, _y, width, height, 0, m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1)); - imSet(srcData.data, _x, _y, width, height, 1, m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1)); - imSet(srcData.data, _x, _y, width, height, 2, m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1)); - imSet(srcData.data, _x, _y, width, height, 3, m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1)); - } - } - ctx.clearRect(0, 0, width, height); - ctx.putImageData(srcData, 0, 0); - } - }; - - svg.Element.feGaussianBlur = class extends svg.Element.ElementBase { - constructor (node) { - super(node); - - this.blurRadius = Math.floor(this.attribute('stdDeviation').numValue()); - this.extraFilterDistance = this.blurRadius; - } - - apply (ctx, x, y, width, height) { - // Todo: This might not be a problem anymore with out `instanceof` fix - // StackBlur requires canvas be on document - ctx.canvas.id = svg.UniqueId(); - ctx.canvas.style.display = 'none'; - document.body.append(ctx.canvas); - canvasRGBA(ctx.canvas, x, y, width, height, this.blurRadius); - ctx.canvas.remove(); - } - }; - - // title element, do nothing - svg.Element.title = class extends svg.Element.ElementBase { - constructor (node) { - super(); - } - }; - - // desc element, do nothing - svg.Element.desc = class extends svg.Element.ElementBase { - constructor (node) { - super(); - } - }; - - svg.Element.MISSING = class extends svg.Element.ElementBase { - constructor (node) { - super(); - svg.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.'); - } - }; - - // element factory - svg.CreateElement = function (node) { - const className = node.nodeName - .replace(/^[^:]+:/, '') // remove namespace - .replace(/-/g, ''); // remove dashes - let e; - if (typeof svg.Element[className] !== 'undefined') { - e = new svg.Element[className](node); - } else { - e = new svg.Element.MISSING(node); - } - - e.type = node.nodeName; - return e; - }; - - // load from url - svg.load = async function (ctx, url) { - const dom = await svg.ajax(url, true); - return svg.loadXml(ctx, dom); - }; - - // load from xml - svg.loadXml = function (ctx, xml) { - return svg.loadXmlDoc(ctx, svg.parseXml(xml)); - }; - - svg.loadXmlDoc = function (ctx, dom) { - let res; - svg.init(ctx); - - const mapXY = function (p) { - let e = ctx.canvas; - while (e) { - p.x -= e.offsetLeft; - p.y -= e.offsetTop; - e = e.offsetParent; - } - if (window.scrollX) p.x += window.scrollX; - if (window.scrollY) p.y += window.scrollY; - return p; - }; - - // bind mouse - if (svg.opts.ignoreMouse !== true) { - ctx.canvas.addEventListener('click', function (e) { - const args = !isNullish(e) - ? [e.clientX, e.clientY] - : [event.clientX, event.clientY]; // eslint-disable-line no-restricted-globals - const {x, y} = mapXY(new svg.Point(...args)); - svg.Mouse.onclick(x, y); - }); - ctx.canvas.addEventListener('mousemove', function (e) { - const args = !isNullish(e) - ? [e.clientX, e.clientY] - : [event.clientX, event.clientY]; // eslint-disable-line no-restricted-globals - const {x, y} = mapXY(new svg.Point(...args)); - svg.Mouse.onmousemove(x, y); - }); - } - - const e = svg.CreateElement(dom.documentElement); - e.root = true; - - // render loop - let isFirstRender = true; - const draw = function (resolve) { - svg.ViewPort.Clear(); - if (ctx.canvas.parentNode) { - svg.ViewPort.SetCurrent( - ctx.canvas.parentNode.clientWidth, - ctx.canvas.parentNode.clientHeight - ); - } - - if (svg.opts.ignoreDimensions !== true) { - // set canvas size - if (e.style('width').hasValue()) { - ctx.canvas.width = e.style('width').toPixels('x'); - ctx.canvas.style.width = ctx.canvas.width + 'px'; - } - if (e.style('height').hasValue()) { - ctx.canvas.height = e.style('height').toPixels('y'); - ctx.canvas.style.height = ctx.canvas.height + 'px'; - } - } - let cWidth = ctx.canvas.clientWidth || ctx.canvas.width; - let cHeight = ctx.canvas.clientHeight || ctx.canvas.height; - if (svg.opts.ignoreDimensions === true && - e.style('width').hasValue() && e.style('height').hasValue() - ) { - cWidth = e.style('width').toPixels('x'); - cHeight = e.style('height').toPixels('y'); - } - svg.ViewPort.SetCurrent(cWidth, cHeight); - - if (!isNullish(svg.opts.offsetX)) { - e.attribute('x', true).value = svg.opts.offsetX; - } - if (!isNullish(svg.opts.offsetY)) { - e.attribute('y', true).value = svg.opts.offsetY; - } - if (!isNullish(svg.opts.scaleWidth) || !isNullish(svg.opts.scaleHeight)) { - const viewBox = svg.ToNumberArray(e.attribute('viewBox').value); - let xRatio = null, yRatio = null; - - if (!isNullish(svg.opts.scaleWidth)) { - if (e.attribute('width').hasValue()) { - xRatio = e.attribute('width').toPixels('x') / svg.opts.scaleWidth; - } else if (!isNaN(viewBox[2])) { - xRatio = viewBox[2] / svg.opts.scaleWidth; - } - } - - if (!isNullish(svg.opts.scaleHeight)) { - if (e.attribute('height').hasValue()) { - yRatio = e.attribute('height').toPixels('y') / svg.opts.scaleHeight; - } else if (!isNaN(viewBox[3])) { - yRatio = viewBox[3] / svg.opts.scaleHeight; - } - } - - if (isNullish(xRatio)) { xRatio = yRatio; } - if (isNullish(yRatio)) { yRatio = xRatio; } - - e.attribute('width', true).value = svg.opts.scaleWidth; - e.attribute('height', true).value = svg.opts.scaleHeight; - e.attribute('viewBox', true).value = '0 0 ' + (cWidth * xRatio) + ' ' + (cHeight * yRatio); - e.attribute('preserveAspectRatio', true).value = 'none'; - } - - // clear and render - if (svg.opts.ignoreClear !== true) { - ctx.clearRect(0, 0, cWidth, cHeight); - } - e.render(ctx); - if (isFirstRender) { - isFirstRender = false; - resolve(dom); - } - }; - - let waitingForImages = true; - svg.intervalID = setInterval(function () { - let needUpdate = false; - - if (waitingForImages && svg.ImagesLoaded()) { - waitingForImages = false; - needUpdate = true; - } - - // need update from mouse events? - if (svg.opts.ignoreMouse !== true) { - needUpdate = needUpdate || svg.Mouse.hasEvents(); - } - - // need update from animations? - if (svg.opts.ignoreAnimation !== true) { - svg.Animations.forEach((animation) => { - const needAnimationUpdate = animation.update(1000 / svg.FRAMERATE); - needUpdate = needUpdate || needAnimationUpdate; - }); - } - - // need update from redraw? - if (typeof svg.opts.forceRedraw === 'function') { - if (svg.opts.forceRedraw() === true) { - needUpdate = true; - } - } - - // render if needed - if (needUpdate) { - draw(res); - svg.Mouse.runEvents(); // run and clear our events - } - }, 1000 / svg.FRAMERATE); - // Todo: Replace with an image loading Promise utility? - // eslint-disable-next-line promise/avoid-new - return new Promise((resolve, reject) => { - if (svg.ImagesLoaded()) { - waitingForImages = false; - draw(resolve); - return; - } - res = resolve; - }); - }; - - svg.stop = () => { - if (svg.intervalID) { - clearInterval(svg.intervalID); - } - }; - - svg.Mouse = { - events: [], - hasEvents () { return this.events.length !== 0; }, - - onclick (x, y) { - this.events.push({ - type: 'onclick', x, y, - run (e) { if (e.onclick) e.onclick(); } - }); - }, - - onmousemove (x, y) { - this.events.push({ - type: 'onmousemove', x, y, - run (e) { if (e.onmousemove) e.onmousemove(); } - }); - }, - - eventElements: [], - - checkPath (element, ctx) { - this.events.forEach(({x, y}, i) => { - if (ctx.isPointInPath && ctx.isPointInPath(x, y)) { - this.eventElements[i] = element; - } - }); - }, - - checkBoundingBox (element, bb) { - this.events.forEach(({x, y}, i) => { - if (bb.isPointInBox(x, y)) { - this.eventElements[i] = element; - } - }); - }, - - runEvents () { - svg.ctx.canvas.style.cursor = ''; - - this.events.forEach((e, i) => { - let element = this.eventElements[i]; - while (element) { - e.run(element); - element = element.parent; - } - }); - - // done running, clear - this.events = []; - this.eventElements = []; - } - }; - - return svg; -} - -if (typeof CanvasRenderingContext2D !== 'undefined') { - CanvasRenderingContext2D.prototype.drawSvg = function (s, dx, dy, dw, dh) { - canvg(this.canvas, s, { - ignoreMouse: true, - ignoreAnimation: true, - ignoreDimensions: true, - ignoreClear: true, - offsetX: dx, - offsetY: dy, - scaleWidth: dw, - scaleHeight: dh - }); - }; -} diff --git a/src/external/canvg/rgbcolor.js b/src/external/canvg/rgbcolor.js deleted file mode 100644 index caeb912d..00000000 --- a/src/external/canvg/rgbcolor.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * For parsing color values. - * @module RGBColor - * @author Stoyan Stefanov - * @see https://www.phpied.com/rgb-color-parser-in-javascript/ - * @license MIT -*/ -const simpleColors = { - aliceblue: 'f0f8ff', - antiquewhite: 'faebd7', - aqua: '00ffff', - aquamarine: '7fffd4', - azure: 'f0ffff', - beige: 'f5f5dc', - bisque: 'ffe4c4', - black: '000000', - blanchedalmond: 'ffebcd', - blue: '0000ff', - blueviolet: '8a2be2', - brown: 'a52a2a', - burlywood: 'deb887', - cadetblue: '5f9ea0', - chartreuse: '7fff00', - chocolate: 'd2691e', - coral: 'ff7f50', - cornflowerblue: '6495ed', - cornsilk: 'fff8dc', - crimson: 'dc143c', - cyan: '00ffff', - darkblue: '00008b', - darkcyan: '008b8b', - darkgoldenrod: 'b8860b', - darkgray: 'a9a9a9', - darkgreen: '006400', - darkkhaki: 'bdb76b', - darkmagenta: '8b008b', - darkolivegreen: '556b2f', - darkorange: 'ff8c00', - darkorchid: '9932cc', - darkred: '8b0000', - darksalmon: 'e9967a', - darkseagreen: '8fbc8f', - darkslateblue: '483d8b', - darkslategray: '2f4f4f', - darkturquoise: '00ced1', - darkviolet: '9400d3', - deeppink: 'ff1493', - deepskyblue: '00bfff', - dimgray: '696969', - dodgerblue: '1e90ff', - feldspar: 'd19275', - firebrick: 'b22222', - floralwhite: 'fffaf0', - forestgreen: '228b22', - fuchsia: 'ff00ff', - gainsboro: 'dcdcdc', - ghostwhite: 'f8f8ff', - gold: 'ffd700', - goldenrod: 'daa520', - gray: '808080', - green: '008000', - greenyellow: 'adff2f', - honeydew: 'f0fff0', - hotpink: 'ff69b4', - indianred: 'cd5c5c', - indigo: '4b0082', - ivory: 'fffff0', - khaki: 'f0e68c', - lavender: 'e6e6fa', - lavenderblush: 'fff0f5', - lawngreen: '7cfc00', - lemonchiffon: 'fffacd', - lightblue: 'add8e6', - lightcoral: 'f08080', - lightcyan: 'e0ffff', - lightgoldenrodyellow: 'fafad2', - lightgrey: 'd3d3d3', - lightgreen: '90ee90', - lightpink: 'ffb6c1', - lightsalmon: 'ffa07a', - lightseagreen: '20b2aa', - lightskyblue: '87cefa', - lightslateblue: '8470ff', - lightslategray: '778899', - lightsteelblue: 'b0c4de', - lightyellow: 'ffffe0', - lime: '00ff00', - limegreen: '32cd32', - linen: 'faf0e6', - magenta: 'ff00ff', - maroon: '800000', - mediumaquamarine: '66cdaa', - mediumblue: '0000cd', - mediumorchid: 'ba55d3', - mediumpurple: '9370d8', - mediumseagreen: '3cb371', - mediumslateblue: '7b68ee', - mediumspringgreen: '00fa9a', - mediumturquoise: '48d1cc', - mediumvioletred: 'c71585', - midnightblue: '191970', - mintcream: 'f5fffa', - mistyrose: 'ffe4e1', - moccasin: 'ffe4b5', - navajowhite: 'ffdead', - navy: '000080', - oldlace: 'fdf5e6', - olive: '808000', - olivedrab: '6b8e23', - orange: 'ffa500', - orangered: 'ff4500', - orchid: 'da70d6', - palegoldenrod: 'eee8aa', - palegreen: '98fb98', - paleturquoise: 'afeeee', - palevioletred: 'd87093', - papayawhip: 'ffefd5', - peachpuff: 'ffdab9', - peru: 'cd853f', - pink: 'ffc0cb', - plum: 'dda0dd', - powderblue: 'b0e0e6', - purple: '800080', - red: 'ff0000', - rosybrown: 'bc8f8f', - royalblue: '4169e1', - saddlebrown: '8b4513', - salmon: 'fa8072', - sandybrown: 'f4a460', - seagreen: '2e8b57', - seashell: 'fff5ee', - sienna: 'a0522d', - silver: 'c0c0c0', - skyblue: '87ceeb', - slateblue: '6a5acd', - slategray: '708090', - snow: 'fffafa', - springgreen: '00ff7f', - steelblue: '4682b4', - tan: 'd2b48c', - teal: '008080', - thistle: 'd8bfd8', - tomato: 'ff6347', - turquoise: '40e0d0', - violet: 'ee82ee', - violetred: 'd02090', - wheat: 'f5deb3', - white: 'ffffff', - whitesmoke: 'f5f5f5', - yellow: 'ffff00', - yellowgreen: '9acd32' -}; - -// array of color definition objects -const colorDefs = [ - { - re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - // re: /^rgb\((?\d{1,3}),\s*(?\d{1,3}),\s*(?\d{1,3})\)$/, - example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], - process (_, ...bits) { - return bits.map((b) => Number.parseInt(b)); - } - }, - { - re: /^(\w{2})(\w{2})(\w{2})$/, - // re: /^(?\w{2})(?\w{2})(?\w{2})$/, - example: ['#00ff00', '336699'], - process (_, ...bits) { - return bits.map((b) => Number.parseInt(b, 16)); - } - }, - { - re: /^(\w)(\w)(\w)$/, - // re: /^(?\w{1})(?\w{1})(?\w{1})$/, - example: ['#fb0', 'f0f'], - process (_, ...bits) { - return bits.map((b) => Number.parseInt(b + b, 16)); - } - } -]; - -/** - * A class to parse color values. - */ -export default class RGBColor { - /** - * @param {string} colorString - */ - constructor (colorString) { - this.ok = false; - - // strip any leading # - if (colorString.charAt(0) === '#') { // remove # if any - colorString = colorString.substr(1, 6); - } - - colorString = colorString.replace(/ /g, ''); - colorString = colorString.toLowerCase(); - - // before getting into regexps, try simple matches - // and overwrite the input - if (colorString in simpleColors) { - colorString = simpleColors[colorString]; - } - // end of simple type-in colors - - // search through the definitions to find a match - - colorDefs.forEach(({re, process: processor}) => { - const bits = re.exec(colorString); - if (bits) { - const [r, g, b] = processor(...bits); - Object.assign(this, {r, g, b}); - this.ok = true; - } - }); - - // validate/cleanup values - this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); - this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); - this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); - } - - // some getters - /** - * @returns {string} - */ - toRGB () { - return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; - } - - /** - * @returns {string} - */ - toHex () { - let r = this.r.toString(16); - let g = this.g.toString(16); - let b = this.b.toString(16); - if (r.length === 1) { r = '0' + r; } - if (g.length === 1) { g = '0' + g; } - if (b.length === 1) { b = '0' + b; } - return '#' + r + g + b; - } - - /** - * Offers a bulleted list of help. - * @returns {HTMLUListElement} - */ - static getHelpXML () { - const examples = [ - // add regexps - ...colorDefs.flatMap(({example}) => { - return example; - }), - // add type-in colors - ...Object.keys(simpleColors) - ]; - - const xml = document.createElement('ul'); - xml.setAttribute('id', 'rgbcolor-examples'); - - xml.append(...examples.map((example) => { - try { - const listItem = document.createElement('li'); - const listColor = new RGBColor(example); - const exampleDiv = document.createElement('div'); - exampleDiv.style.cssText = ` - margin: 3px; - border: 1px solid black; - background: ${listColor.toHex()}; - color: ${listColor.toHex()};`; - exampleDiv.append('test'); - const listItemValue = ` ${example} -> ${listColor.toRGB()} -> ${listColor.toHex()}`; - listItem.append(exampleDiv, listItemValue); - return listItem; - } catch (e) { - return ''; - } - })); - return xml; - } -} diff --git a/src/external/deparam/deparam.esm.js b/src/external/deparam/deparam.esm.js deleted file mode 100644 index 4846b7d4..00000000 --- a/src/external/deparam/deparam.esm.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Created by alexey2baranov on 28.01.17. - */ -/* - An extraction of the deparam method from Ben Alman's jQuery BBQ - http://benalman.com/projects/jquery-bbq-plugin/ - */ - -const coerce_types = {'true': !0, 'false': !1, 'null': null}; - -function deparam (params, coerce) { - // console.log(params) - const obj = {}; - - // Iterate over all name=value pairs. - params.replace(/\+/g, ' ').split('&').forEach(function (v) { - const param = v.split('='); - - let - key = decodeURIComponent(param[0]), - // If key is more complex than 'foo', like 'a[]' or 'a[b][c]', split it - // into its component parts. - keys = key.split(']['), - keys_last = keys.length - 1; - - // If the first keys part contains [ and the last ends with ], then [] - // are correctly balanced. - if (/\[/.test(keys[0]) && /\]$/.test(keys[keys_last])) { - // Remove the trailing ] from the last keys part. - keys[keys_last] = keys[keys_last].replace(/\]$/, ''); - - // Split first keys part into two parts on the [ and add them back onto - // the beginning of the keys array. - keys = keys.shift().split('[').concat(keys); - - keys_last = keys.length - 1; - } else { - // Basic 'foo' style key. - keys_last = 0; - } - - // Are we dealing with a name=value pair, or just a name? - if (param.length >= 2) { - let val = decodeURIComponent(param.slice(1).join('=')); - - // Coerce values. - if (coerce) { - val = val && !isNaN(val) ? +val // number - : val === 'undefined' ? undefined // undefined - : coerce_types[val] !== undefined ? coerce_types[val] // true, false, null - : val; // string - } - - if (keys_last) { - let cur = obj; - // Complex key, build deep object structure based on a few rules: - // * The 'cur' pointer starts at the object top-level. - // * [] = array push (n is set to array length), [n] = array if n is - // numeric, otherwise object. - // * If at the last keys part, set the value. - // * For each keys part, if the current level is undefined create an - // object or array based on the type of the next keys part. - // * Move the 'cur' pointer to the next level. - // * Rinse & repeat. - for (let i = 0; i <= keys_last; i++) { - key = keys[i] === '' ? cur.length : keys[i]; - cur = cur[key] = i < keys_last - ? cur[key] || (keys[i + 1] && isNaN(keys[i + 1]) ? {} : []) - : val; - } - - } else { - // Simple key, even simpler rules, since only scalars and shallow - // arrays are allowed. - - if (Array.isArray(obj[key])) { - // val is already an array, so push on the next value. - obj[key].push(val); - - } else if (obj[key] !== undefined) { - // val isn't an array, but since a second value has been specified, - // convert val into an array. - obj[key] = [obj[key], val]; - - } else { - // val is a scalar. - obj[key] = val; - } - } - - } else if (key) { - // No value was defined, so set something meaningful. - obj[key] = coerce - ? undefined - : ''; - } - }); - - return obj; -} - -export default deparam; diff --git a/src/external/dom-polyfill/dom-polyfill.js b/src/external/dom-polyfill/dom-polyfill.js deleted file mode 100644 index 73a8e002..00000000 --- a/src/external/dom-polyfill/dom-polyfill.js +++ /dev/null @@ -1,121 +0,0 @@ -// From https://github.com/inexorabletash/polyfill/blob/master/dom.js - -/** - * @module DOMPolyfill - */ - -/** - * - * @param {Node} o - * @param {module:DOMPolyfill~ParentNode|module:DOMPolyfill~ChildNode} ps - * @returns {void} - */ -function mixin (o, ps) { - if (!o) return; - Object.keys(ps).forEach((p) => { - if ((p in o) || (p in o.prototype)) { return; } - try { - Object.defineProperty( - o.prototype, - p, - Object.getOwnPropertyDescriptor(ps, p) - ); - } catch (ex) { - // Throws in IE8; just copy it - o[p] = ps[p]; - } - }); -} - -/** - * - * @param {Node[]} nodes - * @returns {Node} - */ -function convertNodesIntoANode (nodes) { - nodes = nodes.map((node) => { - const isNode = node && typeof node === 'object' && 'nodeType' in node; - return isNode ? node : document.createTextNode(node); - }); - if (nodes.length === 1) { - return nodes[0]; - } - const node = document.createDocumentFragment(); - nodes.forEach((n) => { - // // eslint-disable-next-line unicorn/prefer-node-append - node.appendChild(n); - }); - return node; -} - -const ParentNode = { - prepend (...nodes) { - nodes = convertNodesIntoANode(nodes); - this.insertBefore(nodes, this.firstChild); - }, - append (...nodes) { - nodes = convertNodesIntoANode(nodes); - // // eslint-disable-next-line unicorn/prefer-node-append - this.appendChild(nodes); - } -}; - -mixin(Document || HTMLDocument, ParentNode); // HTMLDocument for IE8 -mixin(DocumentFragment, ParentNode); -mixin(Element, ParentNode); - -// Mixin ChildNode -// https://dom.spec.whatwg.org/#interface-childnode - -const ChildNode = { - before (...nodes) { - const parent = this.parentNode; - if (!parent) return; - let viablePreviousSibling = this.previousSibling; - while (nodes.includes(viablePreviousSibling)) { - viablePreviousSibling = viablePreviousSibling.previousSibling; - } - const node = convertNodesIntoANode(nodes); - parent.insertBefore( - node, - viablePreviousSibling - ? viablePreviousSibling.nextSibling - : parent.firstChild - ); - }, - after (...nodes) { - const parent = this.parentNode; - if (!parent) return; - let viableNextSibling = this.nextSibling; - while (nodes.includes(viableNextSibling)) { - viableNextSibling = viableNextSibling.nextSibling; - } - const node = convertNodesIntoANode(nodes); - // eslint-disable-next-line unicorn/prefer-modern-dom-apis - parent.insertBefore(node, viableNextSibling); - }, - replaceWith (...nodes) { - const parent = this.parentNode; - if (!parent) return; - let viableNextSibling = this.nextSibling; - while (nodes.includes(viableNextSibling)) { - viableNextSibling = viableNextSibling.nextSibling; - } - const node = convertNodesIntoANode(nodes); - - if (this.parentNode === parent) { - parent.replaceChild(node, this); - } else { - // eslint-disable-next-line unicorn/prefer-modern-dom-apis - parent.insertBefore(node, viableNextSibling); - } - }, - remove () { - if (!this.parentNode) { return; } - this.parentNode.removeChild(this); // eslint-disable-line unicorn/prefer-node-remove - } -}; - -mixin(DocumentType, ChildNode); -mixin(Element, ChildNode); -mixin(CharacterData, ChildNode); diff --git a/src/external/jamilih/jml-es.js b/src/external/jamilih/jml-es.js deleted file mode 100644 index 484285ec..00000000 --- a/src/external/jamilih/jml-es.js +++ /dev/null @@ -1,1932 +0,0 @@ -/* -Possible todos: -0. Add XSLT to JML-string stylesheet (or even vice versa) -0. IE problem: Add JsonML code to handle name attribute (during element creation) -0. Element-specific: IE object-param handling - -Todos inspired by JsonML: https://github.com/mckamey/jsonml/blob/master/jsonml-html.js - -0. duplicate attributes? -0. expand ATTR_MAP -0. equivalent of markup, to allow strings to be embedded within an object (e.g., {$value: '
id
'}); advantage over innerHTML in that it wouldn't need to work as the entire contents (nor destroy any existing content or handlers) -0. More validation? -0. JsonML DOM Level 0 listener -0. Whitespace trimming? - -JsonML element-specific: -0. table appending -0. canHaveChildren necessary? (attempts to append to script and img) - -Other Todos: -0. Note to self: Integrate research from other jml notes -0. Allow Jamilih to be seeded with an existing element, so as to be able to add/modify attributes and children -0. Allow array as single first argument -0. Settle on whether need to use null as last argument to return array (or fragment) or other way to allow appending? Options object at end instead to indicate whether returning array, fragment, first element, etc.? -0. Allow building of generic XML (pass configuration object) -0. Allow building content internally as a string (though allowing DOM methods, etc.?) -0. Support JsonML empty string element name to represent fragments? -0. Redo browser testing of jml (including ensuring IE7 can work even if test framework can't work) -*/ -// istanbul ignore next -let win = typeof window !== 'undefined' && window; // istanbul ignore next - -let doc = typeof document !== 'undefined' && document || win && win.document; // STATIC PROPERTIES - -const possibleOptions = ['$plugins', // '$mode', // Todo (SVG/XML) -// '$state', // Used internally -'$map' // Add any other options here -]; -const NS_HTML = 'http://www.w3.org/1999/xhtml', - hyphenForCamelCase = /-([a-z])/gu; -const ATTR_MAP = { - maxlength: 'maxLength', - minlength: 'minLength', - readonly: 'readOnly' -}; // We define separately from ATTR_DOM for clarity (and parity with JsonML) but no current need -// We don't set attribute esp. for boolean atts as we want to allow setting of `undefined` -// (e.g., from an empty variable) on templates to have no effect - -const BOOL_ATTS = ['checked', 'defaultChecked', 'defaultSelected', 'disabled', 'indeterminate', 'open', // Dialog elements -'readOnly', 'selected']; // From JsonML - -const ATTR_DOM = BOOL_ATTS.concat(['accessKey', // HTMLElement -'async', 'autocapitalize', // HTMLElement -'autofocus', 'contentEditable', // HTMLElement through ElementContentEditable -'defaultValue', 'defer', 'draggable', // HTMLElement -'formnovalidate', 'hidden', // HTMLElement -'innerText', // HTMLElement -'inputMode', // HTMLElement through ElementContentEditable -'ismap', 'multiple', 'novalidate', 'pattern', 'required', 'spellcheck', // HTMLElement -'translate', // HTMLElement -'value', 'willvalidate']); // Todo: Add more to this as useful for templating -// to avoid setting through nullish value - -const NULLABLES = ['autocomplete', 'dir', // HTMLElement -'integrity', // script, link -'lang', // HTMLElement -'max', 'min', 'minLength', 'maxLength', 'title' // HTMLElement -]; - -const $ = sel => doc.querySelector(sel); - -const $$ = sel => [...doc.querySelectorAll(sel)]; -/** -* Retrieve the (lower-cased) HTML name of a node. -* @static -* @param {Node} node The HTML node -* @returns {string} The lower-cased node name -*/ - - -function _getHTMLNodeName(node) { - return node.nodeName && node.nodeName.toLowerCase(); -} -/** -* Apply styles if this is a style tag. -* @static -* @param {Node} node The element to check whether it is a style tag -* @returns {void} -*/ - - -function _applyAnyStylesheet(node) { - // Only used in IE - // istanbul ignore else - if (!doc.createStyleSheet) { - return; - } // istanbul ignore next - - - if (_getHTMLNodeName(node) === 'style') { - // IE - const ss = doc.createStyleSheet(); // Create a stylesheet to actually do something useful - - ss.cssText = node.cssText; // We continue to add the style tag, however - } -} -/** - * Need this function for IE since options weren't otherwise getting added. - * @private - * @static - * @param {Element} parent The parent to which to append the element - * @param {Node} child The element or other node to append to the parent - * @returns {void} - */ - - -function _appendNode(parent, child) { - const parentName = _getHTMLNodeName(parent); // IE only - // istanbul ignore if - - - if (doc.createStyleSheet) { - if (parentName === 'script') { - parent.text = child.nodeValue; - return; - } - - if (parentName === 'style') { - parent.cssText = child.nodeValue; // This will not apply it--just make it available within the DOM cotents - - return; - } - } - - if (parentName === 'template') { - parent.content.append(child); - return; - } - - try { - parent.append(child); // IE9 is now ok with this - } catch (e) { - // istanbul ignore next - const childName = _getHTMLNodeName(child); // istanbul ignore next - - - if (parentName === 'select' && childName === 'option') { - try { - // Since this is now DOM Level 4 standard behavior (and what IE7+ can handle), we try it first - parent.add(child); - } catch (err) { - // DOM Level 2 did require a second argument, so we try it too just in case the user is using an older version of Firefox, etc. - parent.add(child, null); // IE7 has a problem with this, but IE8+ is ok - } - - return; - } // istanbul ignore next - - - throw e; - } -} -/** - * Attach event in a cross-browser fashion. - * @static - * @param {Element} el DOM element to which to attach the event - * @param {string} type The DOM event (without 'on') to attach to the element - * @param {EventListener} handler The event handler to attach to the element - * @param {boolean} [capturing] Whether or not the event should be - * capturing (W3C-browsers only); default is false; NOT IN USE - * @returns {void} - */ - - -function _addEvent(el, type, handler, capturing) { - el.addEventListener(type, handler, Boolean(capturing)); -} -/** -* Creates a text node of the result of resolving an entity or character reference. -* @param {'entity'|'decimal'|'hexadecimal'} type Type of reference -* @param {string} prefix Text to prefix immediately after the "&" -* @param {string} arg The body of the reference -* @returns {Text} The text node of the resolved reference -*/ - - -function _createSafeReference(type, prefix, arg) { - // For security reasons related to innerHTML, we ensure this string only - // contains potential entity characters - if (!arg.match(/^\w+$/u)) { - throw new TypeError(`Bad ${type} reference; with prefix "${prefix}" and arg "${arg}"`); - } - - const elContainer = doc.createElement('div'); // Todo: No workaround for XML? - // eslint-disable-next-line no-unsanitized/property - - elContainer.innerHTML = '&' + prefix + arg + ';'; - return doc.createTextNode(elContainer.innerHTML); -} -/** -* @param {string} n0 Whole expression match (including "-") -* @param {string} n1 Lower-case letter match -* @returns {string} Uppercased letter -*/ - - -function _upperCase(n0, n1) { - return n1.toUpperCase(); -} // Todo: Make as public utility - -/** - * @param {any} o - * @returns {boolean} - */ - - -function _isNullish(o) { - return o === null || o === undefined; -} // Todo: Make as public utility, but also return types for undefined, boolean, number, document, etc. - -/** -* @private -* @static -* @param {string|JamilihAttributes|JamilihArray|Element|DocumentFragment} item -* @returns {"string"|"null"|"array"|"element"|"fragment"|"object"|"symbol"|"function"|"number"|"boolean"} -*/ - - -function _getType(item) { - const type = typeof item; - - switch (type) { - case 'object': - if (item === null) { - return 'null'; - } - - if (Array.isArray(item)) { - return 'array'; - } - - if ('nodeType' in item) { - switch (item.nodeType) { - case 1: - return 'element'; - - case 9: - return 'document'; - - case 11: - return 'fragment'; - - default: - return 'non-container node'; - } - } - - // Fallthrough - - default: - return type; - } -} -/** -* @private -* @static -* @param {DocumentFragment} frag -* @param {Node} node -* @returns {DocumentFragment} -*/ - - -function _fragReducer(frag, node) { - frag.append(node); - return frag; -} -/** -* @private -* @static -* @param {Object<{string:string}>} xmlnsObj -* @returns {string} -*/ - - -function _replaceDefiner(xmlnsObj) { - return function (n0) { - let retStr = xmlnsObj[''] ? ' xmlns="' + xmlnsObj[''] + '"' : n0; // Preserve XHTML - - for (const [ns, xmlnsVal] of Object.entries(xmlnsObj)) { - if (ns !== '') { - retStr += ' xmlns:' + ns + '="' + xmlnsVal + '"'; - } - } - - return retStr; - }; -} -/** -* @typedef {JamilihAttributes} AttributeArray -* @property {string} 0 The key -* @property {string} 1 The value -*/ - -/** -* @callback ChildrenToJMLCallback -* @param {JamilihArray|Jamilih} childNodeJML -* @param {Integer} i -* @returns {void} -*/ - -/** -* @private -* @static -* @param {Node} node -* @returns {ChildrenToJMLCallback} -*/ - - -function _childrenToJML(node) { - return function (childNodeJML, i) { - const cn = node.childNodes[i]; - const j = Array.isArray(childNodeJML) ? jml(...childNodeJML) : jml(childNodeJML); - cn.replaceWith(j); - }; -} -/** -* @callback JamilihAppender -* @param {JamilihArray} childJML -* @returns {void} -*/ - -/** -* @private -* @static -* @param {Node} node -* @returns {JamilihAppender} -*/ - - -function _appendJML(node) { - return function (childJML) { - if (Array.isArray(childJML)) { - node.append(jml(...childJML)); - } else { - node.append(jml(childJML)); - } - }; -} -/** -* @callback appender -* @param {string|JamilihArray} childJML -* @returns {void} -*/ - -/** -* @private -* @static -* @param {Node} node -* @returns {appender} -*/ - - -function _appendJMLOrText(node) { - return function (childJML) { - if (typeof childJML === 'string') { - node.append(childJML); - } else if (Array.isArray(childJML)) { - node.append(jml(...childJML)); - } else { - node.append(jml(childJML)); - } - }; -} -/** -* @private -* @static -*/ - -/* -function _DOMfromJMLOrString (childNodeJML) { - if (typeof childNodeJML === 'string') { - return doc.createTextNode(childNodeJML); - } - return jml(...childNodeJML); -} -*/ - -/** -* @typedef {Element|DocumentFragment} JamilihReturn -*/ - -/** -* @typedef {PlainObject} JamilihAttributes -*/ - -/** -* @typedef {GenericArray} JamilihArray -* @property {string} 0 The element to create (by lower-case name) -* @property {JamilihAttributes} [1] Attributes to add with the key as the -* attribute name and value as the attribute value; important for IE where -* the input element's type cannot be added later after already added to the page -* @param {Element[]} [children] The optional children of this element -* (but raw DOM elements required to be specified within arrays since -* could not otherwise be distinguished from siblings being added) -* @param {Element} [parent] The optional parent to which to attach the element -* (always the last unless followed by null, in which case it is the -* second-to-last) -* @param {null} [returning] Can use null to indicate an array of elements -* should be returned -*/ - -/** -* @typedef {PlainObject} JamilihOptions -* @property {"root"|"attributeValue"|"fragment"|"children"|"fragmentChildren"} $state -*/ - -/** - * @param {Element} elem - * @param {string} att - * @param {string} attVal - * @param {JamilihOptions} opts - * @returns {void} - */ - - -function checkPluginValue(elem, att, attVal, opts) { - opts.$state = 'attributeValue'; - - if (attVal && typeof attVal === 'object') { - const matchingPlugin = getMatchingPlugin(opts, Object.keys(attVal)[0]); - - if (matchingPlugin) { - return matchingPlugin.set({ - opts, - element: elem, - attribute: { - name: att, - value: attVal - } - }); - } - } - - return attVal; -} -/** - * @param {JamilihOptions} opts - * @param {string} item - * @returns {JamilihPlugin} - */ - - -function getMatchingPlugin(opts, item) { - return opts.$plugins && opts.$plugins.find(p => { - return p.name === item; - }); -} -/** - * Creates an XHTML or HTML element (XHTML is preferred, but only in browsers - * that support); any element after element can be omitted, and any subsequent - * type or types added afterwards. - * @param {...JamilihArray} args - * @returns {JamilihReturn} The newly created (and possibly already appended) - * element or array of elements - */ - - -const jml = function jml(...args) { - let elem = doc.createDocumentFragment(); - /** - * - * @param {Object<{string: string}>} atts - * @returns {void} - */ - - function _checkAtts(atts) { - for (let [att, attVal] of Object.entries(atts)) { - att = att in ATTR_MAP ? ATTR_MAP[att] : att; - - if (NULLABLES.includes(att)) { - attVal = checkPluginValue(elem, att, attVal, opts); - - if (!_isNullish(attVal)) { - elem[att] = attVal; - } - - continue; - } else if (ATTR_DOM.includes(att)) { - attVal = checkPluginValue(elem, att, attVal, opts); - elem[att] = attVal; - continue; - } - - switch (att) { - /* - Todos: - 0. JSON mode to prevent event addition - 0. {$xmlDocument: []} // doc.implementation.createDocument - 0. Accept array for any attribute with first item as prefix and second as value? - 0. {$: ['xhtml', 'div']} for prefixed elements - case '$': // Element with prefix? - nodes[nodes.length] = elem = doc.createElementNS(attVal[0], attVal[1]); - break; - */ - case '#': - { - // Document fragment - opts.$state = 'fragmentChilden'; - nodes[nodes.length] = jml(opts, attVal); - break; - } - - case '$shadow': - { - const { - open, - closed - } = attVal; - let { - content, - template - } = attVal; - const shadowRoot = elem.attachShadow({ - mode: closed || open === false ? 'closed' : 'open' - }); - - if (template) { - if (Array.isArray(template)) { - if (_getType(template[0]) === 'object') { - // Has attributes - template = jml('template', ...template, doc.body); - } else { - // Array is for the children - template = jml('template', template, doc.body); - } - } else if (typeof template === 'string') { - template = $(template); - } - - jml(template.content.cloneNode(true), shadowRoot); - } else { - if (!content) { - content = open || closed; - } - - if (content && typeof content !== 'boolean') { - if (Array.isArray(content)) { - jml({ - '#': content - }, shadowRoot); - } else { - jml(content, shadowRoot); - } - } - } - - break; - } - - case '$state': - { - // Handled internally - break; - } - - case 'is': - { - // Currently only in Chrome - // Handled during element creation - break; - } - - case '$custom': - { - Object.assign(elem, attVal); - break; - } - - /* istanbul ignore next */ - - case '$define': - { - const localName = elem.localName.toLowerCase(); // Note: customized built-ins sadly not working yet - - const customizedBuiltIn = !localName.includes('-'); // We check attribute in case this is a preexisting DOM element - // const {is} = atts; - - let is; - - if (customizedBuiltIn) { - is = elem.getAttribute('is'); - - if (!is) { - if (!{}.hasOwnProperty.call(atts, 'is')) { - throw new TypeError(`Expected \`is\` with \`$define\` on built-in; args: ${JSON.stringify(args)}`); - } - - atts.is = checkPluginValue(elem, 'is', atts.is, opts); - elem.setAttribute('is', atts.is); - ({ - is - } = atts); - } - } - - const def = customizedBuiltIn ? is : localName; - - if (window.customElements.get(def)) { - break; - } - - const getConstructor = cnstrct => { - const baseClass = options && options.extends ? doc.createElement(options.extends).constructor : customizedBuiltIn ? doc.createElement(localName).constructor : window.HTMLElement; - /** - * Class wrapping base class. - */ - - return cnstrct ? class extends baseClass { - /** - * Calls user constructor. - */ - constructor() { - super(); - cnstrct.call(this); - } - - } : class extends baseClass {}; - }; - - let cnstrctr, options, mixin; - - if (Array.isArray(attVal)) { - if (attVal.length <= 2) { - [cnstrctr, options] = attVal; - - if (typeof options === 'string') { - // Todo: Allow creating a definition without using it; - // that may be the only reason to have a string here which - // differs from the `localName` anyways - options = { - extends: options - }; - } else if (options && !{}.hasOwnProperty.call(options, 'extends')) { - mixin = options; - } - - if (typeof cnstrctr === 'object') { - mixin = cnstrctr; - cnstrctr = getConstructor(); - } - } else { - [cnstrctr, mixin, options] = attVal; - - if (typeof options === 'string') { - options = { - extends: options - }; - } - } - } else if (typeof attVal === 'function') { - cnstrctr = attVal; - } else { - mixin = attVal; - cnstrctr = getConstructor(); - } - - if (!cnstrctr.toString().startsWith('class')) { - cnstrctr = getConstructor(cnstrctr); - } - - if (!options && customizedBuiltIn) { - options = { - extends: localName - }; - } - - if (mixin) { - Object.entries(mixin).forEach(([methodName, method]) => { - cnstrctr.prototype[methodName] = method; - }); - } // console.log('def', def, '::', typeof options === 'object' ? options : undefined); - - - window.customElements.define(def, cnstrctr, typeof options === 'object' ? options : undefined); - break; - } - - case '$symbol': - { - const [symbol, func] = attVal; - - if (typeof func === 'function') { - const funcBound = func.bind(elem); - - if (typeof symbol === 'string') { - elem[Symbol.for(symbol)] = funcBound; - } else { - elem[symbol] = funcBound; - } - } else { - const obj = func; - obj.elem = elem; - - if (typeof symbol === 'string') { - elem[Symbol.for(symbol)] = obj; - } else { - elem[symbol] = obj; - } - } - - break; - } - - case '$data': - { - setMap(attVal); - break; - } - - case '$attribute': - { - // Attribute node - const node = attVal.length === 3 ? doc.createAttributeNS(attVal[0], attVal[1]) : doc.createAttribute(attVal[0]); - node.value = attVal[attVal.length - 1]; - nodes[nodes.length] = node; - break; - } - - case '$text': - { - // Todo: Also allow as jml(['a text node']) (or should that become a fragment)? - const node = doc.createTextNode(attVal); - nodes[nodes.length] = node; - break; - } - - case '$document': - { - // Todo: Conditionally create XML document - const node = doc.implementation.createHTMLDocument(); - - if (attVal.childNodes) { - // Remove any extra nodes created by createHTMLDocument(). - const j = attVal.childNodes.length; - - while (node.childNodes[j]) { - const cn = node.childNodes[j]; - cn.remove(); // `j` should stay the same as removing will cause node to be present - } // eslint-disable-next-line unicorn/no-fn-reference-in-iterator - - - attVal.childNodes.forEach(_childrenToJML(node)); - } else { - if (attVal.$DOCTYPE) { - const dt = { - $DOCTYPE: attVal.$DOCTYPE - }; - const doctype = jml(dt); - node.firstChild.replaceWith(doctype); - } - - const html = node.childNodes[1]; - const head = html.childNodes[0]; - const body = html.childNodes[1]; - - if (attVal.title || attVal.head) { - const meta = doc.createElement('meta'); - meta.setAttribute('charset', 'utf-8'); - head.append(meta); - - if (attVal.title) { - node.title = attVal.title; // Appends after meta - } - - if (attVal.head) { - // eslint-disable-next-line unicorn/no-fn-reference-in-iterator - attVal.head.forEach(_appendJML(head)); - } - } - - if (attVal.body) { - // eslint-disable-next-line unicorn/no-fn-reference-in-iterator - attVal.body.forEach(_appendJMLOrText(body)); - } - } - - nodes[nodes.length] = node; - break; - } - - case '$DOCTYPE': - { - const node = doc.implementation.createDocumentType(attVal.name, attVal.publicId || '', attVal.systemId || ''); - nodes[nodes.length] = node; - break; - } - - case '$on': - { - // Events - // Allow for no-op by defaulting to `{}` - for (let [p2, val] of Object.entries(attVal || {})) { - if (typeof val === 'function') { - val = [val, false]; - } - - if (typeof val[0] !== 'function') { - throw new TypeError(`Expect a function for \`$on\`; args: ${JSON.stringify(args)}`); - } - - _addEvent(elem, p2, val[0], val[1]); // element, event name, handler, capturing - - } - - break; - } - - case 'className': - case 'class': - attVal = checkPluginValue(elem, att, attVal, opts); - - if (!_isNullish(attVal)) { - elem.className = attVal; - } - - break; - - case 'dataset': - { - // Map can be keyed with hyphenated or camel-cased properties - const recurse = (atVal, startProp) => { - let prop = ''; - const pastInitialProp = startProp !== ''; - Object.keys(atVal).forEach(key => { - const value = atVal[key]; - - if (pastInitialProp) { - prop = startProp + key.replace(hyphenForCamelCase, _upperCase).replace(/^([a-z])/u, _upperCase); - } else { - prop = startProp + key.replace(hyphenForCamelCase, _upperCase); - } - - if (value === null || typeof value !== 'object') { - if (!_isNullish(value)) { - elem.dataset[prop] = value; - } - - prop = startProp; - return; - } - - recurse(value, prop); - }); - }; - - recurse(attVal, ''); - break; // Todo: Disable this by default unless configuration explicitly allows (for security) - } - // #if IS_REMOVE - // Don't remove this `if` block (for sake of no-innerHTML build) - - case 'innerHTML': - if (!_isNullish(attVal)) { - // eslint-disable-next-line no-unsanitized/property - elem.innerHTML = attVal; - } - - break; - // #endif - - case 'htmlFor': - case 'for': - if (elStr === 'label') { - attVal = checkPluginValue(elem, att, attVal, opts); - - if (!_isNullish(attVal)) { - elem.htmlFor = attVal; - } - - break; - } - - attVal = checkPluginValue(elem, att, attVal, opts); - elem.setAttribute(att, attVal); - break; - - case 'xmlns': - // Already handled - break; - - default: - { - if (att.startsWith('on')) { - attVal = checkPluginValue(elem, att, attVal, opts); - elem[att] = attVal; // _addEvent(elem, att.slice(2), attVal, false); // This worked, but perhaps the user wishes only one event - - break; - } - - if (att === 'style') { - attVal = checkPluginValue(elem, att, attVal, opts); - - if (_isNullish(attVal)) { - break; - } - - if (typeof attVal === 'object') { - for (const [p2, styleVal] of Object.entries(attVal)) { - if (!_isNullish(styleVal)) { - // Todo: Handle aggregate properties like "border" - if (p2 === 'float') { - elem.style.cssFloat = styleVal; - elem.style.styleFloat = styleVal; // Harmless though we could make conditional on older IE instead - } else { - elem.style[p2.replace(hyphenForCamelCase, _upperCase)] = styleVal; - } - } - } - - break; - } // setAttribute unfortunately erases any existing styles - - - elem.setAttribute(att, attVal); - /* - // The following reorders which is troublesome for serialization, e.g., as used in our testing - if (elem.style.cssText !== undefined) { - elem.style.cssText += attVal; - } else { // Opera - elem.style += attVal; - } - */ - - break; - } - - const matchingPlugin = getMatchingPlugin(opts, att); - - if (matchingPlugin) { - matchingPlugin.set({ - opts, - element: elem, - attribute: { - name: att, - value: attVal - } - }); - break; - } - - attVal = checkPluginValue(elem, att, attVal, opts); - elem.setAttribute(att, attVal); - break; - } - } - } - } - - const nodes = []; - let elStr; - let opts; - let isRoot = false; - - if (_getType(args[0]) === 'object' && Object.keys(args[0]).some(key => possibleOptions.includes(key))) { - opts = args[0]; - - if (opts.$state === undefined) { - isRoot = true; - opts.$state = 'root'; - } - - if (opts.$map && !opts.$map.root && opts.$map.root !== false) { - opts.$map = { - root: opts.$map - }; - } - - if ('$plugins' in opts) { - if (!Array.isArray(opts.$plugins)) { - throw new TypeError(`\`$plugins\` must be an array; args: ${JSON.stringify(args)}`); - } - - opts.$plugins.forEach(pluginObj => { - if (!pluginObj || typeof pluginObj !== 'object') { - throw new TypeError(`Plugin must be an object; args: ${JSON.stringify(args)}`); - } - - if (!pluginObj.name || !pluginObj.name.startsWith('$_')) { - throw new TypeError(`Plugin object name must be present and begin with \`$_\`; args: ${JSON.stringify(args)}`); - } - - if (typeof pluginObj.set !== 'function') { - throw new TypeError(`Plugin object must have a \`set\` method; args: ${JSON.stringify(args)}`); - } - }); - } - - args = args.slice(1); - } else { - opts = { - $state: undefined - }; - } - - const argc = args.length; - const defaultMap = opts.$map && opts.$map.root; - - const setMap = dataVal => { - let map, obj; // Boolean indicating use of default map and object - - if (dataVal === true) { - [map, obj] = defaultMap; - } else if (Array.isArray(dataVal)) { - // Array of strings mapping to default - if (typeof dataVal[0] === 'string') { - dataVal.forEach(dVal => { - setMap(opts.$map[dVal]); - }); - return; // Array of Map and non-map data object - } - - map = dataVal[0] || defaultMap[0]; - obj = dataVal[1] || defaultMap[1]; // Map - } else if (/^\[object (?:Weak)?Map\]$/u.test([].toString.call(dataVal))) { - map = dataVal; - obj = defaultMap[1]; // Non-map data object - } else { - map = defaultMap[0]; - obj = dataVal; - } - - map.set(elem, obj); - }; - - for (let i = 0; i < argc; i++) { - let arg = args[i]; - - const type = _getType(arg); - - switch (type) { - default: - throw new TypeError(`Unexpected type: ${type}; arg: ${arg}; index ${i} on args: ${JSON.stringify(args)}`); - - case 'null': - // null always indicates a place-holder (only needed for last argument if want array returned) - if (i === argc - 1) { - _applyAnyStylesheet(nodes[0]); // We have to execute any stylesheets even if not appending or otherwise IE will never apply them - // Todo: Fix to allow application of stylesheets of style tags within fragments? - - - return nodes.length <= 1 ? nodes[0] // eslint-disable-next-line unicorn/no-fn-reference-in-iterator - : nodes.reduce(_fragReducer, doc.createDocumentFragment()); // nodes; - } - - throw new TypeError(`\`null\` values not allowed except as final Jamilih argument; index ${i} on args: ${JSON.stringify(args)}`); - - case 'string': - // Strings normally indicate elements - switch (arg) { - case '!': - nodes[nodes.length] = doc.createComment(args[++i]); - break; - - case '?': - { - arg = args[++i]; - let procValue = args[++i]; - const val = procValue; - - if (val && typeof val === 'object') { - procValue = []; - - for (const [p, procInstVal] of Object.entries(val)) { - procValue.push(p + '=' + '"' + // https://www.w3.org/TR/xml-stylesheet/#NT-PseudoAttValue - procInstVal.replace(/"/gu, '"') + '"'); - } - - procValue = procValue.join(' '); - } // Firefox allows instructions with ">" in this method, but not if placed directly! - - - try { - nodes[nodes.length] = doc.createProcessingInstruction(arg, procValue); - } catch (e) { - // Getting NotSupportedError in IE, so we try to imitate a processing instruction with a comment - // innerHTML didn't work - // var elContainer = doc.createElement('div'); - // elContainer.innerHTML = ''; - // nodes[nodes.length] = elContainer.innerHTML; - // Todo: any other way to resolve? Just use XML? - nodes[nodes.length] = doc.createComment('?' + arg + ' ' + procValue + '?'); - } - - break; // Browsers don't support doc.createEntityReference, so we just use this as a convenience - } - - case '&': - nodes[nodes.length] = _createSafeReference('entity', '', args[++i]); - break; - - case '#': - // // Decimal character reference - ['#', '01234'] // Ӓ // probably easier to use JavaScript Unicode escapes - nodes[nodes.length] = _createSafeReference('decimal', arg, String(args[++i])); - break; - - case '#x': - // Hex character reference - ['#x', '123a'] // ሺ // probably easier to use JavaScript Unicode escapes - nodes[nodes.length] = _createSafeReference('hexadecimal', arg, args[++i]); - break; - - case '![': - // '![', ['escaped <&> text'] // text]]> - // CDATA valid in XML only, so we'll just treat as text for mutual compatibility - // Todo: config (or detection via some kind of doc.documentType property?) of whether in XML - try { - nodes[nodes.length] = doc.createCDATASection(args[++i]); - } catch (e2) { - nodes[nodes.length] = doc.createTextNode(args[i]); // i already incremented - } - - break; - - case '': - nodes[nodes.length] = elem = doc.createDocumentFragment(); // Todo: Report to plugins - - opts.$state = 'fragment'; - break; - - default: - { - // An element - elStr = arg; - const atts = args[i + 1]; - - if (_getType(atts) === 'object' && atts.is) { - const { - is - } = atts; // istanbul ignore else - - if (doc.createElementNS) { - elem = doc.createElementNS(NS_HTML, elStr, { - is - }); - } else { - elem = doc.createElement(elStr, { - is - }); - } - } else - /* istanbul ignore else */ - if (doc.createElementNS) { - elem = doc.createElementNS(NS_HTML, elStr); - } else { - elem = doc.createElement(elStr); - } // Todo: Report to plugins - - - opts.$state = 'element'; - nodes[nodes.length] = elem; // Add to parent - - break; - } - } - - break; - - case 'object': - { - // Non-DOM-element objects indicate attribute-value pairs - const atts = arg; - - if (atts.xmlns !== undefined) { - // We handle this here, as otherwise may lose events, etc. - // As namespace of element already set as XHTML, we need to change the namespace - // elem.setAttribute('xmlns', atts.xmlns); // Doesn't work - // Can't set namespaceURI dynamically, renameNode() is not supported, and setAttribute() doesn't work to change the namespace, so we resort to this hack - let replacer; - - if (typeof atts.xmlns === 'object') { - replacer = _replaceDefiner(atts.xmlns); - } else { - replacer = ' xmlns="' + atts.xmlns + '"'; - } // try { - // Also fix DOMParser to work with text/html - - - elem = nodes[nodes.length - 1] = new win.DOMParser().parseFromString(new win.XMLSerializer().serializeToString(elem) // Mozilla adds XHTML namespace - .replace(' xmlns="' + NS_HTML + '"', replacer), 'application/xml').documentElement; // Todo: Report to plugins - - opts.$state = 'element'; // }catch(e) {alert(elem.outerHTML);throw e;} - } - - _checkAtts(atts); - - break; - } - - case 'document': - case 'fragment': - case 'element': - /* - 1) Last element always the parent (put null if don't want parent and want to return array) unless only atts and children (no other elements) - 2) Individual elements (DOM elements or sequences of string[/object/array]) get added to parent first-in, first-added - */ - if (i === 0) { - // Allow wrapping of element, fragment, or document - elem = arg; // Todo: Report to plugins - - opts.$state = 'element'; - } - - if (i === argc - 1 || i === argc - 2 && args[i + 1] === null) { - // parent - const elsl = nodes.length; - - for (let k = 0; k < elsl; k++) { - _appendNode(arg, nodes[k]); - } // Todo: Apply stylesheets if any style tags were added elsewhere besides the first element? - - - _applyAnyStylesheet(nodes[0]); // We have to execute any stylesheets even if not appending or otherwise IE will never apply them - - } else { - nodes[nodes.length] = arg; - } - - break; - - case 'array': - { - // Arrays or arrays of arrays indicate child nodes - const child = arg; - const cl = child.length; - - for (let j = 0; j < cl; j++) { - // Go through children array container to handle elements - const childContent = child[j]; - const childContentType = typeof childContent; - - if (_isNullish(childContent)) { - throw new TypeError(`Bad children (parent array: ${JSON.stringify(args)}; index ${j} of child: ${JSON.stringify(child)})`); - } - - switch (childContentType) { - // Todo: determine whether null or function should have special handling or be converted to text - case 'string': - case 'number': - case 'boolean': - _appendNode(elem, doc.createTextNode(childContent)); - - break; - - default: - if (Array.isArray(childContent)) { - // Arrays representing child elements - opts.$state = 'children'; - - _appendNode(elem, jml(opts, ...childContent)); - } else if (childContent['#']) { - // Fragment - opts.$state = 'fragmentChildren'; - - _appendNode(elem, jml(opts, childContent['#'])); - } else { - // Single DOM element children - const newChildContent = checkPluginValue(elem, null, childContent, opts); - - _appendNode(elem, newChildContent); - } - - break; - } - } - - break; - } - } - } - - const ret = nodes[0] || elem; - - if (isRoot && opts.$map && opts.$map.root) { - setMap(true); - } - - return ret; -}; -/** -* Converts a DOM object or a string of HTML into a Jamilih object (or string). -* @param {string|HTMLElement} dom If a string, will parse as document -* @param {PlainObject} [config] Configuration object -* @param {boolean} [config.stringOutput=false] Whether to output the Jamilih object as a string. -* @param {boolean} [config.reportInvalidState=true] If true (the default), will report invalid state errors -* @param {boolean} [config.stripWhitespace=false] Strip whitespace for text nodes -* @returns {JamilihArray|string} Array containing the elements which represent -* a Jamilih object, or, if `stringOutput` is true, it will be the stringified -* version of such an object -*/ - - -jml.toJML = function (dom, { - stringOutput = false, - reportInvalidState = true, - stripWhitespace = false -} = {}) { - if (typeof dom === 'string') { - dom = new win.DOMParser().parseFromString(dom, 'text/html'); // todo: Give option for XML once implemented and change JSDoc to allow for Element - } - - const ret = []; - let parent = ret; - let parentIdx = 0; - /** - * @param {string} msg - * @throws {DOMException} - * @returns {void} - */ - - function invalidStateError(msg) { - // These are probably only necessary if working with text/html - - /* eslint-disable no-shadow, unicorn/custom-error-definition */ - - /** - * Polyfill for `DOMException`. - */ - class DOMException extends Error { - /* eslint-enable no-shadow, unicorn/custom-error-definition */ - - /** - * @param {string} message - * @param {string} name - */ - constructor(message, name) { - super(message); // eslint-disable-next-line unicorn/custom-error-definition - - this.name = name; - } - - } - - if (reportInvalidState) { - // INVALID_STATE_ERR per section 9.3 XHTML 5: http://www.w3.org/TR/html5/the-xhtml-syntax.html - const e = new DOMException(msg, 'INVALID_STATE_ERR'); - e.code = 11; - throw e; - } - } - /** - * - * @param {DocumentType|Entity} obj - * @param {Node} node - * @returns {void} - */ - - - function addExternalID(obj, node) { - if (node.systemId.includes('"') && node.systemId.includes("'")) { - invalidStateError('systemId cannot have both single and double quotes.'); - } - - const { - publicId, - systemId - } = node; - - if (systemId) { - obj.systemId = systemId; - } - - if (publicId) { - obj.publicId = publicId; - } - } - /** - * - * @param {any} val - * @returns {void} - */ - - - function set(val) { - parent[parentIdx] = val; - parentIdx++; - } - /** - * @returns {void} - */ - - - function setChildren() { - set([]); - parent = parent[parentIdx - 1]; - parentIdx = 0; - } - /** - * - * @param {string} prop1 - * @param {string} prop2 - * @returns {void} - */ - - - function setObj(prop1, prop2) { - parent = parent[parentIdx - 1][prop1]; - parentIdx = 0; - - if (prop2) { - parent = parent[prop2]; - } - } - /** - * - * @param {Node} node - * @param {object<{string: string}>} namespaces - * @returns {void} - */ - - - function parseDOM(node, namespaces) { - // namespaces = clone(namespaces) || {}; // Ensure we're working with a copy, so different levels in the hierarchy can treat it differently - - /* - if ((node.prefix && node.prefix.includes(':')) || (node.localName && node.localName.includes(':'))) { - invalidStateError('Prefix cannot have a colon'); - } - */ - const type = 'nodeType' in node ? node.nodeType : null; - namespaces = { ...namespaces - }; - const xmlChars = /^([\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/u; // eslint-disable-line no-control-regex - - if ([2, 3, 4, 7, 8].includes(type) && !xmlChars.test(node.nodeValue)) { - invalidStateError('Node has bad XML character value'); - } - - let tmpParent, tmpParentIdx; - /** - * @returns {void} - */ - - function setTemp() { - tmpParent = parent; - tmpParentIdx = parentIdx; - } - /** - * @returns {void} - */ - - - function resetTemp() { - parent = tmpParent; - parentIdx = tmpParentIdx; - parentIdx++; // Increment index in parent container of this element - } - - switch (type) { - case 1: - { - // ELEMENT - setTemp(); - const nodeName = node.nodeName.toLowerCase(); // Todo: for XML, should not lower-case - - setChildren(); // Build child array since elements are, except at the top level, encapsulated in arrays - - set(nodeName); - const start = {}; - let hasNamespaceDeclaration = false; - - if (namespaces[node.prefix || ''] !== node.namespaceURI) { - namespaces[node.prefix || ''] = node.namespaceURI; - - if (node.prefix) { - start['xmlns:' + node.prefix] = node.namespaceURI; - } else if (node.namespaceURI) { - start.xmlns = node.namespaceURI; - } else { - start.xmlns = null; - } - - hasNamespaceDeclaration = true; - } - - if (node.attributes.length) { - set([...node.attributes].reduce(function (obj, att) { - obj[att.name] = att.value; // Attr.nodeName and Attr.nodeValue are deprecated as of DOM4 as Attr no longer inherits from Node, so we can safely use name and value - - return obj; - }, start)); - } else if (hasNamespaceDeclaration) { - set(start); - } - - const { - childNodes - } = node; - - if (childNodes.length) { - setChildren(); // Element children array container - - [...childNodes].forEach(function (childNode) { - parseDOM(childNode, namespaces); - }); - } - - resetTemp(); - break; - } - - case undefined: // Treat as attribute node until this is fixed: https://github.com/jsdom/jsdom/issues/1641 / https://github.com/jsdom/jsdom/pull/1822 - - case 2: - // ATTRIBUTE (should only get here if passing in an attribute node) - set({ - $attribute: [node.namespaceURI, node.name, node.value] - }); - break; - - case 3: - // TEXT - if (stripWhitespace && /^\s+$/u.test(node.nodeValue)) { - set(''); - return; - } - - set(node.nodeValue); - break; - - case 4: - // CDATA - if (node.nodeValue.includes(']]' + '>')) { - invalidStateError('CDATA cannot end with closing ]]>'); - } - - set(['![', node.nodeValue]); - break; - - case 5: - // ENTITY REFERENCE (though not in browsers (was already resolved - // anyways), ok to keep for parity with our "entity" shorthand) - set(['&', node.nodeName]); - break; - - case 7: - // PROCESSING INSTRUCTION - if (/^xml$/iu.test(node.target)) { - invalidStateError('Processing instructions cannot be "xml".'); - } - - if (node.target.includes('?>')) { - invalidStateError('Processing instruction targets cannot include ?>'); - } - - if (node.target.includes(':')) { - invalidStateError('The processing instruction target cannot include ":"'); - } - - if (node.data.includes('?>')) { - invalidStateError('Processing instruction data cannot include ?>'); - } - - set(['?', node.target, node.data]); // Todo: Could give option to attempt to convert value back into object if has pseudo-attributes - - break; - - case 8: - // COMMENT - if (node.nodeValue.includes('--') || node.nodeValue.length && node.nodeValue.lastIndexOf('-') === node.nodeValue.length - 1) { - invalidStateError('Comments cannot include --'); - } - - set(['!', node.nodeValue]); - break; - - case 9: - { - // DOCUMENT - setTemp(); - const docObj = { - $document: { - childNodes: [] - } - }; - set(docObj); // doc.implementation.createHTMLDocument - // Set position to fragment's array children - - setObj('$document', 'childNodes'); - const { - childNodes - } = node; - - if (!childNodes.length) { - invalidStateError('Documents must have a child node'); - } // set({$xmlDocument: []}); // doc.implementation.createDocument // Todo: use this conditionally - - - [...childNodes].forEach(function (childNode) { - // Can't just do documentElement as there may be doctype, comments, etc. - // No need for setChildren, as we have already built the container array - parseDOM(childNode, namespaces); - }); - resetTemp(); - break; - } - - case 10: - { - // DOCUMENT TYPE - setTemp(); // Can create directly by doc.implementation.createDocumentType - - const start = { - $DOCTYPE: { - name: node.name - } - }; - const pubIdChar = /^(\u0020|\u000D|\u000A|[a-zA-Z0-9]|[-'()+,./:=?;!*#@$_%])*$/u; // eslint-disable-line no-control-regex - - if (!pubIdChar.test(node.publicId)) { - invalidStateError('A publicId must have valid characters.'); - } - - addExternalID(start.$DOCTYPE, node); // Fit in internal subset along with entities?: probably don't need as these would only differ if from DTD, and we're not rebuilding the DTD - - set(start); // Auto-generate the internalSubset instead? - - resetTemp(); - break; - } - - case 11: - { - // DOCUMENT FRAGMENT - setTemp(); - set({ - '#': [] - }); // Set position to fragment's array children - - setObj('#'); - const { - childNodes - } = node; - [...childNodes].forEach(function (childNode) { - // No need for setChildren, as we have already built the container array - parseDOM(childNode, namespaces); - }); - resetTemp(); - break; - } - - default: - throw new TypeError('Not an XML type'); - } - } - - parseDOM(dom, {}); - - if (stringOutput) { - return JSON.stringify(ret[0]); - } - - return ret[0]; -}; - -jml.toJMLString = function (dom, config) { - return jml.toJML(dom, Object.assign(config || {}, { - stringOutput: true - })); -}; -/** - * - * @param {...JamilihArray} args - * @returns {JamilihReturn} - */ - - -jml.toDOM = function (...args) { - // Alias for jml() - return jml(...args); -}; -/** - * - * @param {...JamilihArray} args - * @returns {string} - */ - - -jml.toHTML = function (...args) { - // Todo: Replace this with version of jml() that directly builds a string - const ret = jml(...args); // Todo: deal with serialization of properties like 'selected', - // 'checked', 'value', 'defaultValue', 'for', 'dataset', 'on*', - // 'style'! (i.e., need to build a string ourselves) - - return ret.outerHTML; -}; -/** - * - * @param {...JamilihArray} args - * @returns {string} - */ - - -jml.toDOMString = function (...args) { - // Alias for jml.toHTML for parity with jml.toJMLString - return jml.toHTML(...args); -}; -/** - * - * @param {...JamilihArray} args - * @returns {string} - */ - - -jml.toXML = function (...args) { - const ret = jml(...args); - return new win.XMLSerializer().serializeToString(ret); -}; -/** - * - * @param {...JamilihArray} args - * @returns {string} - */ - - -jml.toXMLDOMString = function (...args) { - // Alias for jml.toXML for parity with jml.toJMLString - return jml.toXML(...args); -}; -/** - * Element-aware wrapper for `Map`. - */ - - -class JamilihMap extends Map { - /** - * @param {string|Element} elem - * @returns {any} - */ - get(elem) { - elem = typeof elem === 'string' ? $(elem) : elem; - return super.get.call(this, elem); - } - /** - * @param {string|Element} elem - * @param {any} value - * @returns {any} - */ - - - set(elem, value) { - elem = typeof elem === 'string' ? $(elem) : elem; - return super.set.call(this, elem, value); - } - /** - * @param {string|Element} elem - * @param {string} methodName - * @param {...any} args - * @returns {any} - */ - - - invoke(elem, methodName, ...args) { - elem = typeof elem === 'string' ? $(elem) : elem; - return this.get(elem)[methodName](elem, ...args); - } - -} -/** - * Element-aware wrapper for `WeakMap`. - */ - - -class JamilihWeakMap extends WeakMap { - /** - * @param {string|Element} elem - * @returns {any} - */ - get(elem) { - elem = typeof elem === 'string' ? $(elem) : elem; - return super.get.call(this, elem); - } - /** - * @param {string|Element} elem - * @param {any} value - * @returns {any} - */ - - - set(elem, value) { - elem = typeof elem === 'string' ? $(elem) : elem; - return super.set.call(this, elem, value); - } - /** - * @param {string|Element} elem - * @param {string} methodName - * @param {...any} args - * @returns {any} - */ - - - invoke(elem, methodName, ...args) { - elem = typeof elem === 'string' ? $(elem) : elem; - return this.get(elem)[methodName](elem, ...args); - } - -} - -jml.Map = JamilihMap; -jml.WeakMap = JamilihWeakMap; -/** -* @typedef {GenericArray} MapAndElementArray -* @property {JamilihWeakMap|JamilihMap} 0 -* @property {Element} 1 -*/ - -/** - * @param {GenericObject} obj - * @param {...JamilihArray} args - * @returns {MapAndElementArray} - */ - -jml.weak = function (obj, ...args) { - const map = new JamilihWeakMap(); - const elem = jml({ - $map: [map, obj] - }, ...args); - return [map, elem]; -}; -/** - * @param {any} obj - * @param {...JamilihArray} args - * @returns {MapAndElementArray} - */ - - -jml.strong = function (obj, ...args) { - const map = new JamilihMap(); - const elem = jml({ - $map: [map, obj] - }, ...args); - return [map, elem]; -}; -/** - * @param {string|Element} elem If a string, will be interpreted as a selector - * @param {symbol|string} sym If a string, will be used with `Symbol.for` - * @returns {any} The value associated with the symbol - */ - - -jml.symbol = jml.sym = jml.for = function (elem, sym) { - elem = typeof elem === 'string' ? $(elem) : elem; - return elem[typeof sym === 'symbol' ? sym : Symbol.for(sym)]; -}; -/** - * @param {string|Element} elem If a string, will be interpreted as a selector - * @param {symbol|string|Map|WeakMap} symOrMap If a string, will be used with `Symbol.for` - * @param {string|any} methodName Can be `any` if the symbol or map directly - * points to a function (it is then used as the first argument). - * @param {any[]} args - * @returns {any} - */ - - -jml.command = function (elem, symOrMap, methodName, ...args) { - elem = typeof elem === 'string' ? $(elem) : elem; - let func; - - if (['symbol', 'string'].includes(typeof symOrMap)) { - func = jml.sym(elem, symOrMap); - - if (typeof func === 'function') { - return func(methodName, ...args); // Already has `this` bound to `elem` - } - - return func[methodName](...args); - } - - func = symOrMap.get(elem); - - if (typeof func === 'function') { - return func.call(elem, methodName, ...args); - } - - return func[methodName](elem, ...args); // return func[methodName].call(elem, ...args); -}; -/** - * Expects properties `document`, `XMLSerializer`, and `DOMParser`. - * Also updates `body` with `document.body`. - * @param {Window} wind - * @returns {void} - */ - - -jml.setWindow = wind => { - win = wind; - doc = win.document; - - if (doc && doc.body) { - ({ - body - } = doc); - } -}; -/** - * @returns {Window} - */ - - -jml.getWindow = () => { - return win; -}; -/** - * Does not run Jamilih so can be further processed. - * @param {JamilihArray} jmlArray - * @param {string|JamilihArray|Element} glu - * @returns {Element} - */ - - -function glue(jmlArray, glu) { - return [...jmlArray].reduce((arr, item) => { - arr.push(item, glu); - return arr; - }, []).slice(0, -1); -} // istanbul ignore next - - -let body = doc && doc.body; // eslint-disable-line import/no-mutable-exports - -const nbsp = '\u00A0'; // Very commonly needed in templates - -export default jml; -export { $, $$, body, glue, jml, nbsp }; diff --git a/src/external/load-stylesheets/index-es.js b/src/external/load-stylesheets/index-es.js deleted file mode 100644 index 4129f903..00000000 --- a/src/external/load-stylesheets/index-es.js +++ /dev/null @@ -1,162 +0,0 @@ -function _slicedToArray(arr, i) { - return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); -} - -function _arrayWithHoles(arr) { - if (Array.isArray(arr)) return arr; -} - -function _iterableToArrayLimit(arr, i) { - if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { - return; - } - - var _arr = []; - var _n = true; - var _d = false; - var _e = undefined; - - try { - for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { - _arr.push(_s.value); - - if (i && _arr.length === i) break; - } - } catch (err) { - _d = true; - _e = err; - } finally { - try { - if (!_n && _i["return"] != null) _i["return"](); - } finally { - if (_d) throw _e; - } - } - - return _arr; -} - -function _nonIterableRest() { - throw new TypeError("Invalid attempt to destructure non-iterable instance"); -} - -function loadStylesheets(stylesheets) { - var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - beforeDefault = _ref.before, - afterDefault = _ref.after, - faviconDefault = _ref.favicon, - canvasDefault = _ref.canvas, - _ref$image = _ref.image, - imageDefault = _ref$image === void 0 ? true : _ref$image, - acceptErrors = _ref.acceptErrors; - - stylesheets = Array.isArray(stylesheets) ? stylesheets : [stylesheets]; - - function setupLink(stylesheetURL) { - var options = {}; - - if (Array.isArray(stylesheetURL)) { - var _stylesheetURL = stylesheetURL; - - var _stylesheetURL2 = _slicedToArray(_stylesheetURL, 2); - - stylesheetURL = _stylesheetURL2[0]; - var _stylesheetURL2$ = _stylesheetURL2[1]; - options = _stylesheetURL2$ === void 0 ? {} : _stylesheetURL2$; - } - - var _options = options, - _options$favicon = _options.favicon, - favicon = _options$favicon === void 0 ? faviconDefault : _options$favicon; - var _options2 = options, - _options2$before = _options2.before, - before = _options2$before === void 0 ? beforeDefault : _options2$before, - _options2$after = _options2.after, - after = _options2$after === void 0 ? afterDefault : _options2$after, - _options2$canvas = _options2.canvas, - canvas = _options2$canvas === void 0 ? canvasDefault : _options2$canvas, - _options2$image = _options2.image, - image = _options2$image === void 0 ? imageDefault : _options2$image; - - function addLink() { - if (before) { - before.before(link); - } else if (after) { - after.after(link); - } else { - // eslint-disable-next-line unicorn/prefer-node-append - document.head.appendChild(link); - } - } - - var link = document.createElement('link'); // eslint-disable-next-line promise/avoid-new - - return new Promise(function (resolve, reject) { - var rej = reject; - - if (acceptErrors) { - rej = typeof acceptErrors === 'function' ? function (error) { - acceptErrors({ - error: error, - stylesheetURL: stylesheetURL, - options: options, - resolve: resolve, - reject: reject - }); - } : resolve; - } - - if (stylesheetURL.endsWith('.css')) { - favicon = false; - } else if (stylesheetURL.endsWith('.ico')) { - favicon = true; - } - - if (favicon) { - link.rel = 'shortcut icon'; - link.type = 'image/x-icon'; - - if (image === false) { - link.href = stylesheetURL; - addLink(); - resolve(link); - return; - } - - var cnv = document.createElement('canvas'); - cnv.width = 16; - cnv.height = 16; - var context = cnv.getContext('2d'); - var img = document.createElement('img'); - img.addEventListener('error', function (error) { - reject(error); - }); - img.addEventListener('load', function () { - context.drawImage(img, 0, 0); - link.href = canvas ? cnv.toDataURL('image/x-icon') : stylesheetURL; - addLink(); - resolve(link); - }); - img.src = stylesheetURL; - return; - } - - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = stylesheetURL; - addLink(); - link.addEventListener('error', function (error) { - rej(error); - }); - link.addEventListener('load', function () { - resolve(link); - }); - }); - } - - return Promise.all(stylesheets.map(function (stylesheetURL) { - return setupLink(stylesheetURL); - })); -} - -export default loadStylesheets; diff --git a/src/external/stackblur-canvas/dist/stackblur-es.js b/src/external/stackblur-canvas/dist/stackblur-es.js deleted file mode 100644 index 3721f814..00000000 --- a/src/external/stackblur-canvas/dist/stackblur-es.js +++ /dev/null @@ -1,580 +0,0 @@ -function _typeof(obj) { - "@babel/helpers - typeof"; - - if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { - _typeof = function (obj) { - return typeof obj; - }; - } else { - _typeof = function (obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }; - } - - return _typeof(obj); -} - -function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -} - -/* eslint-disable no-bitwise, unicorn/prefer-query-selector */ - -/** -* StackBlur - a fast almost Gaussian Blur For Canvas -* -* In case you find this class useful - especially in commercial projects - -* I am not totally unhappy for a small donation to my PayPal account -* mario@quasimondo.de -* -* Or support me on flattr: -* {@link https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript}. -* -* @module StackBlur -* @author Mario Klingemann -* Contact: mario@quasimondo.com -* Website: {@link http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html} -* Twitter: @quasimondo -* -* @copyright (c) 2010 Mario Klingemann -* -* Permission is hereby granted, free of charge, to any person -* obtaining a copy of this software and associated documentation -* files (the "Software"), to deal in the Software without -* restriction, including without limitation the rights to use, -* copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the -* Software is furnished to do so, subject to the following -* conditions: -* -* The above copyright notice and this permission notice shall be -* included in all copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -* OTHER DEALINGS IN THE SOFTWARE. -*/ - -/* eslint-disable max-len */ -var mulTable = [512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259]; -var shgTable = [9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24]; -/* eslint-enable max-len */ - -/** - * @param {string|HTMLImageElement} img - * @param {string|HTMLCanvasElement} canvas - * @param {Float} radius - * @param {boolean} blurAlphaChannel - * @returns {undefined} - */ - -function processImage(img, canvas, radius, blurAlphaChannel) { - if (typeof img === 'string') { - img = document.getElementById(img); - } - - if (!img || !('naturalWidth' in img)) { - return; - } - - var w = img.naturalWidth; - var h = img.naturalHeight; - - if (typeof canvas === 'string') { - canvas = document.getElementById(canvas); - } - - if (!canvas || !('getContext' in canvas)) { - return; - } - - canvas.style.width = w + 'px'; - canvas.style.height = h + 'px'; - canvas.width = w; - canvas.height = h; - var context = canvas.getContext('2d'); - context.clearRect(0, 0, w, h); - context.drawImage(img, 0, 0); - - if (isNaN(radius) || radius < 1) { - return; - } - - if (blurAlphaChannel) { - processCanvasRGBA(canvas, 0, 0, w, h, radius); - } else { - processCanvasRGB(canvas, 0, 0, w, h, radius); - } -} -/** - * @param {string|HTMLCanvasElement} canvas - * @param {Integer} topX - * @param {Integer} topY - * @param {Integer} width - * @param {Integer} height - * @throws {Error|TypeError} - * @returns {ImageData} See {@link https://html.spec.whatwg.org/multipage/canvas.html#imagedata} - */ - - -function getImageDataFromCanvas(canvas, topX, topY, width, height) { - if (typeof canvas === 'string') { - canvas = document.getElementById(canvas); - } - - if (!canvas || _typeof(canvas) !== 'object' || !('getContext' in canvas)) { - throw new TypeError('Expecting canvas with `getContext` method ' + 'in processCanvasRGB(A) calls!'); - } - - var context = canvas.getContext('2d'); - - try { - return context.getImageData(topX, topY, width, height); - } catch (e) { - throw new Error('unable to access image data: ' + e); - } -} -/** - * @param {HTMLCanvasElement} canvas - * @param {Integer} topX - * @param {Integer} topY - * @param {Integer} width - * @param {Integer} height - * @param {Float} radius - * @returns {undefined} - */ - - -function processCanvasRGBA(canvas, topX, topY, width, height, radius) { - if (isNaN(radius) || radius < 1) { - return; - } - - radius |= 0; - var imageData = getImageDataFromCanvas(canvas, topX, topY, width, height); - imageData = processImageDataRGBA(imageData, topX, topY, width, height, radius); - canvas.getContext('2d').putImageData(imageData, topX, topY); -} -/** - * @param {ImageData} imageData - * @param {Integer} topX - * @param {Integer} topY - * @param {Integer} width - * @param {Integer} height - * @param {Float} radius - * @returns {ImageData} - */ - - -function processImageDataRGBA(imageData, topX, topY, width, height, radius) { - var pixels = imageData.data; - var x, y, i, p, yp, yi, yw, rSum, gSum, bSum, aSum, rOutSum, gOutSum, bOutSum, aOutSum, rInSum, gInSum, bInSum, aInSum, pr, pg, pb, pa, rbs; - var div = 2 * radius + 1; // const w4 = width << 2; - - var widthMinus1 = width - 1; - var heightMinus1 = height - 1; - var radiusPlus1 = radius + 1; - var sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; - var stackStart = new BlurStack(); - var stack = stackStart; - var stackEnd; - - for (i = 1; i < div; i++) { - stack = stack.next = new BlurStack(); - - if (i === radiusPlus1) { - stackEnd = stack; - } - } - - stack.next = stackStart; - var stackIn = null; - var stackOut = null; - yw = yi = 0; - var mulSum = mulTable[radius]; - var shgSum = shgTable[radius]; - - for (y = 0; y < height; y++) { - rInSum = gInSum = bInSum = aInSum = rSum = gSum = bSum = aSum = 0; - rOutSum = radiusPlus1 * (pr = pixels[yi]); - gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); - bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); - aOutSum = radiusPlus1 * (pa = pixels[yi + 3]); - rSum += sumFactor * pr; - gSum += sumFactor * pg; - bSum += sumFactor * pb; - aSum += sumFactor * pa; - stack = stackStart; - - for (i = 0; i < radiusPlus1; i++) { - stack.r = pr; - stack.g = pg; - stack.b = pb; - stack.a = pa; - stack = stack.next; - } - - for (i = 1; i < radiusPlus1; i++) { - p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); - rSum += (stack.r = pr = pixels[p]) * (rbs = radiusPlus1 - i); - gSum += (stack.g = pg = pixels[p + 1]) * rbs; - bSum += (stack.b = pb = pixels[p + 2]) * rbs; - aSum += (stack.a = pa = pixels[p + 3]) * rbs; - rInSum += pr; - gInSum += pg; - bInSum += pb; - aInSum += pa; - stack = stack.next; - } - - stackIn = stackStart; - stackOut = stackEnd; - - for (x = 0; x < width; x++) { - pixels[yi + 3] = pa = aSum * mulSum >> shgSum; - - if (pa !== 0) { - pa = 255 / pa; - pixels[yi] = (rSum * mulSum >> shgSum) * pa; - pixels[yi + 1] = (gSum * mulSum >> shgSum) * pa; - pixels[yi + 2] = (bSum * mulSum >> shgSum) * pa; - } else { - pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0; - } - - rSum -= rOutSum; - gSum -= gOutSum; - bSum -= bOutSum; - aSum -= aOutSum; - rOutSum -= stackIn.r; - gOutSum -= stackIn.g; - bOutSum -= stackIn.b; - aOutSum -= stackIn.a; - p = yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1) << 2; - rInSum += stackIn.r = pixels[p]; - gInSum += stackIn.g = pixels[p + 1]; - bInSum += stackIn.b = pixels[p + 2]; - aInSum += stackIn.a = pixels[p + 3]; - rSum += rInSum; - gSum += gInSum; - bSum += bInSum; - aSum += aInSum; - stackIn = stackIn.next; - rOutSum += pr = stackOut.r; - gOutSum += pg = stackOut.g; - bOutSum += pb = stackOut.b; - aOutSum += pa = stackOut.a; - rInSum -= pr; - gInSum -= pg; - bInSum -= pb; - aInSum -= pa; - stackOut = stackOut.next; - yi += 4; - } - - yw += width; - } - - for (x = 0; x < width; x++) { - gInSum = bInSum = aInSum = rInSum = gSum = bSum = aSum = rSum = 0; - yi = x << 2; - rOutSum = radiusPlus1 * (pr = pixels[yi]); - gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); - bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); - aOutSum = radiusPlus1 * (pa = pixels[yi + 3]); - rSum += sumFactor * pr; - gSum += sumFactor * pg; - bSum += sumFactor * pb; - aSum += sumFactor * pa; - stack = stackStart; - - for (i = 0; i < radiusPlus1; i++) { - stack.r = pr; - stack.g = pg; - stack.b = pb; - stack.a = pa; - stack = stack.next; - } - - yp = width; - - for (i = 1; i <= radius; i++) { - yi = yp + x << 2; - rSum += (stack.r = pr = pixels[yi]) * (rbs = radiusPlus1 - i); - gSum += (stack.g = pg = pixels[yi + 1]) * rbs; - bSum += (stack.b = pb = pixels[yi + 2]) * rbs; - aSum += (stack.a = pa = pixels[yi + 3]) * rbs; - rInSum += pr; - gInSum += pg; - bInSum += pb; - aInSum += pa; - stack = stack.next; - - if (i < heightMinus1) { - yp += width; - } - } - - yi = x; - stackIn = stackStart; - stackOut = stackEnd; - - for (y = 0; y < height; y++) { - p = yi << 2; - pixels[p + 3] = pa = aSum * mulSum >> shgSum; - - if (pa > 0) { - pa = 255 / pa; - pixels[p] = (rSum * mulSum >> shgSum) * pa; - pixels[p + 1] = (gSum * mulSum >> shgSum) * pa; - pixels[p + 2] = (bSum * mulSum >> shgSum) * pa; - } else { - pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; - } - - rSum -= rOutSum; - gSum -= gOutSum; - bSum -= bOutSum; - aSum -= aOutSum; - rOutSum -= stackIn.r; - gOutSum -= stackIn.g; - bOutSum -= stackIn.b; - aOutSum -= stackIn.a; - p = x + ((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width << 2; - rSum += rInSum += stackIn.r = pixels[p]; - gSum += gInSum += stackIn.g = pixels[p + 1]; - bSum += bInSum += stackIn.b = pixels[p + 2]; - aSum += aInSum += stackIn.a = pixels[p + 3]; - stackIn = stackIn.next; - rOutSum += pr = stackOut.r; - gOutSum += pg = stackOut.g; - bOutSum += pb = stackOut.b; - aOutSum += pa = stackOut.a; - rInSum -= pr; - gInSum -= pg; - bInSum -= pb; - aInSum -= pa; - stackOut = stackOut.next; - yi += width; - } - } - - return imageData; -} -/** - * @param {HTMLCanvasElement} canvas - * @param {Integer} topX - * @param {Integer} topY - * @param {Integer} width - * @param {Integer} height - * @param {Float} radius - * @returns {undefined} - */ - - -function processCanvasRGB(canvas, topX, topY, width, height, radius) { - if (isNaN(radius) || radius < 1) { - return; - } - - radius |= 0; - var imageData = getImageDataFromCanvas(canvas, topX, topY, width, height); - imageData = processImageDataRGB(imageData, topX, topY, width, height, radius); - canvas.getContext('2d').putImageData(imageData, topX, topY); -} -/** - * @param {ImageData} imageData - * @param {Integer} topX - * @param {Integer} topY - * @param {Integer} width - * @param {Integer} height - * @param {Float} radius - * @returns {ImageData} - */ - - -function processImageDataRGB(imageData, topX, topY, width, height, radius) { - var pixels = imageData.data; - var x, y, i, p, yp, yi, yw, rSum, gSum, bSum, rOutSum, gOutSum, bOutSum, rInSum, gInSum, bInSum, pr, pg, pb, rbs; - var div = 2 * radius + 1; // const w4 = width << 2; - - var widthMinus1 = width - 1; - var heightMinus1 = height - 1; - var radiusPlus1 = radius + 1; - var sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2; - var stackStart = new BlurStack(); - var stack = stackStart; - var stackEnd; - - for (i = 1; i < div; i++) { - stack = stack.next = new BlurStack(); - - if (i === radiusPlus1) { - stackEnd = stack; - } - } - - stack.next = stackStart; - var stackIn = null; - var stackOut = null; - yw = yi = 0; - var mulSum = mulTable[radius]; - var shgSum = shgTable[radius]; - - for (y = 0; y < height; y++) { - rInSum = gInSum = bInSum = rSum = gSum = bSum = 0; - rOutSum = radiusPlus1 * (pr = pixels[yi]); - gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); - bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); - rSum += sumFactor * pr; - gSum += sumFactor * pg; - bSum += sumFactor * pb; - stack = stackStart; - - for (i = 0; i < radiusPlus1; i++) { - stack.r = pr; - stack.g = pg; - stack.b = pb; - stack = stack.next; - } - - for (i = 1; i < radiusPlus1; i++) { - p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); - rSum += (stack.r = pr = pixels[p]) * (rbs = radiusPlus1 - i); - gSum += (stack.g = pg = pixels[p + 1]) * rbs; - bSum += (stack.b = pb = pixels[p + 2]) * rbs; - rInSum += pr; - gInSum += pg; - bInSum += pb; - stack = stack.next; - } - - stackIn = stackStart; - stackOut = stackEnd; - - for (x = 0; x < width; x++) { - pixels[yi] = rSum * mulSum >> shgSum; - pixels[yi + 1] = gSum * mulSum >> shgSum; - pixels[yi + 2] = bSum * mulSum >> shgSum; - rSum -= rOutSum; - gSum -= gOutSum; - bSum -= bOutSum; - rOutSum -= stackIn.r; - gOutSum -= stackIn.g; - bOutSum -= stackIn.b; - p = yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1) << 2; - rInSum += stackIn.r = pixels[p]; - gInSum += stackIn.g = pixels[p + 1]; - bInSum += stackIn.b = pixels[p + 2]; - rSum += rInSum; - gSum += gInSum; - bSum += bInSum; - stackIn = stackIn.next; - rOutSum += pr = stackOut.r; - gOutSum += pg = stackOut.g; - bOutSum += pb = stackOut.b; - rInSum -= pr; - gInSum -= pg; - bInSum -= pb; - stackOut = stackOut.next; - yi += 4; - } - - yw += width; - } - - for (x = 0; x < width; x++) { - gInSum = bInSum = rInSum = gSum = bSum = rSum = 0; - yi = x << 2; - rOutSum = radiusPlus1 * (pr = pixels[yi]); - gOutSum = radiusPlus1 * (pg = pixels[yi + 1]); - bOutSum = radiusPlus1 * (pb = pixels[yi + 2]); - rSum += sumFactor * pr; - gSum += sumFactor * pg; - bSum += sumFactor * pb; - stack = stackStart; - - for (i = 0; i < radiusPlus1; i++) { - stack.r = pr; - stack.g = pg; - stack.b = pb; - stack = stack.next; - } - - yp = width; - - for (i = 1; i <= radius; i++) { - yi = yp + x << 2; - rSum += (stack.r = pr = pixels[yi]) * (rbs = radiusPlus1 - i); - gSum += (stack.g = pg = pixels[yi + 1]) * rbs; - bSum += (stack.b = pb = pixels[yi + 2]) * rbs; - rInSum += pr; - gInSum += pg; - bInSum += pb; - stack = stack.next; - - if (i < heightMinus1) { - yp += width; - } - } - - yi = x; - stackIn = stackStart; - stackOut = stackEnd; - - for (y = 0; y < height; y++) { - p = yi << 2; - pixels[p] = rSum * mulSum >> shgSum; - pixels[p + 1] = gSum * mulSum >> shgSum; - pixels[p + 2] = bSum * mulSum >> shgSum; - rSum -= rOutSum; - gSum -= gOutSum; - bSum -= bOutSum; - rOutSum -= stackIn.r; - gOutSum -= stackIn.g; - bOutSum -= stackIn.b; - p = x + ((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width << 2; - rSum += rInSum += stackIn.r = pixels[p]; - gSum += gInSum += stackIn.g = pixels[p + 1]; - bSum += bInSum += stackIn.b = pixels[p + 2]; - stackIn = stackIn.next; - rOutSum += pr = stackOut.r; - gOutSum += pg = stackOut.g; - bOutSum += pb = stackOut.b; - rInSum -= pr; - gInSum -= pg; - bInSum -= pb; - stackOut = stackOut.next; - yi += width; - } - } - - return imageData; -} -/** - * - */ - - -var BlurStack = -/** - * Set properties. - */ -function BlurStack() { - _classCallCheck(this, BlurStack); - - this.r = 0; - this.g = 0; - this.b = 0; - this.a = 0; - this.next = null; -}; - -export { BlurStack, processCanvasRGB as canvasRGB, processCanvasRGBA as canvasRGBA, processImage as image, processImageDataRGB as imageDataRGB, processImageDataRGBA as imageDataRGBA }; diff --git a/src/svgcanvas/svgcanvas.js b/src/svgcanvas/svgcanvas.js index 9501aadd..39ad35f2 100644 --- a/src/svgcanvas/svgcanvas.js +++ b/src/svgcanvas/svgcanvas.js @@ -17,7 +17,7 @@ import {jsPDF} from 'jspdf/dist/jspdf.es.min.js'; import 'svg2pdf.js/dist/svg2pdf.es.js'; -import {canvg} from '../external/canvg/canvg.js'; +import {Canvg as canvg} from 'canvg'; import '../common/svgpathseg.js'; import jQueryPluginSVG from '../common/jQuery.attr.js'; // Needed for SVG attribute setting and array form with `attr`