diff --git a/.gitignore b/.gitignore
index 9fe5dbbf8..64519d058 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ node_modules/
docs/themes/navy/source/js/liquid.browser.min.js
docs/themes/navy/layout/partial/all-contributors.swig
docs/themes/navy/layout/partial/financial-contributors.swig
+docs/themes/navy/layout/partial/used-by.swig
dist/
demo/*/yarn.json
diff --git a/README.md b/README.md
index d7b98f319..9461d2b10 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# LiquidJS
-> A simple, expressive and safe [Shopify Liquid][shopify/liquid] template engine for JavaScript — compatible with Jekyll, GitHub Pages, and Shopify themes.
+A simple, expressive [Shopify Liquid][shopify/liquid] template engine for JavaScript — compatible with Jekyll and GitHub Pages dialects, runs in Node.js, the browser (UMD/ESM), and the CLI via `npx liquidjs`, and ships with TypeScript definitions. Extend it with custom tags, filters, and [plugins][plugins]; optional `memoryLimit`, `renderLimit`, and `ownPropertyOnly` help harden untrusted templates (not a sandbox — see the [security model][security-model]).
[](https://www.npmjs.org/package/liquidjs)
[](https://www.npmjs.org/package/liquidjs)
@@ -10,6 +10,12 @@
[Documentation][doc] · [Playground](https://liquidjs.com/playground.html) · [Setup guide][setup] · [Contributing][contribution]
+
+
+
+
+
Template, context, and live output in the playground.
+
## Quick start
```js
@@ -23,14 +29,6 @@ const html = await engine.parseAndRender(
//=> 'Hello, Liquid!'
```
-## Features
-
-- **Compatible** — Shopify Liquid, Jekyll, and GitHub Pages dialects
-- **Safe by default** — `ownPropertyOnly`, `memoryLimit`, and `renderLimit` help sandbox untrusted templates
-- **Runs everywhere** — Node.js, browser (UMD/ESM), and CLI via `npx liquidjs`
-- **Extensible** — custom tags, filters, and [plugins][plugins]
-- **Typed** — TypeScript definitions included
-
## Installation
**Node.js**
@@ -53,35 +51,28 @@ npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Liquid"}'
See the [setup guide][setup] for partials, layouts, caching, and other options.
-## Example
-
-Liquid templates use tags (`{% %}`) and outputs (`{{ }}`):
-
-```liquid
-{% if username %}
- {{ username | append: ", welcome to LiquidJS!" | capitalize }}
-{% endif %}
-```
-
-Try it in the [playground](https://liquidjs.com/playground.html) or read the [Liquid syntax tutorial](https://liquidjs.com/tutorials/intro-to-liquid.html).
-
## Used by
-- [Eleventy](https://www.11ty.dev/)
-- [GitHub Docs](https://github.com/github/docs)
-- [Kibana](https://github.com/elastic/kibana)
-- [Microsoft Power Pages](https://learn.microsoft.com/en-us/power-pages/introduction)
-- [Azure API Management developer portal](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-developer-portal)
-- [Directus](https://docs.directus.io/)
-- [Builder.io](https://www.builder.io/m/developers)
-- [Mitosis](https://github.com/BuilderIO/mitosis)
-- [Pattern Lab](https://patternlab.io/)
-- [Opensense](https://www.opensense.com/)
-- [Rock RMS](https://www.rockrms.com/)
-- [WISMOlabs](https://wismolabs.com/)
-- [Freshet](https://chromewebstore.google.com/detail/freshet/mpclplhdencffbilobpcapccnihpelcg)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-Using LiquidJS in production? [Open a PR](https://github.com/harttle/liquidjs/edit/master/README.md) to add your project.
+Products and projects running on LiquidJS. [Open a PR](https://github.com/harttle/liquidjs/edit/master/data/used-by.json) to add yours.
## Financial Support
@@ -93,7 +84,7 @@ If you personally love LiquidJS or it's benefiting your business, please conside
-
+
@@ -260,3 +251,4 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
[github]: https://github.com/harttle/liquidjs
[oc]: https://opencollective.com/liquidjs/
[contribution]: https://liquidjs.com/tutorials/contribution-guidelines.html
+[security-model]: https://liquidjs.com/tutorials/security-model.html
diff --git a/bin/build-used-by.js b/bin/build-used-by.js
new file mode 100644
index 000000000..cfbda472b
--- /dev/null
+++ b/bin/build-used-by.js
@@ -0,0 +1,115 @@
+const fs = require('fs')
+const path = require('path')
+
+const root = path.resolve(__dirname, '..')
+const readmePath = path.join(root, 'README.md')
+const dataPath = path.join(root, 'data/used-by.json')
+const outDir = path.join(root, 'docs/themes/navy/layout/partial')
+
+const LINK_STYLE = 'display: inline-block; vertical-align: middle; margin: 8px;'
+const IMG_STYLE = 'vertical-align: middle;'
+const PER_ROW = 7
+
+function escapeHtml (str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+}
+
+function websiteFavicon (url) {
+ return 'https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url='
+ + encodeURIComponent(url) + '&size=128'
+}
+
+async function githubAvatar (url) {
+ try {
+ const { hostname, pathname } = new URL(url)
+ if (hostname !== 'github.com') return
+ const owner = pathname.split('/').filter(Boolean)[0]
+ if (!owner) return
+ const res = await fetch(`https://api.github.com/users/${encodeURIComponent(owner)}`, {
+ headers: { 'User-Agent': 'liquidjs-build-used-by' }
+ })
+ if (!res.ok) return
+ const data = await res.json()
+ if (!data.avatar_url) return
+ const base = data.avatar_url.split('?')[0]
+ return `${base}?s=128`
+ } catch {}
+}
+
+async function resolveLogo (entry) {
+ if (entry.logo) return entry.logo
+ const gh = await githubAvatar(entry.url)
+ if (gh) return gh
+ return websiteFavicon(entry.url)
+}
+
+function imgTag ({ name, src, width, height, imgStyle }) {
+ const alt = escapeHtml(name)
+ const href = escapeHtml(src)
+ const dims = []
+ if (width) dims.push(`width="${width}"`)
+ if (height) dims.push(`height="${height}"`)
+ else if (!width) dims.push('height="80"')
+ const style = imgStyle ? `${IMG_STYLE}${imgStyle}` : IMG_STYLE
+ return `
`
+}
+
+function linkTag (entry, logo) {
+ const href = escapeHtml(entry.url)
+ const img = imgTag({
+ name: entry.name,
+ src: logo,
+ width: entry.width,
+ height: entry.height,
+ imgStyle: entry.imgStyle
+ })
+ return ` ${img}`
+}
+
+async function renderHtml (entries) {
+ const logos = await Promise.all(entries.map(resolveLogo))
+ const links = entries.map((entry, i) => linkTag(entry, logos[i]))
+ const lines = ['']
+ links.forEach((link, i) => {
+ if (i > 0 && i % PER_ROW === 0 && links.length - i > 2) lines.push('
')
+ lines.push(link)
+ })
+ lines.push('
')
+ return lines.join('\n')
+}
+
+function transformUsedBy (html) {
+ return html
+ .replace(/
.*?<\/td>/g, '')
+ .replace(/\n/g, '')
+ .replace(/<\/tr>\s*/g, '')
+}
+
+function patchReadme (html) {
+ const readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n')
+ const begin = ''
+ const end = ''
+ const re = new RegExp(`${begin}[\\s\\S]*?${end}`)
+ if (!re.test(readme)) {
+ throw new Error(`README.md is missing ${begin} … ${end}`)
+ }
+ const next = readme.replace(re, `${begin}\n${html}\n${end}`)
+ fs.writeFileSync(readmePath, next)
+}
+
+const entries = JSON.parse(fs.readFileSync(dataPath, 'utf8'))
+
+async function main () {
+ const html = await renderHtml(entries)
+ patchReadme(html)
+ fs.writeFileSync(path.join(outDir, 'used-by.swig'), transformUsedBy(html))
+}
+
+main().catch((err) => {
+ console.error(err)
+ process.exit(1)
+})
diff --git a/bin/capture-readme-playground.mjs b/bin/capture-readme-playground.mjs
new file mode 100644
index 000000000..2e636ec0f
--- /dev/null
+++ b/bin/capture-readme-playground.mjs
@@ -0,0 +1,331 @@
+import { spawn, execFile } from 'node:child_process'
+import { access, copyFile, mkdir, rm } from 'node:fs/promises'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { promisify } from 'node:util'
+import { chromium } from 'playwright'
+
+const execFileAsync = promisify(execFile)
+const root = join(dirname(fileURLToPath(import.meta.url)), '..')
+const docsDir = join(root, 'docs')
+const framesDir = join(root, '.tmp/playground-gif-frames')
+const outPath = join(docsDir, 'source/playground-demo.gif')
+const url = 'http://127.0.0.1:4001/playground.html'
+
+const CAPTURE_WIDTH = 980
+const VIEWPORT_WIDTH = 1100
+const VIEWPORT_HEIGHT = 1000
+const DEVICE_SCALE = 2
+const FPS = 12
+const LAST_FRAME_HOLD = 2.5
+const CHAR_MS = 55
+
+const LINE_HEIGHT = 21
+const WINDOW_CHROME = 36
+const GRID_GAP = 16
+const EDITOR_PAD = 16
+
+function run (cmd, args, opts = {}) {
+ return new Promise((resolve, reject) => {
+ const child = spawn(cmd, args, { stdio: 'inherit', shell: true, ...opts })
+ child.on('error', reject)
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))))
+ })
+}
+
+async function waitForServer (maxMs = 120000) {
+ const start = Date.now()
+ while (Date.now() - start < maxMs) {
+ try {
+ const res = await fetch(url)
+ if (res.ok) return
+ } catch {}
+ await new Promise((r) => setTimeout(r, 500))
+ }
+ throw new Error(`Timed out waiting for ${url}`)
+}
+
+async function ensureDocsDeps () {
+ try {
+ await access(join(docsDir, 'node_modules/hexo/package.json'))
+ } catch {
+ await run('npm', ['ci'], { cwd: docsDir })
+ }
+}
+
+async function prepareCapture (page, { fullTemplate, fullContext, fullHtml }) {
+ await page.addStyleTag({
+ content: `
+ #editors .capture-pane {
+ margin: 0;
+ box-sizing: border-box;
+ font: 14px/1.5 "Source Code Pro", ui-monospace, Monaco, Menlo, Consolas, monospace;
+ white-space: pre;
+ overflow: hidden;
+ color: #24292f;
+ background: #fff;
+ }
+ #editors .capture-cursor {
+ color: #24292f;
+ }
+ #editors .capture-json .json-key { color: #953800; }
+ #editors .capture-json .json-string { color: #cf222e; }
+ `
+ })
+
+ await page.evaluate((opts) => {
+ const {
+ captureWidth,
+ fullTemplate,
+ fullContext,
+ fullHtml,
+ lineHeight,
+ windowChrome,
+ gridGap,
+ editorPad
+ } = opts
+
+ function paneHeight (lines) {
+ return windowChrome + editorPad * 2 + lines * lineHeight + 4
+ }
+
+ function escapeHtml (str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ }
+
+ function highlightJson (text) {
+ return escapeHtml(text)
+ .replace(/"([^"]+)":/g, '"$1":')
+ .replace(/: "([^"]*)"/g, ': "$1"')
+ }
+
+ function mountPane (shell, className, html) {
+ shell.innerHTML = ''
+ shell.style.paddingTop = '0'
+ shell.style.overflow = 'hidden'
+ const pre = document.createElement('pre')
+ pre.className = 'capture-pane ' + className
+ pre.style.height = '100%'
+ pre.style.padding = `${editorPad}px`
+ if (html) pre.innerHTML = html
+ shell.appendChild(pre)
+ return pre
+ }
+
+ document.querySelector('#playground .version')?.remove()
+ document.querySelector('#playground h1')?.remove()
+ document.querySelector('.loader')?.remove()
+ document.querySelector('#playground > .wrapper')?.style.setProperty('margin-bottom', '0')
+ document.body.style.background = '#fff'
+ document.querySelector('#playground').style.background = '#fff'
+
+ window.__captureEngine = new liquidjs.Liquid({ memoryLimit: 1e5, renderLimit: 1e5 })
+ window.__captureContext = JSON.parse(fullContext)
+ window.__captureLastHtml = ''
+
+ const templateLineCount = fullTemplate.split('\n').length
+ const contextLineCount = fullContext.split('\n').length
+ const outputLineCount = Math.max(fullHtml.split('\n').length, 1)
+
+ const templateEditorH = paneHeight(templateLineCount)
+ const contextEditorH = paneHeight(contextLineCount)
+ const outputEditorH = paneHeight(outputLineCount)
+
+ const leftRow1 = templateEditorH
+ const leftRow2 = contextEditorH
+ const rightCol = leftRow1 + gridGap + leftRow2
+ const gridH = rightCol
+
+ const editors = document.querySelector('#editors')
+ editors.classList.remove('inner')
+ editors.style.margin = '0 auto'
+ editors.style.width = captureWidth + 'px'
+ editors.style.maxWidth = captureWidth + 'px'
+ editors.style.height = gridH + 'px'
+ editors.style.minHeight = gridH + 'px'
+ editors.style.overflow = 'visible'
+ editors.style.display = 'grid'
+ editors.style.gridTemplateColumns = '1fr 1fr'
+ editors.style.gridTemplateRows = `${leftRow1}px ${leftRow2}px`
+ editors.style.gridGap = gridGap + 'px'
+ editors.style.alignItems = 'start'
+
+ document.querySelectorAll('.editor-wrapper').forEach((wrapper) => {
+ wrapper.style.overflow = 'visible'
+ wrapper.style.minHeight = '0'
+ })
+
+ document.querySelector('.area-output').style.gridRow = '1 / -1'
+ document.querySelector('.area-output').style.gridColumn = '2'
+
+ const tplShell = document.querySelector('#editorEl')
+ const ctxShell = document.querySelector('#dataEl')
+ const tplPane = tplShell.closest('.pane-window')
+ const ctxPane = ctxShell.closest('.pane-window')
+ tplShell.closest('.editor-wrapper').style.height = templateEditorH + 'px'
+ ctxShell.closest('.editor-wrapper').style.height = contextEditorH + 'px'
+ tplPane.style.height = templateEditorH + 'px'
+ ctxPane.style.height = contextEditorH + 'px'
+ tplShell.style.height = (templateEditorH - windowChrome) + 'px'
+ ctxShell.style.height = (contextEditorH - windowChrome) + 'px'
+
+ window.__captureTpl = mountPane(tplShell, 'capture-liquid', '')
+ window.__captureCtx = mountPane(ctxShell, 'capture-json', highlightJson(fullContext))
+
+ const code = document.querySelector('#previewCode')
+ code.textContent = ''
+ if (window.Prism) {
+ delete code.dataset.highlighted
+ window.Prism.highlightElement(code)
+ }
+
+ const outputPane = document.querySelector('.area-output .pane-window')
+ const outputShell = document.querySelector('.output-preview')
+ const outputWrap = document.querySelector('.area-output')
+ outputWrap.style.height = rightCol + 'px'
+ outputPane.style.height = rightCol + 'px'
+ outputShell.style.height = (rightCol - windowChrome) + 'px'
+ outputShell.style.flex = 'none'
+ outputShell.style.overflow = 'hidden'
+ outputShell.style.paddingTop = '0'
+ const pre = outputShell.querySelector('pre.highlight')
+ pre.style.minHeight = '0'
+ pre.style.height = '100%'
+ pre.style.padding = editorPad + 'px'
+ pre.style.margin = '0'
+ pre.style.boxSizing = 'border-box'
+ pre.style.border = 'none'
+ pre.style.boxShadow = 'none'
+ }, {
+ captureWidth: CAPTURE_WIDTH,
+ fullTemplate,
+ fullContext,
+ fullHtml,
+ lineHeight: LINE_HEIGHT,
+ windowChrome: WINDOW_CHROME,
+ gridGap: GRID_GAP,
+ editorPad: EDITOR_PAD
+ })
+
+ await page.waitForTimeout(200)
+}
+
+async function setCaptureFrame (page, templateText) {
+ await page.evaluate(async (tpl) => {
+ function escapeHtml (str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ }
+
+ window.__captureTpl.innerHTML =
+ escapeHtml(tpl) + '▎'
+
+ let html = window.__captureLastHtml || ''
+ try {
+ html = await window.__captureEngine.parseAndRender(tpl, window.__captureContext)
+ window.__captureLastHtml = html
+ } catch {}
+
+ const code = document.querySelector('#previewCode')
+ code.textContent = html
+ if (window.Prism) {
+ delete code.dataset.highlighted
+ window.Prism.highlightElement(code)
+ }
+ }, templateText)
+}
+
+async function screenshotEditors (page, file) {
+ await page.locator('#editors').screenshot({
+ path: file,
+ animations: 'disabled',
+ type: 'png',
+ scale: 'device'
+ })
+}
+
+async function encodeGif () {
+ await execFileAsync('ffmpeg', [
+ '-y',
+ '-framerate', String(FPS),
+ '-i', join(framesDir, 'frame-%03d.png'),
+ '-vf', `tpad=stop_mode=clone:stop_duration=${LAST_FRAME_HOLD},split[s0][s1];[s0]palettegen=max_colors=256:stats_mode=full[p];[s1][p]paletteuse=dither=floyd_steinberg`,
+ '-loop', '0',
+ outPath
+ ], { stdio: 'inherit' })
+}
+
+async function main () {
+ await run('npm', ['run', 'build:docs-liquid'], { cwd: root })
+ await run('npm', ['run', 'build:contributors'], { cwd: root })
+ await run('npm', ['run', 'build:used-by'], { cwd: root })
+ await ensureDocsDeps()
+ await rm(framesDir, { recursive: true, force: true })
+ await mkdir(framesDir, { recursive: true })
+
+ const server = spawn('npx', ['hexo', 'server', '-p', '4001'], {
+ cwd: docsDir,
+ shell: true,
+ stdio: 'ignore'
+ })
+
+ try {
+ await waitForServer()
+
+ const browser = await chromium.launch()
+ const page = await browser.newPage({
+ viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT },
+ deviceScaleFactor: DEVICE_SCALE
+ })
+ await page.emulateMedia({ colorScheme: 'light' })
+ await page.goto(url, { waitUntil: 'networkidle' })
+
+ const allowCookies = page.getByRole('button', { name: 'Allow all cookies' })
+ if (await allowCookies.count()) {
+ await allowCookies.click()
+ }
+
+ await page.waitForSelector('#editors:not(.hide)', { timeout: 60000 })
+ await page.waitForFunction(() => {
+ return window.ace?.edit?.('editorEl')?.getValue().includes('name | capitalize')
+ })
+
+ const fullTemplate = await page.evaluate(() => window.ace.edit('editorEl').getValue())
+ const fullContext = await page.evaluate(() => window.ace.edit('dataEl').getValue())
+ const fullHtml = await page.evaluate(() => document.querySelector('#previewCode').textContent)
+
+ await prepareCapture(page, { fullTemplate, fullContext, fullHtml })
+
+ const framePaths = []
+ let frameIndex = 0
+
+ for (let i = 0; i <= fullTemplate.length; i++) {
+ const partial = fullTemplate.slice(0, i)
+ await setCaptureFrame(page, partial)
+ await page.waitForTimeout(CHAR_MS)
+ const file = join(framesDir, `frame-${String(frameIndex).padStart(3, '0')}.png`)
+ framePaths.push(file)
+ await screenshotEditors(page, file)
+ frameIndex++
+ }
+
+ await encodeGif()
+ await mkdir(join(root, '.tmp'), { recursive: true })
+ await copyFile(framePaths[framePaths.length - 1], join(root, '.tmp/playground-gif-last-frame.png'))
+ await browser.close()
+ console.log(`Wrote ${outPath} (${framePaths.length} frames @ ${FPS} fps, char typing + live output)`)
+ } finally {
+ server.kill()
+ await rm(framesDir, { recursive: true, force: true })
+ }
+}
+
+main().catch((err) => {
+ console.error(err)
+ process.exit(1)
+})
diff --git a/data/used-by.json b/data/used-by.json
new file mode 100644
index 000000000..eb73ae271
--- /dev/null
+++ b/data/used-by.json
@@ -0,0 +1,27 @@
+[
+ { "name": "Eleventy", "url": "https://www.11ty.dev/" },
+ { "name": "GitHub Docs", "url": "https://docs.github.com/" },
+ { "name": "Kibana", "url": "https://www.elastic.co/kibana" },
+ {
+ "name": "Power Pages, Azure API Management developer portal",
+ "url": "https://learn.microsoft.com/en-us/power-pages/"
+ },
+ {
+ "name": "Shopify CLI, Checkout Blocks",
+ "url": "https://www.shopify.com/"
+ },
+ { "name": "Directus", "url": "https://directus.io/" },
+ {
+ "name": "Builder.io, Mitosis",
+ "url": "https://www.builder.io/"
+ },
+ { "name": "Pattern Lab", "url": "https://patternlab.io/" },
+ { "name": "Rock RMS", "url": "https://www.rockrms.com/" },
+ { "name": "WISMOlabs", "url": "https://wismolabs.com/" },
+ { "name": "Dropkiq", "url": "https://www.dropkiq.com/" },
+ {
+ "name": "Freshet",
+ "url": "https://chromewebstore.google.com/detail/freshet/mpclplhdencffbilobpcapccnihpelcg",
+ "logo": "https://raw.githubusercontent.com/MattAltermatt/freshet/main/public/icon-128.png"
+ }
+]
diff --git a/docs/package.json b/docs/package.json
index 71fbcf87a..28252b62e 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -6,6 +6,7 @@
"version": "7.3.0"
},
"scripts": {
+ "prebuild": "npm --prefix .. run build:contributors && npm --prefix .. run build:used-by",
"build": "hexo generate",
"start": "hexo serve",
"lint": "eslint .",
diff --git a/docs/source/playground-demo.gif b/docs/source/playground-demo.gif
new file mode 100644
index 000000000..bd1fd8d25
Binary files /dev/null and b/docs/source/playground-demo.gif differ
diff --git a/docs/themes/navy/languages/en.yml b/docs/themes/navy/languages/en.yml
index f8a787a34..c18fd497a 100644
--- a/docs/themes/navy/languages/en.yml
+++ b/docs/themes/navy/languages/en.yml
@@ -13,7 +13,10 @@ index:
description: 'Thanks to these wonderful people! See contribution guidelines if you'd like to help.'
sponsors:
title: Sponsors
- description: 'If you personally love LiquidJS or it's benefiting your business, please sponsor us!'
+ description: 'Organizations and individuals who sponsor LiquidJS. Thank you!'
+ used_by:
+ title: Used by
+ description: 'Products and projects running on LiquidJS. Open a PR to add yours.'
playground:
title: Playground
diff --git a/docs/themes/navy/layout/index.swig b/docs/themes/navy/layout/index.swig
index 7b30eeabf..da3289319 100644
--- a/docs/themes/navy/layout/index.swig
+++ b/docs/themes/navy/layout/index.swig
@@ -1,42 +1,37 @@
-
-
-
-
- {{ page.content }}
-
-
-
-
-
-
-
-
- {{ partial('partial/all-contributors') }}
-
-
-
-
-
+
+
+
+
+ {{ page.content }}
+
+
+
+{{ partial('partial/home-section', {
+ wrapId: 'used-by',
+ variant: 'logos',
+ title: __('index.used_by.title'),
+ description: __('index.used_by.description'),
+ contentPartial: 'used-by'
+}) }}
+{{ partial('partial/home-section', {
+ wrapId: 'sponsors',
+ variant: 'logos',
+ title: __('index.sponsors.title'),
+ description: __('index.sponsors.description'),
+ contentPartial: 'financial-contributors'
+}) }}
+{{ partial('partial/home-section', {
+ wrapId: 'contributors',
+ variant: 'people',
+ title: __('index.contributors.title'),
+ description: __('index.contributors.description'),
+ contentPartial: 'all-contributors'
+}) }}
diff --git a/docs/themes/navy/layout/partial/after_footer.swig b/docs/themes/navy/layout/partial/after_footer.swig
index e6778541a..4cc27d2c8 100644
--- a/docs/themes/navy/layout/partial/after_footer.swig
+++ b/docs/themes/navy/layout/partial/after_footer.swig
@@ -1,6 +1,8 @@
{% if page.layout === 'playground' %}
{{ js('js/liquid.browser.min.js') }}
+
+
{% endif %}
{{ js('js/main') }}
diff --git a/docs/themes/navy/layout/partial/demo.json b/docs/themes/navy/layout/partial/demo.json
index d237b3911..20e523f8b 100644
--- a/docs/themes/navy/layout/partial/demo.json
+++ b/docs/themes/navy/layout/partial/demo.json
@@ -1,7 +1,3 @@
{
- "people": [
- "alice",
- "bob",
- "carol"
- ]
+ "name": "liquid"
}
diff --git a/docs/themes/navy/layout/partial/demo.liquid b/docs/themes/navy/layout/partial/demo.liquid
index b8bf0d5ea..0b4e32285 100644
--- a/docs/themes/navy/layout/partial/demo.liquid
+++ b/docs/themes/navy/layout/partial/demo.liquid
@@ -1,9 +1 @@
-
+Hello, {{ name | capitalize }}!
diff --git a/docs/themes/navy/layout/partial/home-section.swig b/docs/themes/navy/layout/partial/home-section.swig
new file mode 100644
index 000000000..7a25a8aa7
--- /dev/null
+++ b/docs/themes/navy/layout/partial/home-section.swig
@@ -0,0 +1,19 @@
+
+
+
+
+
+ {% if contentPartial == 'used-by' %}
+ {{ partial('partial/used-by') }}
+ {% elif contentPartial == 'financial-contributors' %}
+ {{ partial('partial/financial-contributors') }}
+ {% else %}
+ {{ partial('partial/all-contributors') }}
+ {% endif %}
+
+
+
+
diff --git a/docs/themes/navy/layout/playground.swig b/docs/themes/navy/layout/playground.swig
index e09cfa1d0..061c42905 100644
--- a/docs/themes/navy/layout/playground.swig
+++ b/docs/themes/navy/layout/playground.swig
@@ -4,16 +4,24 @@
-
Template
-
{{ raw('partial/demo.liquid') }}
+
+
Template
+
{{ raw('partial/demo.liquid') }}
+
-
Context
-
{{ raw('partial/demo.json') }}
+
+
Context
+
{{ raw('partial/demo.json') }}
+
-
Output
-
{{__('playground.loading')}}
+
+
Output
+
+
{{__('playground.loading')}}
+
+
diff --git a/docs/themes/navy/source/css/_partial/index.styl b/docs/themes/navy/source/css/_partial/index.styl
index 082d7a927..701333122 100644
--- a/docs/themes/navy/source/css/_partial/index.styl
+++ b/docs/themes/navy/source/css/_partial/index.styl
@@ -126,7 +126,8 @@
background: var(--color-link-hover)
color: #fff
-#sponsors-wrap, #contributors-wrap
+#used-by-wrap, #sponsors-wrap, #contributors-wrap,
+.home-section
background: var(--color-navy-lighter)
border-top: 1px solid #161d24
border-bottom: 1px solid #161d24
@@ -166,24 +167,40 @@
&:hover
color: var(--color-link-hover)
-#sponsors-wrap
+.home-section--logos, #sponsors-wrap, #used-by-wrap
background: var(--color-content-bg)
.inner
h3
color: var(--color-default)
p
color: var(--color-gray)
- .open-collective
- max-width: 100%
@media (prefers-color-scheme: dark)
p
color: var(--color-default)
-#contributors-wrap
+.home-section--people, #contributors-wrap
border: none
- overflow: hidden;
+ overflow: hidden
.contributors
+ p
+ margin: 0
+ line-height: 2.5
+ text-align: center
+ a
+ display: inline-block
+ vertical-align: middle
+ margin: 8px
+ a img
+ display: inline-block
+ height: 72px
+ width: auto
+ max-width: 160px
+ vertical-align: middle
+ object-fit: contain
+ background: transparent
+ border-radius: 0
+ margin-bottom: 0
tr
display: flex
flex-wrap: wrap
diff --git a/docs/themes/navy/source/css/_partial/playground.styl b/docs/themes/navy/source/css/_partial/playground.styl
index 953dd1e88..f1230bc1c 100644
--- a/docs/themes/navy/source/css/_partial/playground.styl
+++ b/docs/themes/navy/source/css/_partial/playground.styl
@@ -13,14 +13,6 @@
margin-bottom: 24px
color: var(--color-default)
- h2
- font-size: 0.8125rem
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.04em
- color: var(--color-gray)
- margin: 0
-
#editors
display: grid
overflow: hidden
@@ -56,25 +48,80 @@
.editor-wrapper
display: flex
- gap: 8px
flex-direction: column
min-height: 0
overflow: hidden
- .editor
- flex: 1 1 auto
- min-height: 0
- position: relative
- code-block-chrome()
- overflow: hidden
- @media mq-mobile
- min-height: 240px
+
+ .pane-window
+ flex: 1 1 auto
+ min-height: 0
+ position: relative
+ display: flex
+ flex-direction: column
+ playground-window-chrome()
+ overflow: hidden
+ @media mq-mobile
+ min-height: 240px
+
+ .pane-tab
+ position: absolute
+ z-index: 5
+ top: 0
+ left: 0
+ right: 0
+ height: 36px
+ line-height: 36px
+ text-align: center
+ font-size: 0.75rem
+ font-weight: 600
+ letter-spacing: 0.02em
+ color: var(--color-gray)
+ pointer-events: none
+ user-select: none
+
+ .editor
+ flex: 1 1 auto
+ min-height: 0
+ position: relative
+ overflow: hidden
.ace_editor
- font-family: font-mono
- font-size: 14px
- line-height: 1.5
- border-radius: 6px
- .ace_scrollbar
- z-index: 2
+ top: 0
+ bottom: 0
+ height: auto !important
+
+ .output-preview
+ flex: 1 1 auto
+ min-height: 0
+ cursor: default
+ overflow: auto
+ pre.highlight
+ margin: 0
+ min-height: 100%
+ padding: 16px
+ border: none
+ box-shadow: none
+ border-radius: 0 0 6px 6px
+ background: var(--highlight-background)
+ code
+ display: block
+ font-size: 14px
+ line-height: 1.5
+ background: transparent
+ padding: 0
+ white-space: pre
+
+ .ace_editor
+ font-family: font-mono
+ font-size: 14px
+ line-height: 1.5
+ border-radius: 6px
+ .ace_gutter
+ display: none !important
+ width: 0 !important
+ .ace_scroller
+ left: 0 !important
+ .ace_scrollbar
+ z-index: 2
.version
font-size: 0.8125rem
diff --git a/docs/themes/navy/source/css/_variables.styl b/docs/themes/navy/source/css/_variables.styl
index cac6b94fe..2ef2bc84e 100644
--- a/docs/themes/navy/source/css/_variables.styl
+++ b/docs/themes/navy/source/css/_variables.styl
@@ -102,3 +102,20 @@ code-block-chrome()
border-radius: 6px
border: 1px solid var(--code-border)
box-shadow: var(--code-shadow)
+
+playground-window-chrome()
+ code-block-chrome()
+ padding-top: 36px
+ &:before
+ content: ''
+ position: absolute
+ z-index: 4
+ top: 0
+ left: 0
+ right: 0
+ height: 36px
+ background: var(--highlight-background)
+ border-bottom: 1px solid var(--code-border)
+ border-radius: 6px 6px 0 0
+ pointer-events: none
+ background-image: radial-gradient(circle at 12px 18px, #ff5f57 5px, transparent 5px), radial-gradient(circle at 28px 18px, #febc2e 5px, transparent 5px), radial-gradient(circle at 44px 18px, #28c840 5px, transparent 5px)
diff --git a/docs/themes/navy/source/js/main.js b/docs/themes/navy/source/js/main.js
index b113fb27b..9988013b6 100644
--- a/docs/themes/navy/source/js/main.js
+++ b/docs/themes/navy/source/js/main.js
@@ -48,20 +48,17 @@
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
const editor = createEditor('editorEl', 'liquid');
const dataEditor = createEditor('dataEl', 'json');
- const preview = createEditor('previewEl', 'html');
- preview.setReadOnly(true);
- preview.renderer.setShowGutter(false);
- preview.renderer.setPadding(16);
+ const previewCode = document.getElementById('previewCode');
- const editors = [editor, dataEditor, preview];
+ const editors = [editor, dataEditor];
colorScheme.addEventListener('change', function() {
editors.forEach(applyEditorTheme);
});
const init = parseArgs(location.hash.slice(1));
if (init) {
- editor.setValue(init.tpl, 1);
- dataEditor.setValue(init.data, 1);
+ editor.setValue(init.tpl, -1);
+ dataEditor.setValue(init.data, -1);
}
editor.on('change', update);
dataEditor.on('change', update);
@@ -76,7 +73,10 @@
const editorsEl = document.querySelector('#editors');
editorsEl.classList.remove('hide');
editorsEl.setAttribute('aria-hide', false);
- editors.forEach(function(ed) { ed.resize(); });
+ editors.forEach(function(ed) {
+ ed.clearSelection();
+ ed.resize();
+ });
}
function getEditorTheme() {
@@ -96,11 +96,15 @@
fontFamily: '"Source Code Pro", ui-monospace, Monaco, Menlo, Consolas, monospace',
fontSize: '14px',
showPrintMargin: false,
+ showGutter: false,
+ highlightActiveLine: false,
+ highlightSelectedWord: false,
tabSize: 2,
useSoftTabs: true,
scrollPastEnd: 0.25
});
editor.getSession().setMode('ace/mode/' + lang);
+ editor.renderer.setShowGutter(false);
editor.renderer.setScrollMargin(8, 8, 0, 0);
return editor;
}
@@ -118,15 +122,23 @@
return utoa(obj.tpl) + ',' + utoa(obj.data);
}
+ function setPreview(value) {
+ previewCode.textContent = value;
+ if (window.Prism) {
+ delete previewCode.dataset.highlighted;
+ window.Prism.highlightElement(previewCode);
+ }
+ }
+
async function update() {
const tpl = editor.getValue();
const data = dataEditor.getValue();
history.replaceState({}, '', '#' + serializeArgs({tpl, data}));
try {
const html = await engine.parseAndRender(tpl, JSON.parse(data));
- preview.setValue(html, 1);
+ setPreview(html);
} catch (err) {
- preview.setValue(err.stack, 1);
+ setPreview(err.stack);
throw err;
}
}
diff --git a/package.json b/package.json
index a99acadc7..948309347 100644
--- a/package.json
+++ b/package.json
@@ -29,12 +29,16 @@
"build:min": "BUNDLES=min rollup -c rollup.config.mjs",
"build:umd": "BUNDLES=umd rollup -c rollup.config.mjs",
"build:charmap": "./bin/character-gen.js > src/util/character.ts",
- "build:docs": "run-s build:docs-liquid build:contributors build:apidoc build:changelog build:docs-hexo",
+ "build:docs": "run-s build:docs-liquid build:contributors build:used-by build:apidoc build:changelog build:docs-hexo",
"build:docs-liquid": "cross-env BUNDLES=min rollup -c rollup.config.mjs && shx cp dist/liquid.browser.min.js docs/themes/navy/source/js/",
"build:contributors": "node bin/build-contributors.js",
+ "build:used-by": "node bin/build-used-by.js",
"build:apidoc": "shx rm -rf docs/source/api && typedoc --plugin typedoc-plugin-missing-exports ./src --gitRevision master --out docs/source/api",
"build:changelog": "node bin/build-changelog.js",
- "build:docs-hexo": "cd docs && npm ci && npm run build && shx cp CNAME public/"
+ "build:docs-hexo": "cd docs && npm ci && npm run build && shx cp CNAME public/",
+ "docs:dev": "run-s build:docs-liquid build:contributors build:used-by docs:serve",
+ "docs:serve": "cd docs && npm run start",
+ "capture:readme-playground": "node bin/capture-readme-playground.mjs"
},
"bin": {
"liquidjs": "./bin/liquid.js",