-
{{__('playground.title')}}
+
-
-
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",