Skip to content

Commit

Permalink
Redo Maketiles to use export make (#100)
Browse files Browse the repository at this point in the history
* `Maketile.{coffee,js}` must `export make` or `export default` rules,
  though same implicit export rules apply.
* Rules are processed recursively and support plain objects, `Map`,
  `WeakMap`, arrays, and functions.  A function's return value is mostly
  ignored (just tested against `null` to decide whether it defined rule),
  and should call `svgtiler()` to execute build steps.
* Now require Maketile rules to not have `.`s, to avoid accidental
  infinite recursion with e.g. `(lang) -> svgtiler("mapping.#{lang}")`.
  • Loading branch information
edemaine committed Oct 28, 2022
1 parent 9279004 commit 97a8a0b
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 98 deletions.
69 changes: 50 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ objects:
2. A [**`Map` object**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
mapping tile names to tiles.
3. A **function** taking two arguments — a tile name (string)
and a `Context` object — and returning a tile.
and a `Context` object (also passed as `this`) — and returning a tile.
This feature allows you to parse tile names how you want, and to
vary the tile depending on the context (e.g., neighboring tile names
or parity of the tile location).
Expand Down Expand Up @@ -762,22 +762,27 @@ via one of the following options (any one will do):
SVG Tiler has a simple `Makefile`-like build system for keeping track of the
`svgtiler` command-line arguments needed to build your tiled figures.
These `Maketile`s generally get run whenever you run `svgtiler` without
any filename arguments (e.g. just running `svgtiler` without any arguments).
any filename arguments (including directories or glob patterns), for example,
when just running `svgtiler` without any arguments,
or when providing just flags like `svgtiler -f`.
(If you ever want to skip the `Maketile` behavior, just provide mapping
and drawing filename arguments like you normally would.)
Several examples of `Maketile`s are in the [examples](examples) directory.

### Maketile.args

At the simplest level, you can put the command-line arguments to `svgtiler`
into a file called `Maketile.args` (or `maketile.args`), and running
`svgtiler` without any filename arguments will automatically append those
arguments to the command line. Thus the `.args` file could specify the
into a file called `Maketile.args` (or `maketile.args`), and then running
`svgtiler` without any filename arguments will automatically
append those arguments to the command line.
Thus the `.args` file could specify the
mappings and drawings to render, and you can still add extra options like
`--pdf` or `--force` to the actual command line as needed.
The `.args` file gets parsed similar to the `bash` shell,
so you can write one-line comments with `#`,
you can use glob expressions like `**/*.asc`,
you can use
[glob expressions](https://github.com/isaacs/node-glob#glob-primer)
like `**/*.asc`,
and you put quotes around filenames with spaces or other special characters.
You can also write the arguments over multiple lines
(with no need to end lines with `\`).
Expand All @@ -787,16 +792,40 @@ You can also write the arguments over multiple lines
The more sophisticated system is to write a `Maketile.coffee` or
`Maketile.js` file. This system offers the entire CoffeeScript or
JavaScript programming language to express complex build rules.
In particular, you can write multiple different build rules by
`export`ing different named functions. The `default` export is the rule
that gets run when `svgtiler` has no filename arguments; you can run
another exported rule `foo` by running `svgtiler foo` from the command line.
However, directory names, filenames with extensions, and glob patterns
take priority over Maketile rule names, so avoid using rule names containing
`.`, `*`, `?`, `{`, `[`, `!(`, `+(`, `@(`, or whole directory names
to prevent such conflicts.

Build rules can run the equivalent of an `svgtiler` command line by calling
The file can provide build rules in one of a few ways:

1. `export make = ...` (ESM) or `exports.make = ...` (CommonJS)
2. `export default ...` (ESM) or `exports.default = ...` (CommonJS)
3. Writing a top-level object, array, or function expression
(without e.g. being assigned to a variable),
which triggers an implicit `export default`.

The exported rules can be one of the following types:

1. A **function** taking two arguments — a rule name (string)
and a `Mapping` object representing the Maketile (also passed as `this`).
The function should directly run build steps by calling `svgtiler()`;
see below. The function's return value is mostly ignored, except that
a return value of `null` is interpreted as "no rule with that name".
If the function takes no arguments, it is treated as just defining
the default build rule `""` (empty string).
2. A plain **JavaScript object** whose properties rule names to rules
(e.g., `{foo: rule1, bar: rule2, '': defaultRule}`).
3. A [**`Map` object**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
mapping rule names to rules.
4. An `Array` of any of the above types, meaning to run the rules in sequence.

If you run `svgtiler` with no filename arguments,
the default rule name of `""` (empty string) gets run.
If you run `svgtiler` with one or more arguments that are valid rule names
(containing no `.`s or glob magic patterns like `*` or `?`),
then instead these rules get run in sequence.
(Note that directory names, filenames with extensions, and glob patterns
take priority over Maketile rule names. So avoid naming rule names that match
directory names; other conflicts are prevented by forbidding `.`/`*`/`?`/etc.
in rule names.)

Rule functions can run the equivalent of an `svgtiler` command line by calling
the `svgtiler` function, e.g., `svgtiler('mapping.coffee *.asc')`.
String arguments are parsed just like `.args` files: whitespace separates
arguments, `#` indicates comments, glob patterns get expanded, and quotes
Expand All @@ -808,7 +837,8 @@ would not).
Instead of strings, you can also directly pass in `Mapping` or `Drawing` or
`Style` objects (as arguments or as part of an array argument).
An easy way to create such objects is to call `svgtiler.require()`,
which processes any given filename (without any processing of its filename).
which loads any given filename as if it was given on the command line
(without any processing of its filename).
For example, `svgtiler.require('filename with spaces and *s.asc')` transforms
an ASCII file into a `Drawing` object.

Expand All @@ -823,11 +853,12 @@ Thus you can write your own `for` loops and e.g.
`switch` depending on what additional pattern(s) the filenames match.
For example, `svgtiler('mapping.coffee *.asc')` can be rewritten as
`svgtiler.glob('*.asc').forEach((asc) =>
svgtiler(['mapping.coffee', asc])` or
`svgtiler.glob('*.asc').forEach((asc) =>
svgtiler('mapping.coffee', svgtiler.require(asc))`.

No calls to `svgtiler()` or other side effects should be at the top level
of the `Maketile`. Instead, put these build steps inside an `export default`
function or a named `export`ed function.
of the `Maketile`. Instead, put these build steps inside a rule function.

### Directories

Expand Down
4 changes: 2 additions & 2 deletions examples/Maketile.coffee
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default ->
->
for example in svgtiler.glob '*/' # match directories only
svgtiler [example]
svgtiler [example] # array argument isn't processed for globs etc.
16 changes: 8 additions & 8 deletions examples/anim/Maketile.coffee
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
for lang in ['css', 'styl']
exports[lang] = do (lang) -> ->
svgtiler """
-f css-anim.#{lang}
( shapes.coffee css-anim.csv )
( ascii.coffee ascii.asc )
"""
# Supported rules: css (default), styl

export default exports.css
(lang) ->
lang = 'css' unless lang
svgtiler """
-f css-anim.#{lang}
( shapes.coffee css-anim.csv )
( ascii.coffee ascii.asc )
"""
13 changes: 5 additions & 8 deletions examples/chess/Maketile.coffee
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
export coffee = ->
coffee: ->
svgtiler '-f map.coffee *.asc'
export js = ->
js: ->
svgtiler '-f map.jsx *.asc'
export graph = ->
graph: ->
svgtiler '-f -O graph-* map.coffee graph.coffee *.asc'

export default ->
coffee()
js()
graph()
'': ->
svgtiler 'coffee js graph'
32 changes: 15 additions & 17 deletions examples/mario/Maketile.coffee
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
palettes = ['castle', 'overworld', 'underground', 'underwater']

## Define individual palette rules and a default rule that builds them all.
for palette in palettes
exports[palette] = do (palette) -> ->
export make = (palette) ->
if palette
svgtiler "-f -s palette=#{palette} -O *_#{palette} mario.coffee door.tsv"

export default ->
for palette in palettes
exports[palette]()
else
svgtiler palettes

## Example definition of just the "everything" rule.
export simple = ->
for palette in palettes
svgtiler "-f -s palette=#{palette} -O *_#{palette} mario.coffee door.tsv"
#export make = ->
# for palette in palettes
# svgtiler "-f -s palette=#{palette} -O *_#{palette} mario.coffee door.tsv"

## Example definition using just a single call to svgtiler()
export singleCall = ->
svgtiler '''
-f
( -s palette=castle -O *_castle mario.coffee door.tsv )
( -s palette=overworld -O *_overworld mario.coffee door.tsv )
( -s palette=underground -O *_underground mario.coffee door.tsv )
( -s palette=underwater -O *_underwater mario.coffee door.tsv )
'''
#export make = ->
# svgtiler '''
# -f
# ( -s palette=castle -O *_castle mario.coffee door.tsv )
# ( -s palette=overworld -O *_overworld mario.coffee door.tsv )
# ( -s palette=underground -O *_underground mario.coffee door.tsv )
# ( -s palette=underwater -O *_underwater mario.coffee door.tsv )
# '''
13 changes: 7 additions & 6 deletions examples/polyomino/Maketile.coffee
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Supported rules: cjsx, jsx, coffee
# Default `svgtiler` behavior runs all rules.

langs = ['cjsx', 'jsx', 'coffee']

for lang in langs
exports[lang] = do (lang) -> ->
(lang) ->
if lang
svgtiler "-f outlines.#{lang} *.asc"

export default ->
for lang in langs
exports[lang]()
else
svgtiler langs
11 changes: 4 additions & 7 deletions examples/tetris/Maketile.coffee
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
export txt = ->
txt: ->
svgtiler '-f -P --bg black NES_level7.txt example.asc'

export coffee = ->
coffee: ->
svgtiler '-f -P NES_level7.coffee example.asc'

export default ->
txt()
coffee()
'': ->
svgtiler 'txt coffee'
13 changes: 7 additions & 6 deletions examples/tilt/Maketile.coffee
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Supported rules: coffee, txt
# Default `svgtiler` behavior runs all rules.

langs = ['coffee', 'txt']

for lang in langs
exports[lang] = do (lang) -> ->
export make = (lang) ->
if lang
svgtiler "-f --tw 50 --th 50 tilt.#{lang} *.asc *.csv"

export default ->
for lang in langs
exports[lang]()
else
svgtiler langs
95 changes: 70 additions & 25 deletions src/svgtiler.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -1192,14 +1192,26 @@ parseIntoArgs = (text) ->

class Args extends Input
###
Args list stores in `@args` an array of strings such as "-p" and
"filename.asc". It's represented in a file as an ASCII file
that's parsed like a shell, allowing quoted strings, backslash escapes,
comments via `#`, and newlines treated like regular whitespace.
Base args list stores in `@args` an array of strings
such as "-p" and "filename.asc".
###
parse: (@args) ->
makeRule: (key) ->
## Only default rule '' defined by Args.
return if key
## Run child driver with arguments.
svgtiler @args
return true

class ParsedArgs extends Args
###
`.args` files are represented as an ASCII file that's parsed like a shell,
allowing quoted strings, backslash escapes, comments via `#`, and
newlines treated like regular whitespace.
###
@title: "Additional command-line arguments parsed like a shell"
parse: (text) ->
@args = parseIntoArgs text
super parseIntoArgs text

class Mapping extends Input
###
Expand Down Expand Up @@ -1306,6 +1318,41 @@ class Mapping extends Input
## Save in cache if overall static.
@cache.set key, value if isStatic
value
makeRule: (key) ->
executed = false
recurse = (value, usedKey) =>
while value?
#if typeof value == 'string'
# run value
# executed = true
# return
if value instanceof Map or value instanceof WeakMap
value = value.get key
usedKey = true
else if typeof value == 'function'
## Functions without arguments only match default rule '',
## unless we've already used the rule through object or Map lookup.
return if key and not usedKey and not value.length
value = value.call @, key, @
## Function can return `null` (not `undefined`) to say 'no rule'.
executed = true unless value == null
return
else if Array.isArray value
for item in value
recurse item, usedKey
return
else if typeof value == 'object'
if value.hasOwnProperty key # avoid inherited property e.g. toString
value = value[key]
usedKey = true
else
value = undefined
else
console.warn "Unsupported data type #{typeof value} in looking up Maketile rule '#{key}'"
value = undefined
value
recurse @module?.make ? @module?.default
executed
onInit: (fn) ->
@initQueue.push fn
preprocess: (fn) ->
Expand Down Expand Up @@ -2307,9 +2354,9 @@ extensionMap =
'.styl': StylusStyle
# Other
'.svg': SVGFile
'.args': Args
'.args': ParsedArgs
#filenameMap =
# 'maketile': Args
# 'maketile': ParsedArgs

renderDOM = (elts, settings) ->
if typeof elts == 'string'
Expand Down Expand Up @@ -2541,20 +2588,14 @@ class Driver extends HasSettings
if i >= args.length and not numFileArgs and not ranMaketile and
@loadMaketile()?
ranMaketile = true
if @maketile instanceof Args
showHelp = "No filename arguments on command line or in '#{@maketile.filename}'"
args.push ...@maketile.args
else if @maketile instanceof Mapping
showHelp = false
@maketile.doInit()
if @maketile.isFunction()
console.log "** #{@maketile.filename} - default"
@maketile.map()
else
console.log "Maketile '#{@maketile.filename}' did not `export default` a function"
showHelp = false
if @maketile.makeRule? # Args or Mapping
@maketile.doInit?()
console.log "** #{@maketile.filename} (default)"
unless @maketile.makeRule ''
console.log "Maketile '#{@maketile.filename}' does not define a default rule"
else
showHelp = false
console.log "Unrecognized Maketile '#{@maketile.filename}' of type '#{@maketile.constructor?.name}'"
console.warn "Unrecognized Maketile '#{@maketile.filename}' of type '#{@maketile.constructor?.name}'"
break if i >= args.length

## Split up single-dash arguments with multiple options like -pP
Expand Down Expand Up @@ -2671,12 +2712,15 @@ class Driver extends HasSettings
## Otherwise, check for a matching rule in the Maketile.
else
files = []
if @loadMaketile()?.module?.hasOwnProperty arg
if arg.includes '.'
console.warn "No such file or directory '#{arg}', and not a valid Maketile rule name"
else if @loadMaketile()?
console.log "** #{@maketile.filename} - #{arg}"
@maketile.doInit()
@maketile.module[arg]()
@maketile.doInit?()
unless @maketile.makeRule arg
console.warn "Maketile '#{@maketile.filename}' does not define rule '#{arg}'"
else
console.warn "No files or Maketile rules match '#{arg}'"
console.warn "No such file or directory '#{arg}', and no Maketile to define rules"
append = i+1 # where to append Args
for file in files
if typeof file == 'string'
Expand Down Expand Up @@ -2745,6 +2789,7 @@ class Driver extends HasSettings
if showHelp
console.log showHelp
#help()
return

run: (...protoArgs) ->
args = []
Expand Down Expand Up @@ -2780,7 +2825,7 @@ svgtiler = Object.assign run, {
DSVDrawing, SSVDrawing, CSVDrawing, TSVDrawing,
Drawings, XLSXDrawings,
Style, CSSStyle, StylusStyle, Styles,
Args, SVGFile,
Args, ParsedArgs, SVGFile,
extensionMap, Input, DummyInput, ArrayWrapper,
Render, getRender, runWithRender, onInit, preprocess, postprocess,
id: globalId, def: globalDef, background: globalBackground,
Expand Down

0 comments on commit 97a8a0b

Please sign in to comment.