From 4831efaea17cd4a5adde1fa859d174fa5081ed41 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Tue, 27 Sep 2022 10:50:55 -0400 Subject: [PATCH] Allow functions etc. inside array tiles (fix #33) * Allow functions and maps inside arrays, via recursion * Proper detection of static (every item in array must be static, or wrap the array in `svgtiler.static`) --- README.md | 30 ++++++----- examples/chess/map.coffee | 30 ++++++----- src/svgtiler.coffee | 108 +++++++++++++++++++++++--------------- 3 files changed, 99 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 43fe597..b328dac 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ The code specifies a `mapping` in one of three ways: 1. `export default mapping` (ECMAScript modules style) 2. `exports.default = mapping` (CommonJS modules style) 3. Writing a `mapping` expression at the end of the file (implicit export), - which must be a top-level object or function expression + which must be a top-level object, array, or function expression (without e.g. being assigned to a variable). In any case, `mapping` should be one of the following types of **mapping** @@ -211,23 +211,25 @@ or return value of a function) should be specified as one of the following: 6. An empty string, short for the empty symbol ``. 7. `undefined` or `null`, indicating that this mapping doesn't define a tile for this tile name (and the next mapping should be checked). -8. An `Array` of tiles of any of the above formats (or more `Array`s, which - get flattened), meaning to stack multiple tiles on top of each other, - where the first non-null tile defines the `viewBox` and size - (but all can influence the [`overflowBox`](#overflow-and-bounding-box)). - Use [`z-index`](#z-index-stacking-order-of-tiles) - to control stacking order. - Null items in the array get ignored, and an empty array acts like `null` - (this mapping does not define a tile for this tile name). -9. Another mapping (JavaScript object, `Map` object, or function) that gets +8. Another mapping (JavaScript object, `Map` object, or function) that gets recursively evaluated as described above (with the same tile name and context). For example, a top-level JavaScript object could map some tile names to functions (when they need to be dynamic); or a top-level function could return different mappings depending on context. -10. One of the tiles above wrapped in a call to `svgtiler.static`, e.g., - `svgtiler.static()`. This wrapper tells SVG Tiler that the tile - mapping is always the same for this tile name, and does not depend on - `Context` (e.g. adjacent tiles), enabling SVG Tiler to do more caching. +9. A tile in one of the listed formats wrapped in a call to `svgtiler.static`, + e.g., `svgtiler.static()`. This wrapper tells SVG Tiler that the + tile mapping is always the same for this tile name, and does not depend on + `Context` (e.g. adjacent tiles), enabling SVG Tiler to do more caching. + This is only necessary if you use functions to define tiles; otherwise, + SVG Tiler will automatically mark the tiles as static. +10. An `Array` of tiles of any of the listed formats (including more `Array`s, + which get flattened), meaning to stack multiple tiles on top of each other, + where the first non-null tile defines the `viewBox` and size + (but all can influence the [`overflowBox`](#overflow-and-bounding-box)). + Use [`z-index`](#z-index-stacking-order-of-tiles) + to control stacking order. + Null items in the array get ignored, and an empty array acts like `null` + (this mapping does not define a tile for this tile name). If you need to use a ``, ``, gradient, or other element intended for ``, call `svgtiler.def(tag)`, where `tag` diff --git a/examples/chess/map.coffee b/examples/chess/map.coffee index b106c29..3dbb767 100644 --- a/examples/chess/map.coffee +++ b/examples/chess/map.coffee @@ -3,6 +3,14 @@ size = 45 # width and height of svg files # checkerboard backgrounds light = null dark = +background = -> + + {if (@i + @j) % 2 == 0 + light + else + dark + } + read = (filename) -> dom = require filename @@ -14,21 +22,15 @@ svgtiler.afterRender (render) -> -(key) -> - # Map blanks to empty string - key = key.trim() - key = '' if key == '.' - piece = key.toLowerCase() - [ - - {if (@i + @j) % 2 == 0 - light - else - dark - } - +[ + background + (key) -> + # Map blanks to empty string + key = key.trim() + key = '' if key == '.' + piece = key.toLowerCase() if key {read "./Chess_#{piece}#{if piece == key then "d" else "l"}t45.svg"} - ] +] diff --git a/src/svgtiler.coffee b/src/svgtiler.coffee index 201c8d4..3fc8247 100755 --- a/src/svgtiler.coffee +++ b/src/svgtiler.coffee @@ -28,8 +28,8 @@ unless window? Babel plugin to add implicit `export default` to last line of program, to simulate the effect of `eval` but in a module context. Only added if there isn't already an `export default` or `exports.default` - in the code, and when the last line is an object or function expression - (with the idea that it wouldn't do much by itself). + in the code, and when the last line is an object, array, or function + expression (with the idea that it wouldn't do much by itself). ### implicitFinalExportDefault = ({types}) -> visitor: @@ -62,7 +62,8 @@ unless window? if types.isExpressionStatement(last) and ( types.isObjectExpression(lastNode.expression) or types.isFunctionExpression(lastNode.expression) or - types.isArrowFunctionExpression(lastNode.expression) + types.isArrowFunctionExpression(lastNode.expression) or + types.isArrayExpression(lastNode.expression) # not AssignmentExpression or CallExpression ) exportLast = types.exportDefaultDeclaration lastNode.expression @@ -984,46 +985,71 @@ class Mapping extends Input return found ## Repeatedly expand `@map` until we get string, Preact VDOM, or - ## null/undefined. - value = @map - isStatic = undefined - while value? and typeof value != 'string' and not Array.isArray(value) and - not isPreact value - if value instanceof Map or value instanceof WeakMap - value = value.get key - else if typeof value == 'function' - value = value.call context, key, context - ## Use of a function implies dynamic, unless there's a static wrapper. - isStatic ?= false - else if $static of value # static wrapper from wrapStatic - value = value[$static] - ## Static wrapper forces static, even if there are functions. - isStatic = true - else # typeof value == 'object' - if value.hasOwnProperty key # avoid inherited property e.g. toString - value = value[key] + ## null/undefined. Arrays get expanded recursively. + recurse = (value, isStatic = undefined) => + while value? + #console.log key, ( + # switch + # when Array.isArray value then 'array' + # when isPreact value then 'preact' + # else typeof value + #), isStatic + if typeof value == 'string' or isPreact value + ## Static unless we saw a function and no static wrapper. + isStatic ?= true + ## Symbol ends up getting `isStatic` set to global `isStatic` value, + ## instead of this local value. For example, not helpful to mark + ## this symbol as static if another one in an array isn't static. + value = new SVGSymbol "tile '#{key}'", value, @settings + #value.isStatic = isStatic + return {value, isStatic} + else if value instanceof Map or value instanceof WeakMap + value = value.get key + else if typeof value == 'function' + value = value.call context, key, context + ## Use of a function implies dynamic, unless there's a static wrapper. + isStatic ?= false + else if $static of value # static wrapper from wrapStatic + value = value[$static] + ## Static wrapper forces static, even if there are functions. + isStatic = true + else if Array.isArray value + ## Items in an array inherit parent staticness if any, + ## with no influence between items. + ## Overall array is static if every item is. + allStatic = true + value = + for item in value + result = recurse item, isStatic + allStatic = false if result.isStatic == false + result.value + return {value, isStatic: allStatic} + else if typeof value == 'object' + if value.hasOwnProperty key # avoid inherited property e.g. toString + value = value[key] + else + value = undefined else + console.warn "Unsupported data type #{typeof value} in looking up tile '#{key}'" value = undefined - isStatic ?= true # static unless we saw a function and no static wrapper - - ## Build symbol if non-null, and save in cache if it's static. - if value? - makeSymbol = (data) => - symbol = new SVGSymbol "tile '#{key}'", data, @settings - symbol.isStatic = isStatic - symbol - if Array.isArray value - ## Enforce `value` to be a (flat) array with no nulls - symbols = - for part in value.flat Infinity when part? - makeSymbol part - else - symbols = makeSymbol value - @cache.set key, symbols if isStatic - symbols - else - @cache.set key, value if isStatic - value + ## Static unless we saw a function and no static wrapper + isStatic ?= true + {value, isStatic} + {value, isStatic} = recurse @map + + ## Set each symbol's `isStatic` flag to the global `isStatic` value. + ## Enforce arrays to be flat with no nulls. + if Array.isArray value + value = + for symbol in value.flat Infinity when symbol? + symbol.isStatic = isStatic + symbol + else if value? + value.isStatic = isStatic + + ## Save in cache if overall static. + @cache.set key, value if isStatic + value beforeRender: (fn) -> @beforeRenderQueue.push fn afterRender: (fn) ->