Skip to content

Commit

Permalink
Allow functions etc. inside array tiles (fix #33)
Browse files Browse the repository at this point in the history
* 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`)
  • Loading branch information
edemaine committed Sep 27, 2022
1 parent 75ac7a6 commit 4831efa
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 69 deletions.
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down Expand Up @@ -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 `<symbol viewBox="0 0 0 0"/>`.
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(<symbol/>)`. 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(<symbol/>)`. 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 `<marker>`, `<filter>`, gradient, or other element
intended for `<defs>`, call `svgtiler.def(tag)`, where `tag`
Expand Down
30 changes: 16 additions & 14 deletions examples/chess/map.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ size = 45 # width and height of svg files
# checkerboard backgrounds
light = null
dark = <path stroke="#000" d="M 7.5,0 L 0,7.5 M 15,0 L 0,15 M 22.5,0 L 0,22.5 M 30,0 L 0,30 M 37.5,0 L 0,37.5 M 45,0 L 0,45 M 45,7.5 L 7.5,45 M 45,15 L 15,45 M 45,22.5 L 22.5,45 M 45,30 L 30,45 M 45,37.5 L 37.5,45"/>
background = ->
<symbol viewBox="0 0 #{size} #{size}" z-index="-1">
{if (@i + @j) % 2 == 0
light
else
dark
}
</symbol>

read = (filename) ->
dom = require filename
Expand All @@ -14,21 +22,15 @@ svgtiler.afterRender (render) ->
<rect fill="white" z-index="-2"
x={render.xMin} y={render.yMin} width={render.width} height={render.height}/>

(key) ->
# Map blanks to empty string
key = key.trim()
key = '' if key == '.'
piece = key.toLowerCase()
[
<symbol viewBox="0 0 #{size} #{size}" z-index="-1">
{if (@i + @j) % 2 == 0
light
else
dark
}
</symbol>
[
background
(key) ->
# Map blanks to empty string
key = key.trim()
key = '' if key == '.'
piece = key.toLowerCase()
if key
<symbol viewBox="0 0 #{size} #{size}">
{read "./Chess_#{piece}#{if piece == key then "d" else "l"}t45.svg"}
</symbol>
]
]
108 changes: 67 additions & 41 deletions src/svgtiler.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) ->
Expand Down

0 comments on commit 4831efa

Please sign in to comment.