├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── browser.ts ├── netlify.toml ├── package.json ├── src ├── h.ts ├── indent.ts ├── index.ts ├── tokenize.ts └── tsconfig.json ├── tests ├── index.spec.ts ├── index.spec.yaml └── tsconfig.json ├── tsconfig.json ├── web ├── example.pug.txt ├── index.css ├── index.html └── index.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | * 2 | !*/ 3 | !*.js 4 | !*.ts 5 | node_modules 6 | dist 7 | umd 8 | lib 9 | .cache -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | mocha: true, 6 | }, 7 | extends: [ 8 | 'standard', 9 | 'plugin:import/errors', 10 | 'plugin:import/warnings', 11 | 'plugin:import/typescript', 12 | 'plugin:json/recommended', 13 | ], 14 | globals: { 15 | Atomics: 'readonly', 16 | SharedArrayBuffer: 'readonly', 17 | }, 18 | parser: '@typescript-eslint/parser', 19 | parserOptions: { 20 | ecmaVersion: 2018, 21 | sourceType: 'module', 22 | }, 23 | plugins: [ 24 | '@typescript-eslint', 25 | ], 26 | rules: { 27 | 'no-unused-vars': 0, 28 | 'no-useless-constructor': 0, 29 | 'no-cond-assign': 0, 30 | 'no-new': 0, 31 | 'arrow-parens': ['error', 'always'], 32 | 'quote-props': ['error', 'as-needed'], 33 | 'comma-dangle': ['error', 'always-multiline'], 34 | semi: 'off', 35 | '@typescript-eslint/semi': ['error', 'never'], 36 | '@typescript-eslint/member-delimiter-style': ['error', { 37 | multiline: { 38 | delimiter: 'none', 39 | }, 40 | }], 41 | 'import/no-unresolved': 0, 42 | 'import/order': [ 43 | 2, 44 | { 45 | groups: [ 46 | 'builtin', 47 | 'external', 48 | 'internal', 49 | ['parent', 'sibling', 'index'], 50 | ], 51 | 'newlines-between': 'always', 52 | }, 53 | ], 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /umd/ 3 | /dist/ 4 | /lib/ 5 | .cache 6 | .netlify -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.pug.txt": "jade" 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pacharapol Withayasakpunt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyperPug 2 | 3 | [![npm version](https://badge.fury.io/js/hyperpug.svg)](https://badge.fury.io/js/hyperpug) [![Website shields.io](https://img.shields.io/website-up-down-green-red/https/hyperpug.netlify.app.svg)](https://hyperpug.netlify.app/) 4 | 5 | Lighter Pug for browser/Electron. With Pug filters' support. 6 | 7 | ## Usage 8 | 9 | ```typescript 10 | import HyperPug from 'hyperpug' 11 | const hp = new HyperPug() 12 | 13 | console.log(hp.parse(HYPERPUG_STRING)) 14 | ``` 15 | 16 | ## Usage with filters 17 | 18 | Filters are normalized for Markdown and other indented languages are well. 19 | 20 | ```typescript 21 | import HyperPug from 'hyperpug' 22 | const hp = new HyperPug({ 23 | markdown: (s) => { 24 | return markdownMaker(s) 25 | } 26 | }) 27 | 28 | console.log(hp.parse(HYPERPUG_STRING)) 29 | ``` 30 | 31 | ## Usage on the browser 32 | 33 | ```html 34 |
35 | 36 | 63 | ``` 64 | -------------------------------------------------------------------------------- /browser.ts: -------------------------------------------------------------------------------- 1 | import HyperPug from './src' 2 | Object.assign(window, { HyperPug }) 3 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "yarn predeploy" 3 | publish = "dist" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperpug", 3 | "version": "1.5.3", 4 | "license": "MIT", 5 | "files": [ 6 | "lib", 7 | "src", 8 | "tsconfig.json" 9 | ], 10 | "description": "Pug for browser, based on Hyperscript", 11 | "main": "lib/index.js", 12 | "unpkg": "lib/index.umd.js", 13 | "types": "lib/index.d.ts", 14 | "author": { 15 | "name": "Pacharapol Withayasakpunt", 16 | "email": "patarapolw@gmail.com", 17 | "url": "https://www.polv.cc" 18 | }, 19 | "repository": "github:patarapolw/hyperpug", 20 | "keywords": [ 21 | "pug", 22 | "hyperpug", 23 | "hyperscript" 24 | ], 25 | "scripts": { 26 | "prebuild": "yarn lint && yarn test", 27 | "build": "tsc -p src/tsconfig.json", 28 | "browserify": "parcel build -d lib -o index.umd.js ./browser.ts", 29 | "test": "ts-mocha --paths -p tests/tsconfig.json tests/**/*.spec.ts", 30 | "web": "parcel web/index.html", 31 | "predeploy": "parcel build web/index.html", 32 | "deploy": "netlify deploy -d dist", 33 | "prepack": "yarn build && yarn browserify", 34 | "lint": "eslint '**'" 35 | }, 36 | "devDependencies": { 37 | "@types/codemirror": "^0.0.84", 38 | "@types/js-yaml": "^3.12.1", 39 | "@types/markdown-it": "^10.0.1", 40 | "@types/mocha": "^5.2.7", 41 | "@types/node": "^13.1.6", 42 | "@types/prismjs": "^1.16.1", 43 | "@typescript-eslint/eslint-plugin": "^2.15.0", 44 | "@typescript-eslint/parser": "^2.15.0", 45 | "eslint": ">=6.2.2", 46 | "eslint-config-standard": "^14.1.0", 47 | "eslint-plugin-import": ">=2.18.0", 48 | "eslint-plugin-json": "^2.0.1", 49 | "eslint-plugin-node": ">=9.1.0", 50 | "eslint-plugin-promise": ">=4.2.1", 51 | "eslint-plugin-standard": ">=4.0.0", 52 | "js-yaml": "^3.13.1", 53 | "mocha": "^6.0.0", 54 | "parcel-bundler": "^1.12.4", 55 | "ts-mocha": "^6.0.0", 56 | "typescript": "^3.9.5" 57 | }, 58 | "resolutions": { 59 | "lodash": "^4.17.19" 60 | }, 61 | "engines": { 62 | "node": "12", 63 | "yarn": "1.x", 64 | "npm": "please-use-yarn" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/h.ts: -------------------------------------------------------------------------------- 1 | export function encodeInnerHTML (s: string) { 2 | const map: Record = { 3 | '<': '<', 4 | '>': '>', 5 | } 6 | return s.split('').map((c) => map[c] || c).join('') 7 | } 8 | 9 | export const h = (name: string, eqdict: string, children: string | string[]) => { 10 | const childrenNodes = typeof children === 'string' ? [encodeInnerHTML(children)] : children 11 | 12 | eqdict = ' ' + eqdict 13 | 14 | const classes: string[] = [] 15 | name = name.replace(/\.[^'"#.]+/g, (p0) => { 16 | classes.push(p0.substring(1)) 17 | return '' 18 | }) 19 | 20 | let classList = '' 21 | eqdict = eqdict.replace(/\sclass=(['"])([^'"]*?)\1/g, (_full, _quote, classList_) => { 22 | classList = classList_ 23 | return '' 24 | }) 25 | 26 | classList = [classList.trim(), ...classes].join(' ').trim() 27 | 28 | if (classList) { 29 | eqdict = `class="${classList}" ${eqdict}` 30 | } 31 | 32 | let id = '' 33 | name = name.replace(/#[^'"#.]+/g, (p0) => { 34 | id = p0.substring(1) 35 | return '' 36 | }) 37 | 38 | if (id) { 39 | eqdict = eqdict.replace(/\sid=(['"])[^'"]*?\1/g, '') 40 | eqdict = `id="${id}" ${eqdict}` 41 | } 42 | 43 | eqdict = eqdict.trim() 44 | 45 | if (!name) { 46 | name = 'div' 47 | } 48 | 49 | return `<${name}${eqdict ? ` ${eqdict}` : ''}>${childrenNodes.join('')}` 50 | } 51 | -------------------------------------------------------------------------------- /src/indent.ts: -------------------------------------------------------------------------------- 1 | export function getIndent (s: string) { 2 | const indents: number[] = [] 3 | for (const r of s.split('\n')) { 4 | if (r.trim()) { 5 | const m = /^ */.exec(r) 6 | if (m) { 7 | indents.push(m[0].length) 8 | } 9 | } 10 | } 11 | 12 | if (indents.length === 0) { 13 | indents.push(0) 14 | } 15 | 16 | return Math.min(...indents) 17 | } 18 | 19 | export function stripIndent (s: string) { 20 | const indent = getIndent(s) 21 | return s.split('\n').map((r) => r.substr(indent)).join('\n') 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { stripIndent } from './indent' 2 | import { tokenize } from './tokenize' 3 | import { h } from './h' 4 | 5 | export type IHyperPugFilter = (s: string) => string 6 | 7 | export interface IHyperPugFilters { 8 | [name: string]: IHyperPugFilter 9 | } 10 | 11 | export default class HyperPug { 12 | private filters: IHyperPugFilters 13 | 14 | constructor (filters: IHyperPugFilters = {}) { 15 | this.filters = filters 16 | } 17 | 18 | public parse (s: string): string { 19 | return this.precompile(s).join('') 20 | } 21 | 22 | private precompile (s: string): string[] { 23 | let key = '' 24 | let childrenRows: string[] = [] 25 | const nodes: string[] = [] 26 | 27 | let isInFilter = false 28 | 29 | for (const r of stripIndent(s).split('\n')) { 30 | if (!r[0] || (r[0] && r[0] !== ' ')) { 31 | isInFilter = false 32 | } 33 | 34 | if (/\S/.test(r[0] || ' ') && !isInFilter) { 35 | if (r[0] === ':') { 36 | isInFilter = true 37 | } 38 | 39 | if (key) { 40 | nodes.push(this.generate(key, childrenRows)) 41 | childrenRows = [] 42 | } 43 | 44 | key = r 45 | continue 46 | } 47 | 48 | childrenRows.push(r) 49 | } 50 | 51 | if (key) { 52 | nodes.push(this.generate(key, childrenRows)) 53 | } 54 | 55 | return nodes 56 | } 57 | 58 | private generate (key: string, childrenRows: string[]) { 59 | const c = childrenRows.join('\n') 60 | const children = c ? this.precompile(c) : undefined 61 | 62 | if (key[0] === ':') { 63 | return this.buildH(key, '', stripIndent(c)) 64 | } 65 | 66 | let attrs: string = '' 67 | 68 | if (key[0] === ':') { 69 | return this.buildH(key, attrs, stripIndent(c)) 70 | } 71 | 72 | const { key: k1, dict, suffix, content } = tokenize(key) 73 | 74 | if (dict) { 75 | attrs = dict 76 | } 77 | 78 | if (suffix === '.') { 79 | return this.buildH(k1, attrs, stripIndent(c)) 80 | } else if (suffix === ':') { 81 | return this.buildH(k1, attrs, this.precompile(content)) 82 | } 83 | 84 | return this.buildH(k1, attrs, content || children || []) 85 | } 86 | 87 | private buildH (key: string, attrs: string, children: string | string[]) { 88 | if (key[0] === ':') { 89 | const filterName = key.substr(1) 90 | const fn = this.filters[filterName] 91 | if (!fn) { 92 | throw new Error(`Filter not installed: ${filterName}`) 93 | } 94 | 95 | if (typeof children !== 'string') { 96 | throw new Error(`Nothing to feed to filter: ${filterName}`) 97 | } 98 | 99 | return h('div', '', [fn(children)]) 100 | } 101 | 102 | try { 103 | return h(key, attrs, children) 104 | } catch (e) { 105 | return h('div', attrs, children) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/tokenize.ts: -------------------------------------------------------------------------------- 1 | export function tokenize (s: string): { 2 | key: string 3 | dict: string 4 | suffix: string 5 | content: string 6 | } { 7 | let key = '' 8 | let dict = '' 9 | let suffix = '' 10 | let content = '' 11 | 12 | let wasInsideBracket = false 13 | let wasExitBracket = false 14 | let wasEndOfKey = false 15 | 16 | const bracketStack: string[] = [] 17 | 18 | for (const c of s.split('')) { 19 | if (c === '(') { 20 | bracketStack.push(c) 21 | wasInsideBracket = true 22 | 23 | continue 24 | } else if (c === ')') { 25 | bracketStack.pop() 26 | if (bracketStack.length === 0) { 27 | wasExitBracket = true 28 | } 29 | 30 | continue 31 | } 32 | 33 | if ([' ', ':'].includes(c)) { 34 | wasEndOfKey = true 35 | } 36 | 37 | if (!wasInsideBracket) { 38 | if (wasEndOfKey) { 39 | content += c 40 | } else { 41 | key += c 42 | } 43 | } else if (!wasExitBracket) { 44 | dict += c 45 | } else { 46 | content += c 47 | } 48 | } 49 | 50 | if ([':', '.'].some((el) => content.startsWith(el))) { 51 | suffix = content[0] 52 | content = content.substr(1) 53 | } 54 | 55 | if ([':', '.'].some((el) => key.endsWith(el))) { 56 | suffix = key[key.length - 1] 57 | key = key.substr(0, key.length - 1) 58 | } 59 | 60 | content = content.trim() 61 | 62 | return { 63 | key, 64 | dict, 65 | suffix, 66 | content, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "../lib" 6 | } 7 | } -------------------------------------------------------------------------------- /tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import assert from 'assert' 3 | 4 | import yaml from 'js-yaml' 5 | import HyperPug from '@/.' 6 | 7 | const testCase = yaml.safeLoad(fs.readFileSync(`${__dirname}/index.spec.yaml`, 'utf8')) 8 | const hp = new HyperPug() 9 | 10 | describe('HyperPug', () => { 11 | testCase.HyperPug.forEach((t: any) => { 12 | it(t.name, () => { 13 | assert.strictEqual(hp.parse(t.input).trim(), t.expected.trim()) 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tests/index.spec.yaml: -------------------------------------------------------------------------------- 1 | HyperPug: 2 | - name: Basic 3 | input: |2 4 | div(class="x") 5 | div hello 6 | div 7 | div goodbye 8 | div good idea 9 | expected:
hello
goodbye
good idea
10 | - name: With extra indentation and non-standard attributes 11 | input: |2 12 | div(:class="x") 13 | div hello 14 | div 15 | div goodbye 16 | div good idea 17 | expected:
hello
goodbye
good idea
18 | - name: With number 19 | input: |2 20 | div(class=1) 21 | div hello 22 | div 23 | div goodbye 24 | div good idea 25 | expected:
hello
goodbye
good idea
26 | - name: With raw string 27 | input: |2 28 | div. 29 | div hello 30 | div 31 | div goodbye 32 | div good idea 33 | expected: "
div hello\ndiv\n div goodbye
good idea
" 34 | - name: One liner 35 | input: ".w-100.mt-3: h3.text-center 天地玄黃,宇宙洪荒。" 36 | expected:

天地玄黃,宇宙洪荒。

37 | - name: Filtered with space in-between 38 | input: |2 39 | details 40 | summary Aforementioned matter 41 | markdown. 42 | Something else 43 | 44 | ```yaml 45 | title: Awesome front matter 46 | isCool: true 47 | numbers: 48 | - a-list: 1 49 | ``` 50 | expected: |2 51 |
Aforementioned matterSomething else 52 | 53 | ```yaml 54 | title: Awesome front matter 55 | isCool: true 56 | numbers: 57 | - a-list: 1 58 | ``` 59 |
60 | - name: Vue template 61 | input: div(v-if="isCool" :role="role" @click="doClick") 62 | expected:
63 | - name: Potentially dangerous innerText 64 | input: | 65 | pre(data-template style="display: none;"). 66 | image: 'https://dev.to/social_previews/article/310429.png' 67 | title: 'Is there a library for better ''s, like social sharing cards?' 68 | description: >- 69 | It seems to need a backend to fetch metadata so as to bypass CORS... I can 70 | try to create my own (in... 71 | expected: | 72 |
image: 'https://dev.to/social_previews/article/310429.png'
73 |       title: 'Is there a library for better <a href>''s, like social sharing cards?'
74 |       description: >-
75 |         It seems to need a backend to fetch metadata so as to bypass CORS...  I can
76 |         try to create my own (in...
77 |       
78 | - name: Entities 79 | input: | 80 | h1 I can't leave home, even if only for a while. 81 | expected: | 82 |

I can't leave home, even if only for a while.

83 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": [ 7 | "../src/*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "../src", 13 | "../tests" 14 | ] 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | "strictNullChecks": true, /* Enable strict null checks. */ 31 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/example.pug.txt: -------------------------------------------------------------------------------- 1 | style. 2 | h1 { 3 | color: blue; 4 | } 5 | 6 | section { 7 | margin-bottom: 1rem; 8 | } 9 | 10 | section .red { 11 | color: red; 12 | } 13 | 14 | .card { 15 | padding: 1em; 16 | border: 1px solid gray; 17 | box-sizing: border-box; 18 | } 19 | 20 | small { 21 | font-size: 0.3rem; 22 | } 23 | 24 | h1 25 | span Made with  26 | a(href="https://github.com/patarapolw/hyperpug", target="_blank", rel="noopener noreferrer") HyperPug 27 | 28 | section 29 | div hello 30 | blockquote 31 | .red goodbye 32 | :markdown 33 | ## This is some heading 34 | 35 | ```pug parsed 36 | .red And you can embed HyperPug inside it. 37 | ``` 38 | .card 39 | .red Outside the container is not red. 40 | 41 | small Yes, this is a good idea. 42 | -------------------------------------------------------------------------------- /web/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | height: 100vh; 8 | width: 100vw; 9 | } 10 | 11 | .github-corner svg { 12 | z-index: 1000; 13 | } 14 | 15 | .view { 16 | flex-grow: 1; 17 | border: 1px solid lightgray; 18 | overflow: scroll; 19 | margin: 0.2rem; 20 | } 21 | 22 | #output { 23 | padding: 1rem; 24 | font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; 25 | } 26 | 27 | .columns { 28 | display: grid; 29 | grid-template-columns: 1fr 1fr; 30 | gap: 1rem; 31 | height: 100%; 32 | width: 100%; 33 | box-sizing: border-box; 34 | } 35 | 36 | @media only screen and (max-width: 1000px) { 37 | .columns { 38 | grid-template-rows: 1fr 1fr; 39 | grid-template-columns: none; 40 | } 41 | } 42 | 43 | table { 44 | border-collapse: collapse; 45 | } 46 | 47 | table, th, td { 48 | border: 1px solid black; 49 | } -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | HyperPug 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /web/index.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | CodeMirror: typeof import('codemirror') 3 | HyperPug: typeof import('../src').default 4 | Prism: typeof import('prismjs') 5 | markdownit: typeof import('markdown-it') 6 | } 7 | 8 | const editorEl = document.getElementById('editor') as HTMLTextAreaElement 9 | const outputEl = document.getElementById('output') as HTMLDivElement 10 | 11 | const editor = window.CodeMirror.fromTextArea(editorEl, { 12 | mode: 'pug', 13 | extraKeys: { 14 | Tab: (cm) => cm.execCommand('indentMore'), 15 | 'Shift-Tab': (cm) => cm.execCommand('indentLess'), 16 | }, 17 | // @ts-ignore 18 | matchBrackets: true, 19 | autoCloseBrackets: true, 20 | lineWrapping: true, 21 | }) 22 | editor.setSize('100%', '100%') 23 | 24 | console.log(window) 25 | 26 | const md = window.markdownit().use((md) => { 27 | const { fence } = md.renderer.rules 28 | 29 | md.renderer.rules.fence = (tokens, idx, options, env, slf) => { 30 | const token = tokens[idx] 31 | const info = (token.info || '').trim() 32 | const content = token.content 33 | 34 | if (info === 'pug parsed') { 35 | return hp.parse(content) 36 | } 37 | 38 | return fence!(tokens, idx, options, env, slf) 39 | } 40 | return md 41 | }) 42 | 43 | const hp = new window.HyperPug({ 44 | markdown: (s) => { 45 | return md.render(s) 46 | }, 47 | }) 48 | 49 | editor.on('change', () => { 50 | const content = hp.parse(editor.getValue()) 51 | outputEl.innerHTML = content 52 | outputEl.querySelectorAll('pre code').forEach((el) => { 53 | window.Prism.highlightElement(el) 54 | }) 55 | }) 56 | 57 | editor.setValue(require('fs').readFileSync(`${__dirname}/example.pug.txt`, 'utf8')) 58 | --------------------------------------------------------------------------------