diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..006bc2f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +end_of_line = lf diff --git a/.eleventy.js b/.eleventy.js new file mode 100644 index 0000000..2db345c --- /dev/null +++ b/.eleventy.js @@ -0,0 +1,22 @@ +const { EleventyPluginCodeDemo } = require('./src'); + +/** + * @param {import('@11ty/eleventy/src/UserConfig')} eleventyConfig + */ +module.exports = (eleventyConfig) => { + eleventyConfig.addPlugin(EleventyPluginCodeDemo, { + renderHead: ({ css }) => ``, + renderBody: ({ html, js }) => `${html}`, + iframeAttributes: { + height: '100', + }, + }); + + return { + dir: { + input: 'demo', + output: '_site', + }, + markdownTemplateEngine: 'njk', + }; +}; diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..a638b31 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:prettier/recommended" + ], + "plugins": [ + "prettier" + ], + "rules": { + "eqeqeq": "error", + "no-console": "warn", + "prettier/prettier": "error" + }, + "parserOptions": { + "sourceType": "module", + "ecmaVersion": "latest" + }, + "env": { + "browser": true, + "node": true, + "es6": true, + "jest": true + }, + "ignorePatterns": [ + "node_modules", + "build", + "dist", + "public" + ] +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..53c21a0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# https://www.aleksandrhovhannisyan.com/blog/crlf-vs-lf-normalizing-line-endings-in-git/ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a799fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +_site/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8642af9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "useTabs": false, + "tabWidth": 2, + "singleQuote": true, + "printWidth": 120, + "trailingComma": "es5" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..be7f13f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "eslint.validate": ["javascript"], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a50402d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Aleksandr Hovhannisyan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f2f712 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# eleventy-plugin-code-demo + +## Getting Started + +Install the package: + +``` +yarn add eleventy-plugin-code-demo +``` + +Register it as a plugin in your Eleventy config: + +```js +const { EleventyPluginCodeDemo } = require('eleventy-plugin-code-demo'); + +eleventyConfig.addPlugin(EleventyPluginCodeDemo, { + // Render whatever content you want to go in the
+ renderHead: ({ css }) => ``, + // Render whatever content you want to go in the + renderBody: ({ html, js }) => `${html}`, + // key-value pairs for HTML attributes; these are applied to all code previews + props: { + height: '300', + style: 'width: 100%;', + frameborder: '0', + }, +}); +``` + +### Example Usage + +The shortcode will render as an interactive iframe powered by the fenced code blocks that you define in its body: + +````md +{% codeDemo 'My iframe title' %} +```html + +``` +```css +button { + padding: 8px; +} +``` +```js +const button = document.querySelector('button'); +button.addEventListener('click', () => { + alert('hello, 11ty!'); +}); +``` +{% endcodeDemo %} +```` + +You could also define the code separately and interpolate it (example in Liquid): + +````md +{% capture html %} +```html + +``` +{% endcapture %} + +{% capture css %} +```css +button { + padding: 8px; +} +``` +{% endcapture %} + +{% capture js %} +```js +const button = document.querySelector('button'); +button.addEventListener('click', () => { + alert('hello, 11ty!'); +}); +``` +{% endcapture %} + +{% codeDemo 'Title' %} +{{ html }} +{{ css }} +{{ js }} +{% endcodeDemo %} +```` + +Note that the order does not matter. Also, all children are optional. + +## Use Cases and Motivation + +On my site, I wanted to be able to create isolated, independent code demos containing only the markup, styling, and interactivity that I decided to give them. I could use jsFiddle or Codepen, but those services may not be around forever, and they also typically slow down your page load speed with JavaScript. + +I could create blank pages on my site and embed them as iframes, but that feels like overkill. Plus, I wanted to be able to show my users code snippets while keeping my demos in sync with the code. Stephanie Eckles has written about [how to add static code demos to an 11ty site](https://11ty.rocks/posts/eleventy-templating-static-code-demos/), but I wanted to leverage iframes to: + +1. Avoid having to wrangle with CSS specificity, and +2. Be able to write custom JavaScript isolated from the rest of the page. + +## How It Works + +This plugin was inspired by Maciej Mionskowski's idea in the following article: [Building HTML, CSS, and JS code preview using iframe's srcdoc attribute](https://mionskowski.pl/posts/iframe-code-preview/). In short, iframes allow us to define their markup using the `srcdoc` HTML attribute, which basically contains all of the markup for the page. + +This plugin allows you to define your code snippets in a familiar way, complete with syntax highlighting, and then compresses all of your code into one long `srcdoc` string embedded in a lightweight iframe. diff --git a/demo/index.md b/demo/index.md new file mode 100644 index 0000000..55076e6 --- /dev/null +++ b/demo/index.md @@ -0,0 +1,53 @@ +--- +permalink: / +--- + +Below is an interactive code demo: + +{% codeDemo 'Code demo', height="400" %} +```html + + +``` + +```css +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} +html, +body { + height: 100%; +} +body { + display: grid; + place-content: center; + text-align: center; +} +.buttons { + display: flex; + gap: 4px; + margin-bottom: 8px; +} +button { + padding: 4px; + line-height: 1; +} +``` + +```js +const buttons = document.querySelectorAll('[data-step]'); +const output = document.querySelector('output'); +let count = Number(output.innerHTML); +buttons.forEach((button) => { + button.addEventListener('click', () => { + count += Number(button.getAttribute('data-step')); + output.innerHTML = count; + }); +});; +``` +{% endcodeDemo %} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e53505 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "eleventy-plugin-code-demo", + "version": "0.0.0", + "description": "Render interactive code demos in your Eleventy site with iframes", + "keywords": [ + "eleventy", + "eleventy-plugin", + "iframe" + ], + "main": "src/index.js", + "files": [ + "src/index.js", + "src/typedefs.js", + "README.md", + "package.json" + ], + "repository": { + "type": "git", + "url": "https://github.com/AleksandrHovhannisyan/eleventy-plugin-code-demo.git" + }, + "homepage": "https://github.com/AleksandrHovhannisyan/eleventy-plugin-code-demo.git", + "author": { + "name": "Aleksandr Hovhannisyan", + "url": "https://www.aleksandrhovhannisyan.com" + }, + "license": "MIT", + "scripts": { + "dev": "ELEVENTY_ENV=development npx @11ty/eleventy --serve --incremental" + }, + "devDependencies": { + "@11ty/eleventy": "^1.0.2", + "eslint": "^8.26.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "husky": "4.3.0", + "lint-staged": "12.1.7", + "prettier": "^2.7.1" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged", + "post-checkout": "yarn" + } + }, + "lint-staged": { + "*.js": "yarn run lint:js:fix" + }, + "dependencies": { + "@minify-html/node": "^0.10.3", + "lodash.escape": "^4.0.1", + "markdown-it": "^13.0.1", + "outdent": "^0.8.0" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b75ac00 --- /dev/null +++ b/src/index.js @@ -0,0 +1,61 @@ +const escape = require('lodash/escape'); +const minifyHtml = require('@minify-html/node'); +const markdownIt = require('markdown-it'); +const outdent = require('outdent'); +const { parseCode, stringifyAttributes } = require('./utils'); + +const SHORTCODE_NAME = 'codeDemo'; + +/** + * Higher-order function that takes user configuration options and returns the plugin shortcode. + * @param {import('./typedefs').EleventyPluginCodeDemoOptions} options + */ +const makeCodeDemoShortcode = (options) => { + /** + * @param {string} source The children of this shortcode, as Markdown code blocks. + * @param {string} title The title to set on the iframe. + * @param {Record