From ff52a00ac4f7559b8c94f8b5b8ba7cfc5c350f04 Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Mon, 22 Jun 2026 19:54:46 +0800 Subject: [PATCH] docs(readme): playground GIF, used-by grid, and docs homepage sync Add data/used-by.json with build:used-by for README and docs homepage, playground demo capture, and shared home-section layout. Used by lists products with site logos; Financial Support keeps org and individual sponsors. Co-authored-by: Cursor --- .gitignore | 1 + README.md | 64 ++-- bin/build-used-by.js | 115 ++++++ bin/capture-readme-playground.mjs | 331 ++++++++++++++++++ data/used-by.json | 27 ++ docs/package.json | 1 + docs/source/playground-demo.gif | Bin 0 -> 33612 bytes docs/themes/navy/languages/en.yml | 5 +- docs/themes/navy/layout/index.swig | 79 ++--- .../navy/layout/partial/after_footer.swig | 2 + docs/themes/navy/layout/partial/demo.json | 6 +- docs/themes/navy/layout/partial/demo.liquid | 10 +- .../navy/layout/partial/home-section.swig | 19 + docs/themes/navy/layout/playground.swig | 20 +- .../navy/source/css/_partial/index.styl | 29 +- .../navy/source/css/_partial/playground.styl | 93 +++-- docs/themes/navy/source/css/_variables.styl | 17 + docs/themes/navy/source/js/main.js | 32 +- package.json | 8 +- 19 files changed, 719 insertions(+), 140 deletions(-) create mode 100644 bin/build-used-by.js create mode 100644 bin/capture-readme-playground.mjs create mode 100644 data/used-by.json create mode 100644 docs/source/playground-demo.gif create mode 100644 docs/themes/navy/layout/partial/home-section.swig 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 0000000000000000000000000000000000000000..bd1fd8d259b415ba2018ad1ae06159d4c7fbdfd4 GIT binary patch literal 33612 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 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",