From ea1ede59e591f1c7568ae9fd7f8bef81c05e5c2c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 13 Feb 2024 17:31:16 -0500 Subject: [PATCH] Support nested if statements --- package.json | 2 +- src/cli/mapshaper-run-commands.mjs | 1 - src/commands/mapshaper-if-elif-else-endif.mjs | 27 +++------ src/mapshaper-control-flow.mjs | 56 ++++++++++++------- test/if-elif-else-test.mjs | 33 +++++++++++ 5 files changed, 80 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index d3aef23f..0071b6f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.6.62", + "version": "0.6.63", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-run-commands.mjs b/src/cli/mapshaper-run-commands.mjs index 845135fb..60a569e3 100644 --- a/src/cli/mapshaper-run-commands.mjs +++ b/src/cli/mapshaper-run-commands.mjs @@ -8,7 +8,6 @@ import { error, UserError, message, print, loggingEnabled, printError } from '.. import { Job } from '../mapshaper-job'; import { runningInBrowser } from '../mapshaper-env'; import utils from '../utils/mapshaper-utils'; -import { resetControlFlow } from '../mapshaper-control-flow'; import require from '../mapshaper-require'; import { commandTakesFileInput } from '../cli/mapshaper-command-info'; import { version } from '../../package.json'; diff --git a/src/commands/mapshaper-if-elif-else-endif.mjs b/src/commands/mapshaper-if-elif-else-endif.mjs index c1dd9f86..15e92535 100644 --- a/src/commands/mapshaper-if-elif-else-endif.mjs +++ b/src/commands/mapshaper-if-elif-else-endif.mjs @@ -3,13 +3,14 @@ import cmd from '../mapshaper-cmd'; import { layerIsEmpty } from '../dataset/mapshaper-layer-utils'; import { stop } from '../utils/mapshaper-logging'; import { - resetControlFlow, inControlBlock, enterActiveBranch, enterInactiveBranch, inActiveBranch, - blockWasActive, - jobIsStopped + blockIsComplete, + jobIsStopped, + enterBlock, + leaveBlock } from '../mapshaper-control-flow'; import { compileIfCommandExpression } from '../expressions/mapshaper-layer-expressions'; @@ -17,13 +18,11 @@ export function skipCommand(cmdName, job) { // allow all control commands to run if (jobIsStopped(job)) return true; if (isControlFlowCommand(cmdName)) return false; - return inControlBlock(job) && !inActiveBranch(job); + return !inActiveBranch(job); } cmd.if = function(job, opts) { - if (inControlBlock(job)) { - stop('Nested -if commands are not supported.'); - } + enterBlock(job); evaluateIf(job, opts); }; @@ -38,7 +37,7 @@ cmd.else = function(job) { if (!inControlBlock(job)) { stop('-else command must be preceded by an -if command.'); } - if (blockWasActive(job)) { + if (blockIsComplete(job)) { enterInactiveBranch(job); } else { enterActiveBranch(job); @@ -49,7 +48,7 @@ cmd.endif = function(job) { if (!inControlBlock(job)) { stop('-endif command must be preceded by an -if command.'); } - resetControlFlow(job); + leaveBlock(job); }; function isControlFlowCommand(cmd) { @@ -57,24 +56,16 @@ function isControlFlowCommand(cmd) { } function test(catalog, opts) { - // var targ = getTargetLayer(catalog, opts); if (opts.expression) { return compileIfCommandExpression(opts.expression, catalog, opts)(); } - // if (opts.empty) { - // return layerIsEmpty(targ.layer); - // } - // if (opts.not_empty) { - // return !layerIsEmpty(targ.layer); - // } return true; } function evaluateIf(job, opts) { - if (!blockWasActive(job) && test(job.catalog, opts)) { + if (!blockIsComplete(job) && test(job.catalog, opts)) { enterActiveBranch(job); } else { enterInactiveBranch(job); } } - diff --git a/src/mapshaper-control-flow.mjs b/src/mapshaper-control-flow.mjs index b9084a98..4b99a33e 100644 --- a/src/mapshaper-control-flow.mjs +++ b/src/mapshaper-control-flow.mjs @@ -1,41 +1,59 @@ -export function resetControlFlow(job) { - job.control = null; -} - export function stopJob(job) { - getState(job).stopped = true; + job.stopped = true; } export function jobIsStopped(job) { - return getState(job).stopped === true; + return job.stopped === true; } export function inControlBlock(job) { - return !!getState(job).inControlBlock; + return getStack(job).length > 0; +} + +export function enterBlock(job) { + var stack = getStack(job); + // skip over a block if it is inside an inactive branch + stack.push({ + active: false, + complete: !inActiveBranch(job) + }); +} + +export function leaveBlock(job) { + var stack = getStack(job); + stack.pop(); } export function enterActiveBranch(job) { - var state = getState(job); - state.inControlBlock = true; - state.active = true; - state.complete = true; + var block = getCurrentBlock(job); + block.active = true; + block.complete = true; } export function enterInactiveBranch(job) { - var state = getState(job); - state.inControlBlock = true; - state.active = false; + var block = getCurrentBlock(job); + block.active = false; +} + +export function blockIsComplete(job) { + var block = getCurrentBlock(job); + return block.complete; } -export function blockWasActive(job) { - return !!getState(job).complete; +function getCurrentBlock(job) { + var stack = getStack(job); + return stack[stack.length-1]; } +// A branch is considered to be active if it and all its parents are active +// (Main branch is considered to be active) export function inActiveBranch(job) { - return !!getState(job).active; + var stack = getStack(job); + return stack.length === 0 || stack.every(block => block.active); } -function getState(job) { - return job.control || (job.control = {}); +function getStack(job) { + job.control = job.control || {stack: []}; + return job.control.stack; } diff --git a/test/if-elif-else-test.mjs b/test/if-elif-else-test.mjs index e2ddec8e..83ae16d2 100644 --- a/test/if-elif-else-test.mjs +++ b/test/if-elif-else-test.mjs @@ -18,6 +18,7 @@ describe('mapshaper-if-elif-else-endif.js', function () { }); }); + it ('test expression', function(done) { var data = { type: 'GeometryCollection', @@ -188,4 +189,36 @@ describe('mapshaper-if-elif-else-endif.js', function () { done(); }); }); + + it ('nested if statement 1', async function() { + var data = 'name\na'; + var cmd = `-i data.csv + -if true -each 'name="b"' + -if false -each 'name="c"' + -elif true -each 'name="d"' + -else -each 'name="e"' + -endif + -else -each name="f"' + -endif + -o`; + var out = await api.applyCommands(cmd, {'data.csv': data}); + assert.equal(out['data.csv'], 'name\nd'); + }) + + it ('nested if statement 2', async function() { + var data = 'name\na'; + var cmd = `-i data.csv + -if false -each 'name="b"' + -if true -each 'name="c"' + -elif true -each 'name="d"' + -else -each 'name="e"' + -endif + -else -each name="f"' + -endif + -o`; + var out = await api.applyCommands(cmd, {'data.csv': data}); + assert.equal(out['data.csv'], 'name\nf'); + }) + + })