-

{{__('playground.title')}}

+
+
+

{{__('playground.title')}}

+

{{__('playground.lead')}}

+
+

+
-
-

Template

-
{{ raw('partial/demo.liquid') }}
-
-
-

Context

-
{{ raw('partial/demo.json') }}
-
-
-

Output

-
{{__('playground.loading')}}
+
+
+
+ +

Template

+
+
+
{{ raw('partial/demo.liquid') }}
+
+
+
+
+ +

Context

+
+
+
{{ raw('partial/demo.json') }}
+
+
+
+
+ +

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..9a1ab78e3 100644 --- a/docs/themes/navy/source/css/_partial/index.styl +++ b/docs/themes/navy/source/css/_partial/index.styl @@ -126,7 +126,7 @@ background: var(--color-link-hover) color: #fff -#sponsors-wrap, #contributors-wrap +#used-by-wrap, #sponsors-wrap, #contributors-wrap background: var(--color-navy-lighter) border-top: 1px solid #161d24 border-bottom: 1px solid #161d24 @@ -181,7 +181,7 @@ #contributors-wrap border: none - overflow: hidden; + overflow: hidden .contributors tr diff --git a/docs/themes/navy/source/css/_partial/playground.styl b/docs/themes/navy/source/css/_partial/playground.styl index 953dd1e88..11d9ca349 100644 --- a/docs/themes/navy/source/css/_partial/playground.styl +++ b/docs/themes/navy/source/css/_partial/playground.styl @@ -1,107 +1,312 @@ #playground + --playground-gap: 12px + --playground-radius: 10px + --playground-inset: 16px background: var(--color-content-bg) overflow: hidden box-shadow: var(--panel-shadow) .wrapper - margin-bottom: 40px + margin-bottom: 32px + @media mq-mobile + margin-bottom: 20px + + .playground-hero + display: flex + flex-wrap: wrap + align-items: flex-end + justify-content: space-between + gap: 16px 24px + padding-top: 32px + padding-bottom: 20px + @media mq-mobile + padding-top: 16px + padding-bottom: 12px + gap: 10px + align-items: flex-start + + .playground-hero-text + flex: 1 1 280px + min-width: 0 h1 - font-size: 36px - font-weight: 300 - margin-top: 40px - margin-bottom: 24px - color: var(--color-default) - - h2 - font-size: 0.8125rem + font-size: 28px font-weight: 600 - text-transform: uppercase - letter-spacing: 0.04em - color: var(--color-gray) + letter-spacing: -0.02em + margin: 0 0 8px + color: var(--color-default) + @media mq-mobile + font-size: 22px + margin-bottom: 4px + + .playground-lead margin: 0 + font-size: 15px + line-height: 1.5 + color: var(--color-gray) + @media mq-mobile + font-size: 14px + line-height: 1.45 + + .playground-version + flex: 0 0 auto + margin: 0 + font-size: 12px + line-height: 1.4 + font-family: font-mono + padding: 6px 12px + border-radius: 999px + background: var(--playground-surface) + border: 1px solid var(--color-border) + color: var(--color-gray) + a + color: var(--color-default) + text-decoration: none + font-weight: 500 + &:hover + color: var(--color-link) + text-decoration: none #editors - display: grid - overflow: hidden margin-bottom: 0 - height: 75vh - min-height: 480px + + .playground-workspace + display: grid + gap: var(--playground-gap) grid-template-columns: 1fr 1fr grid-template-rows: 3fr 2fr - grid-gap: 16px align-items: stretch + @media mq-normal + overflow: hidden + height: 75vh + max-height: unquote('calc(100vh - 200px)') + min-height: 520px @media mq-mobile - height: auto - min-height: 0 grid-template-columns: 1fr grid-template-rows: auto - grid-gap: 20px + gap: 12px .area-tpl grid-row: 1 grid-column: 1 min-height: 0 + --pane-dot: var(--color-link) .area-data grid-row: 2 grid-column: 1 min-height: 0 + --pane-dot: var(--highlight-orange) .area-output grid-column: 2 grid-row: 1 / -1 min-height: 0 + min-width: 0 + --pane-dot: var(--highlight-green) @media mq-mobile grid-row: auto grid-column: 1 - .editor-wrapper + .playground-pane 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 - .ace_editor - font-family: font-mono - font-size: 14px - line-height: 1.5 - border-radius: 6px - .ace_scrollbar - z-index: 2 + background: var(--playground-pane-head) + border: 1px solid var(--code-border) + border-radius: var(--playground-radius) + box-shadow: var(--code-shadow) - .version - font-size: 0.8125rem - line-height: 1.5 + .pane-head + display: flex + align-items: center + gap: 10px + flex-shrink: 0 + height: 36px + padding: 0 var(--playground-inset) + border-bottom: 1px solid var(--code-border) + @media mq-mobile + height: 32px + padding: 0 10px + h2 + font-size: 13px + font-weight: 600 + letter-spacing: 0.01em + text-transform: none + color: var(--color-default) + margin: 0 + @media mq-mobile + font-size: 12px + + .pane-indicator + width: 8px + height: 8px + border-radius: 50% + flex-shrink: 0 + background: unquote('color-mix(in srgb, var(--pane-dot) 38%, var(--color-border))') + transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease + + &[data-state="active"] + background: var(--pane-dot) + animation: playground-dot-typing 0.85s ease-in-out infinite + + &[data-state="pending"] + background: var(--highlight-yellow) + + &[data-state="ok"] + background: var(--highlight-green) + animation: playground-dot-ok 0.45s ease-out + + &[data-state="error"] + background: var(--highlight-red) + animation: playground-dot-error 0.35s ease-out + + .area-output .pane-indicator + &[data-state="pending"] + animation: playground-dot-pending 0.55s ease-in-out infinite + + @keyframes playground-dot-typing + 0%, 100% + transform: scale(1) + box-shadow: 0 0 0 0 unquote('color-mix(in srgb, var(--pane-dot) 0%, transparent)') + 50% + transform: scale(1.2) + box-shadow: 0 0 0 4px unquote('color-mix(in srgb, var(--pane-dot) 28%, transparent)') + + @keyframes playground-dot-pending + 0%, 100% + transform: scale(1) + opacity: 0.75 + 50% + transform: scale(1.12) + opacity: 1 + + @keyframes playground-dot-ok + 0% + transform: scale(0.85) + box-shadow: 0 0 0 0 unquote('color-mix(in srgb, var(--highlight-green) 50%, transparent)') + 70% + transform: scale(1.15) + box-shadow: 0 0 0 5px unquote('color-mix(in srgb, var(--highlight-green) 0%, transparent)') + 100% + transform: scale(1) + box-shadow: none + + @keyframes playground-dot-error + 0%, 100% + transform: translateX(0) + 20% + transform: translateX(-2px) + 40% + transform: translateX(2px) + 60% + transform: translateX(-1px) + 80% + transform: translateX(1px) + + .pane-body + flex: 1 1 auto + min-height: 0 + min-width: 0 + display: flex + flex-direction: column + overflow: hidden + background: var(--highlight-background) + + .area-tpl .pane-body, + .area-data .pane-body + padding: var(--playground-inset) + box-sizing: border-box + @media mq-mobile + padding: 12px + + .area-tpl .ace_gutter, + .area-data .ace_gutter + display: none + width: 0 + min-width: 0 + + .area-tpl .ace_editor, + .area-data .ace_editor, + .area-tpl .ace_scroller, + .area-data .ace_scroller, + .area-tpl .ace_content, + .area-data .ace_content, + .area-tpl .ace_text-layer, + .area-data .ace_text-layer + background: transparent + + .editor + flex: 1 1 auto + min-height: 0 + position: relative + overflow: hidden + @media mq-mobile + min-height: 180px + + .output-preview + flex: 1 1 auto + min-height: 0 + min-width: 0 + width: 100% + overflow: auto + @media mq-mobile + min-height: 120px + pre.highlight + margin: 0 + min-height: 100% + width: 100% + box-sizing: border-box + padding: var(--playground-inset) + border: none + box-shadow: none + border-radius: 0 + background: transparent + color: var(--highlight-foreground) + overflow-x: hidden + overflow-y: auto + white-space: pre-wrap + overflow-wrap: break-word + @media mq-mobile + padding: 12px + code + display: block + width: 100% + box-sizing: border-box + font-family: font-mono + font-size: 14px + line-height: 1.55 + color: var(--highlight-foreground) + background: transparent + padding: 0 + white-space: inherit + overflow-wrap: inherit + @media mq-mobile + font-size: 13px + + .ace_editor font-family: font-mono - color: var(--color-gray) - margin-top: 20px - margin-bottom: 32px - a - color: inherit - text-decoration: none - &:hover - color: var(--color-link) - text-decoration: underline + font-size: 14px + line-height: 1.55 + border-radius: 0 + @media mq-mobile + font-size: 13px + .ace_scrollbar + z-index: 2 .hide display: none .loader - width: 48px - height: 48px - margin: 150px auto 200px - border: 3px solid var(--color-border) + width: 40px + height: 40px + margin: 120px auto 160px + border: 2px solid var(--color-border) border-top-color: var(--color-link) border-radius: 50% - animation: spin 0.8s infinite linear + animation: playground-spin 0.7s infinite linear + @media mq-mobile + margin: 60px auto 80px - @keyframes spin + @keyframes playground-spin 100% transform: rotate(360deg) diff --git a/docs/themes/navy/source/css/_variables.styl b/docs/themes/navy/source/css/_variables.styl index cac6b94fe..27363cc6b 100644 --- a/docs/themes/navy/source/css/_variables.styl +++ b/docs/themes/navy/source/css/_variables.styl @@ -37,6 +37,8 @@ vendor-prefixes = webkit moz ms official --highlight-aqua: #0550ae --highlight-blue: #0550ae --highlight-purple: #8250df + --playground-surface: #f3f4f6 + --playground-pane-head: #fff } @media (prefers-color-scheme: dark) { @@ -75,6 +77,8 @@ vendor-prefixes = webkit moz ms official --highlight-aqua: #79c0ff --highlight-blue: #79c0ff --highlight-purple: #d2a8ff + --playground-surface: hsl(218, 26%, 10%) + --playground-pane-head: hsl(218, 22%, 16%) } } diff --git a/docs/themes/navy/source/js/main.js b/docs/themes/navy/source/js/main.js index b113fb27b..1d4e5eb4c 100644 --- a/docs/themes/navy/source/js/main.js +++ b/docs/themes/navy/source/js/main.js @@ -38,8 +38,8 @@ (function() { // playground - /* global liquidjs, ace */ - if (!location.pathname.match(/playground.html$/)) return; + /* global liquidjs, ace, Prism */ + if (!/\/playground(?:\.html)?$/.test(location.pathname)) return; updateVersion(liquidjs.version); const engine = new liquidjs.Liquid({ memoryLimit: 1e5, @@ -48,14 +48,19 @@ 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 indicatorTpl = document.querySelector('.area-tpl .pane-indicator'); + const indicatorData = document.querySelector('.area-data .pane-indicator'); + const indicatorOutput = document.querySelector('.area-output .pane-indicator'); - const editors = [editor, dataEditor, preview]; + const editors = [editor, dataEditor]; + let previewValue = ''; + let hadPreview = false; + let renderTimer = null; + const RENDER_DELAY = 180; colorScheme.addEventListener('change', function() { editors.forEach(applyEditorTheme); + if (previewValue) setPreview(previewValue); }); const init = parseArgs(location.hash.slice(1)); @@ -63,9 +68,11 @@ editor.setValue(init.tpl, 1); dataEditor.setValue(init.data, 1); } - editor.on('change', update); - dataEditor.on('change', update); - update(); + editor.on('change', onTemplateChange); + dataEditor.on('change', onContextChange); + editor.on('focus', function () { setIndicator(indicatorTpl, 'active'); }); + dataEditor.on('focus', function () { setIndicator(indicatorData, 'active'); }); + scheduleUpdate(); ready(); function ready() { @@ -87,6 +94,8 @@ function applyEditorTheme(editor) { editor.setTheme(getEditorTheme()); + editor.renderer.setPadding(0); + editor.container.style.background = 'transparent'; } function createEditor(id, lang) { @@ -96,15 +105,61 @@ fontFamily: '"Source Code Pro", ui-monospace, Monaco, Menlo, Consolas, monospace', fontSize: '14px', showPrintMargin: false, + showGutter: false, + highlightActiveLine: false, tabSize: 2, useSoftTabs: true, - scrollPastEnd: 0.25 + scrollPastEnd: 0 }); editor.getSession().setMode('ace/mode/' + lang); - editor.renderer.setScrollMargin(8, 8, 0, 0); + editor.renderer.setShowGutter(false); + if (editor.renderer.$gutter) { + editor.renderer.$gutter.style.display = 'none'; + } + editor.renderer.setScrollMargin(0, 0, 0, 0); + bindClipboard(editor); return editor; } + function bindClipboard(editor) { + editor.commands.addCommand({ + name: 'copy', + bindKey: {win: 'Ctrl-C', mac: 'Command-C'}, + exec: function (ed) { + const text = ed.getCopyText(); + if (!text) return; + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text); + } + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'cut', + bindKey: {win: 'Ctrl-X', mac: 'Command-X'}, + exec: function (ed) { + const text = ed.getCopyText(); + if (!text) return; + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(function () { + ed.insert(''); + }); + } + } + }); + editor.commands.addCommand({ + name: 'paste', + bindKey: {win: 'Ctrl-V', mac: 'Command-V'}, + exec: function (ed) { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.readText().then(function (text) { + ed.insert(text); + }); + } + } + }); + } + function parseArgs(hash) { if (!hash) return; try { @@ -118,16 +173,64 @@ return utoa(obj.tpl) + ',' + utoa(obj.data); } + function setPreview(value) { + previewValue = value; + previewCode.textContent = value; + if (window.Prism) { + delete previewCode.dataset.highlighted; + window.Prism.highlightElement(previewCode); + } + } + + function setIndicator(indicator, state) { + if (indicator) indicator.dataset.state = state; + } + + function onTemplateChange() { + setIndicator(indicatorTpl, 'active'); + if (indicatorData.dataset.state !== 'error') setIndicator(indicatorData, 'idle'); + setIndicator(indicatorOutput, 'pending'); + scheduleUpdate(); + } + + function onContextChange() { + setIndicator(indicatorData, 'active'); + if (indicatorTpl.dataset.state !== 'error') setIndicator(indicatorTpl, 'idle'); + setIndicator(indicatorOutput, 'pending'); + scheduleUpdate(); + } + + function scheduleUpdate() { + clearTimeout(renderTimer); + renderTimer = setTimeout(update, RENDER_DELAY); + } + async function update() { const tpl = editor.getValue(); const data = dataEditor.getValue(); history.replaceState({}, '', '#' + serializeArgs({tpl, data})); + let parsed; try { - const html = await engine.parseAndRender(tpl, JSON.parse(data)); - preview.setValue(html, 1); + parsed = JSON.parse(data); } catch (err) { - preview.setValue(err.stack, 1); - throw err; + setIndicator(indicatorData, 'error'); + setIndicator(indicatorTpl, 'idle'); + setIndicator(indicatorOutput, 'error'); + return; + } + try { + const html = await engine.parseAndRender(tpl, parsed); + if (html !== '' || !hadPreview) { + setPreview(html); + if (html !== '') hadPreview = true; + } + setIndicator(indicatorTpl, 'idle'); + setIndicator(indicatorData, 'idle'); + setIndicator(indicatorOutput, 'ok'); + } catch (err) { + setIndicator(indicatorTpl, 'error'); + setIndicator(indicatorData, 'idle'); + setIndicator(indicatorOutput, 'error'); } } diff --git a/package.json b/package.json index a99acadc7..97220efd4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "liquidjs", "version": "10.27.0", "sideEffects": false, - "description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.", + "description": "A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support.", "main": "dist/liquid.node.js", "module": "dist/liquid.node.mjs", "es2015": "dist/liquid.browser.mjs", @@ -29,12 +29,15 @@ "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", + "prepare:docs": "run-s build:docs-liquid build:contributors build:apidoc build:changelog", + "build:docs": "run-s prepare:docs 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: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/", + "serve:docs": "cd docs && npm run start", + "dev:docs": "run-s prepare:docs serve:docs" }, "bin": { "liquidjs": "./bin/liquid.js",