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]). [![npm version](https://img.shields.io/npm/v/liquidjs.svg?logo=npm&style=flat-square)](https://www.npmjs.org/package/liquidjs) [![npm downloads](https://img.shields.io/npm/dm/liquidjs.svg?style=flat-square)](https://www.npmjs.org/package/liquidjs) @@ -10,6 +10,12 @@ [Documentation][doc] · [Playground](https://liquidjs.com/playground.html) · [Setup guide][setup] · [Contributing][contribution] + + LiquidJS playground: edit a template and context, see live HTML output + + +

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) + + +

+ Eleventy + GitHub Docs + Kibana + Power Pages, Azure API Management developer portal + Shopify CLI, Checkout Blocks + Directus + Builder.io, Mitosis +
+ Pattern Lab + Rock RMS + WISMOlabs + Dropkiq + Freshet +

+ -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 Opensense Inc. Microsoft Sentry - Checkout Blocks + Checkout Blocks Customer IO Syntax Podcast
@@ -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 `${alt}` +} + +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 }} -
-
-
-
-
-
-
-

{{__('index.contributors.title')}}

-

{{__('index.contributors.description')}}

-
-
- {{ partial('partial/all-contributors') }} -
-
-
-
-
-
-
-
-

{{__('index.sponsors.title')}}

-

{{__('index.sponsors.description')}}

-
-
- {{ partial('partial/financial-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 @@ +
+
+
+
+

{{ title }}

+

{{ description | safe }}

+
+
+ {% 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",