Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for options to CLI and improves usability #586

Merged
merged 9 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ Or use the UMD bundle from jsDelivr:
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
```

More details, refer to [The Setup Guide][setup].
Or render directly from CLI using npx:

```bash
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
```

For more details, refer to the [Setup Guide][setup].

## Related Projects

Expand Down
117 changes: 102 additions & 15 deletions bin/liquid.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,111 @@
#!/usr/bin/env node

const fs = require('fs')
const Liquid = require('..').Liquid
const contextArg = process.argv.slice(2)[0]
let context = {}

if (contextArg) {
if (contextArg.endsWith('.json')) {
const fs = require('fs')
context = JSON.parse(fs.readFileSync(contextArg, 'utf8'))
} else {
context = JSON.parse(contextArg)

// Preserve compatibility by falling back to legacy CLI behavior if:
// - stdin is redirected (i.e. not connected to a terminal)
// - there are either no arguments, or only a single argument which does not start with a dash
// TODO: Remove this fallback for 11.0

if (!process.stdin.isTTY) {
if (process.argv.length === 2 || (process.argv.length === 3 && !process.argv[2].startsWith('-'))) {
renderLegacy()
return
}
}

const { program } = require('commander')

program
.name('npx liquidjs')
DaRosenberg marked this conversation as resolved.
Show resolved Hide resolved
.description('Render a Liquid template')
.requiredOption('-t, --template <path | liquid>', 'liquid template to render (as path or inline)') // TODO: Change to argument in 11.0
DaRosenberg marked this conversation as resolved.
Show resolved Hide resolved
.option('-c, --context <path | json>', 'input context in JSON format (as path or inline; omit to read from stdin)')
.option('-o, --output <path>', 'write rendered output to file (omit to write to stdout)')
.option('--cache [size]', 'cache previously parsed template structures (default cache size: 1024)')
.option('--extname <string>', 'use a default filename extension when resolving partials and layouts')
.option('--jekyll-include', 'use jekyll-style include (pass parameters to include variable of current scope)')
.option('--js-truthy', 'use JavaScript-style truthiness')
.option('--layouts <path...>', 'directories from where to resolve layouts (defaults to --root)')
.option('--lenient-if', 'do not throw on undefined variables in conditional expressions (when using --strict-variables)')
.option('--no-dynamic-paths', 'always treat file paths for partials and layouts as a literal value')
.option('--no-greedy-trim', 'disable greedy matching for --trim* options')
.option('--no-relative-paths', 'require absolute file paths for partials and layouts')
.option('--ordered-filter-parameters', 'respect parameter order when using filters')
.option('--output-delimiter-left <string>', 'left delimiter to use for liquid outputs')
.option('--output-delimiter-right <string>', 'right delimiter to use for liquid outputs')
.option('--partials <path...>', 'directories from where to resolve partials (defaults to --root)')
.option('--preserve-timezones', 'preserve input timezone in date filter')
.option('--root <path...>', 'directories from where to resolve partials and layouts (defaults to ".")')
.option('--strict-filters', 'throw on undefined filters instead of skipping them')
.option('--strict-variables', 'throw on undefined variables instead of rendering them as empty string')
.option('--tag-delimiter-left', 'left delimiter to use for liquid tags')
.option('--tag-delimiter-right', 'right delimiter to use for liquid tags')
.option('--timezone-offset <value>', 'JavaScript timezone name or timezoneOffset value to use in date filter (defaults to local timezone)')
.option('--trim-output-left', 'trim whitespace from left of liquid outputs')
.option('--trim-output-right', 'trim whitespace from right of liquid outputs')
.option('--trim-tag-left', 'trim whitespace from left of liquid tags')
.option('--trim-tag-right', 'trim whitespace from right of liquid tags')
.showHelpAfterError('Use -h or --help for additional information.')
.parse()

const options = program.opts()
const template = resolvePathOption(options.template)
const context = resolveContext(options.context)
const liquidOptions = Object.fromEntries(Object.entries(options).map(([key, value]) => {
switch (key) { // Remap options where CLI names differ from property names
DaRosenberg marked this conversation as resolved.
Show resolved Hide resolved
case 'dynamicPaths': return ['dynamicPartials', value]
case 'greedyTrim': return ['greedy', value]
case 'relativePaths': return ['relativeReference', value]
default: return [key, value]
}
}))

const liquid = new Liquid(liquidOptions)
const output = liquid.parseAndRenderSync(template, context)
if (options.output) {
fs.writeFileSync(options.output, output)
} else {
process.stdout.write(output)
process.stdout.write('\n')
DaRosenberg marked this conversation as resolved.
Show resolved Hide resolved
}

let tpl = ''
process.stdin.on('data', chunk => (tpl += chunk))
process.stdin.on('end', () => render(tpl))
function resolveContext (contextOption) {
let contextJson = '{}'
if (contextOption) {
contextJson = resolvePathOption(contextOption)
DaRosenberg marked this conversation as resolved.
Show resolved Hide resolved
} else if (!process.stdin.isTTY) { // Read context from stdin if not connected to a terminal
contextJson = fs.readFileSync(process.stdin.fd, 'utf8')
}
const context = JSON.parse(contextJson)
return context
}

async function render (tpl) {
function resolvePathOption (option) {
let content = null
if (option) {
const stat = fs.statSync(option, { throwIfNoEntry: false })
if (stat && stat.isFile) {
content = fs.readFileSync(option, 'utf8')
} else {
content = option
}
}
return content
}

// TODO: Remove for 11.0
function renderLegacy () {
process.stderr.write('Reading template from stdin. This mode will be removed in next major version, use --template option instead.\n')
const contextArg = process.argv.slice(2)[0]
let context = {}
if (contextArg) {
const contextJson = resolvePathOption(contextArg)
context = JSON.parse(contextJson)
}
const template = fs.readFileSync(process.stdin.fd, 'utf8')
const liquid = new Liquid()
const html = await liquid.parseAndRender(tpl, context)
process.stdout.write(html)
const output = liquid.parseAndRenderSync(template, context)
process.stdout.write(output)
}
31 changes: 27 additions & 4 deletions docs/source/tutorials/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,43 @@ Pre-built UMD bundles are also available:

## LiquidJS in CLI

LiquidJS is also available from CLI:
LiquidJS can also be used to render a template directly from CLI using `npx`:

```bash
echo '{{"hello" | capitalize}}' | npx liquidjs
npx liquidjs --template '{{"hello" | capitalize}}'
```

If you pass a path to a JSON file or a JSON string as the first argument, it will be used as the context for your template.
You can either pass the template inline (shown above) or you can read it from a file like so:

```bash
echo 'Hello, {{ name }}.' | npx liquidjs '{"name": "Snake"}'
npx liquidjs --template ./some-template.liquid
```

You can also pass a context, either inline or from a path or piped through `stdin`. The following three are equivalent:

```bash
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
npx liquidjs --template 'Hello, {{ name }}!' --context ./some-context.json
echo '{"name": "Snake"}' | npx liquidjs --template 'Hello, {{ name }}!'
```

The rendered output is written to `stdout` by default, but you can also specify an output file (if the file exists, it will be overwritten):

```bash
npx liquidjs --template '{{"hello" | capitalize}}' --output ./hello.txt
```

You can also pass a number of options to customize template rendering behavior. For example, the `--js-truthy` option can be used to enable JavaScript truthiness:

```bash
npx liquidjs --template ./some-template.liquid --js-truthy
```

Most of the [options available through the JavaScript API][options] are also available from the CLI. For help on available options, use `npx liquidjs --help`.

## Miscellaneous

A ReactJS demo is also added by [@stevenanthonyrevo](https://github.com/stevenanthonyrevo), see [liquidjs/demo/reactjs/](https://github.com/harttle/liquidjs/blob/master/demo/reactjs/).

[intro]: ./intro-to-liquid.html
[options]: ./options.md
7 changes: 3 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
"typedoc-plugin-markdown": "^2.2.17",
"typescript": "^4.5.3"
},
"dependencies": {
"commander": "^10.0.0"
},
"release": {
"branch": "master",
"plugins": [
Expand Down Expand Up @@ -160,6 +163,5 @@
"pre-commit": "npm run check",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"dependencies": {}
}
}