├── .circleci └── config.yml ├── .github ├── CONTRIBUTING.md ├── dependabot.yml └── workflows │ └── github-release.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .stylelintrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── browser.d.ts ├── browser.js ├── eslint.config.mjs ├── jest.config.mjs ├── marp.config.mjs ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── sandbox └── README.md ├── scripts ├── browser.js └── postcss-optimize-default-theme.mjs ├── src ├── auto-scaling │ ├── code-block.ts │ ├── fitting-header.ts │ ├── index.ts │ └── utils.ts ├── browser.ts ├── custom-elements │ ├── browser │ │ ├── index.ts │ │ ├── marp-auto-scaling.ts │ │ └── marp-custom-element.ts │ ├── definitions.ts │ ├── index.ts │ └── postcss-plugin.ts ├── emoji │ ├── emoji.ts │ └── twemoji.scss ├── highlightjs.ts ├── html │ ├── allowlist.ts │ └── html.ts ├── marp.ts ├── math │ ├── context.ts │ ├── katex.scss │ ├── katex.ts │ ├── math.ts │ ├── mathjax.scss │ └── mathjax.ts ├── observer.ts ├── script │ ├── browser-script.ts │ └── script.ts ├── size │ └── size.ts ├── slug │ └── slug.ts └── typings.d.ts ├── test ├── __snapshots__ │ └── marp.ts.snap ├── _transformers │ └── sass.js ├── browser.ts ├── custom-elements │ └── browser.ts ├── marp.ts ├── math │ ├── __snapshots__ │ │ └── katex.ts.snap │ └── katex.ts └── size │ └── size.ts ├── themes ├── README.md ├── assets │ └── uncover-quote.svg ├── default.scss ├── example.md ├── gaia.scss └── uncover.scss └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | codecov: codecov/codecov@5.0.3 5 | 6 | executors: 7 | node: 8 | parameters: 9 | version: 10 | type: string 11 | default: lts 12 | docker: 13 | - image: cimg/node:<< parameters.version >> 14 | working_directory: ~/marp-core 15 | 16 | commands: 17 | install: 18 | parameters: 19 | force: 20 | type: boolean 21 | default: false 22 | postinstall: 23 | type: steps 24 | default: [] 25 | steps: 26 | - restore_cache: 27 | keys: 28 | - v3-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package-lock.json" }}-{{ .Branch }} 29 | - v3-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package-lock.json" }}- 30 | - v3-dependencies-{{ .Environment.CIRCLE_JOB }}- 31 | 32 | - run: npm ci <<# parameters.force >>--force<> 33 | - steps: << parameters.postinstall >> 34 | 35 | - save_cache: 36 | key: v3-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package-lock.json" }}-{{ .Branch }} 37 | paths: 38 | - ~/.npm 39 | 40 | audit: 41 | steps: 42 | - checkout 43 | - install: 44 | postinstall: 45 | - run: npm -s run check:audit 46 | 47 | prepare: 48 | parameters: 49 | force: 50 | type: boolean 51 | default: false 52 | steps: 53 | - run: node --version 54 | 55 | - checkout 56 | - install: 57 | force: << parameters.force >> 58 | 59 | lint: 60 | steps: 61 | - run: 62 | name: Prettier formatting 63 | command: npm run check:format 64 | 65 | - run: 66 | name: TypeScript type checking 67 | command: npm run check:ts 68 | 69 | - run: 70 | name: ESLint 71 | command: npm run lint:js 72 | 73 | - run: 74 | name: stylelint 75 | command: npm run lint:css 76 | 77 | test: 78 | steps: 79 | - run: 80 | name: Jest 81 | command: npm run test:coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit 82 | environment: 83 | JEST_JUNIT_OUTPUT_DIR: tmp/test-results 84 | 85 | - codecov/upload 86 | 87 | - store_test_results: 88 | path: tmp/test-results 89 | 90 | - store_artifacts: 91 | path: ./coverage 92 | destination: coverage 93 | 94 | jobs: 95 | audit: 96 | executor: node 97 | steps: 98 | - audit 99 | 100 | test-node18: 101 | executor: 102 | name: node 103 | version: '18.20' 104 | steps: 105 | - prepare: 106 | force: true 107 | - test 108 | 109 | test-node20: 110 | executor: 111 | name: node 112 | version: '20.19' 113 | steps: 114 | - prepare 115 | - lint 116 | - test 117 | 118 | test-node22: 119 | executor: 120 | name: node 121 | version: '22.15.0' # Specify LTS version for development 122 | steps: 123 | - prepare 124 | - lint 125 | - test 126 | 127 | test-node24: 128 | executor: 129 | name: node 130 | version: '24.0' 131 | steps: 132 | - prepare 133 | - lint 134 | - test 135 | 136 | workflows: 137 | test: 138 | jobs: 139 | - audit 140 | - test-node18: 141 | requires: 142 | - audit 143 | - test-node20: 144 | requires: 145 | - audit 146 | - test-node22: 147 | requires: 148 | - audit 149 | - test-node24: 150 | requires: 151 | - audit 152 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Marp core 2 | 3 | Thank you for taking the time to read how to contribute to Marp core! This is the guideline for contributing to Marp core. 4 | 5 | But this document hardly has contents! We are following [**the contributing guideline of Marp team projects**](https://github.com/marp-team/.github/blob/master/CONTRIBUTING.md). _You have to read this before starting work._ 6 | 7 | ## Development 8 | 9 | ```bash 10 | # Build 11 | npm run build 12 | 13 | # Watch 14 | npm run watch 15 | 16 | # Watch with sandbox 17 | npm run sandbox 18 | 19 | # Output type definitions 20 | npm run types 21 | ``` 22 | 23 | ### Official theme 24 | 25 | Marp core has some built-in official themes in `themes` folder. They should load when Marp class is initialized. 26 | 27 | #### Requirements 28 | 29 | - All of built-in theme have to support `invert` class. It provides an inverted color scheme from default color. Please also see [yhatt/marp#77](https://github.com/yhatt/marp/issues/77). 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: npm 5 | directory: '/' 6 | schedule: 7 | interval: daily 8 | allow: 9 | - dependency-name: '@marp-team/*' 10 | versioning-strategy: increase 11 | 12 | - package-ecosystem: github-actions 13 | directory: '/' 14 | schedule: 15 | interval: weekly 16 | # versioning-strategy: increase-if-necessary 17 | open-pull-requests-limit: 0 # Dependabot does not allow relaxed versioning :( 18 | -------------------------------------------------------------------------------- /.github/workflows/github-release.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | github-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: marp-team/actions@v1 14 | with: 15 | task: release 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | /sandbox/** 3 | !/sandbox/README.md 4 | tmp/ 5 | types/ 6 | 7 | ########## gitignore.io ########## 8 | # Created by https://www.gitignore.io/api/node,windows,macos,linux,sublimetext,emacs,vim,visualstudiocode 9 | 10 | ### Emacs ### 11 | # -*- mode: gitignore; -*- 12 | *~ 13 | \#*\# 14 | /.emacs.desktop 15 | /.emacs.desktop.lock 16 | *.elc 17 | auto-save-list 18 | tramp 19 | .\#* 20 | 21 | # Org-mode 22 | .org-id-locations 23 | *_archive 24 | 25 | # flymake-mode 26 | *_flymake.* 27 | 28 | # eshell files 29 | /eshell/history 30 | /eshell/lastdir 31 | 32 | # elpa packages 33 | /elpa/ 34 | 35 | # reftex files 36 | *.rel 37 | 38 | # AUCTeX auto folder 39 | /auto/ 40 | 41 | # cask packages 42 | .cask/ 43 | dist/ 44 | 45 | # Flycheck 46 | flycheck_*.el 47 | 48 | # server auth directory 49 | /server/ 50 | 51 | # projectiles files 52 | .projectile 53 | 54 | # directory configuration 55 | .dir-locals.el 56 | 57 | ### Linux ### 58 | 59 | # temporary files which can be created if a process still has a handle open of a deleted file 60 | .fuse_hidden* 61 | 62 | # KDE directory preferences 63 | .directory 64 | 65 | # Linux trash folder which might appear on any partition or disk 66 | .Trash-* 67 | 68 | # .nfs files are created when an open file is removed but is still being accessed 69 | .nfs* 70 | 71 | ### macOS ### 72 | *.DS_Store 73 | .AppleDouble 74 | .LSOverride 75 | 76 | # Icon must end with two \r 77 | Icon 78 | 79 | # Thumbnails 80 | ._* 81 | 82 | # Files that might appear in the root of a volume 83 | .DocumentRevisions-V100 84 | .fseventsd 85 | .Spotlight-V100 86 | .TemporaryItems 87 | .Trashes 88 | .VolumeIcon.icns 89 | .com.apple.timemachine.donotpresent 90 | 91 | # Directories potentially created on remote AFP share 92 | .AppleDB 93 | .AppleDesktop 94 | Network Trash Folder 95 | Temporary Items 96 | .apdisk 97 | 98 | ### Node ### 99 | # Logs 100 | logs 101 | *.log 102 | npm-debug.log* 103 | yarn-debug.log* 104 | yarn-error.log* 105 | 106 | # Runtime data 107 | pids 108 | *.pid 109 | *.seed 110 | *.pid.lock 111 | 112 | # Directory for instrumented libs generated by jscoverage/JSCover 113 | lib-cov 114 | 115 | # Coverage directory used by tools like istanbul 116 | coverage 117 | 118 | # nyc test coverage 119 | .nyc_output 120 | 121 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 122 | .grunt 123 | 124 | # Bower dependency directory (https://bower.io/) 125 | bower_components 126 | 127 | # node-waf configuration 128 | .lock-wscript 129 | 130 | # Compiled binary addons (http://nodejs.org/api/addons.html) 131 | build/Release 132 | 133 | # Dependency directories 134 | node_modules/ 135 | jspm_packages/ 136 | 137 | # Typescript v1 declaration files 138 | typings/ 139 | 140 | # Optional npm cache directory 141 | .npm 142 | 143 | # Optional eslint cache 144 | .eslintcache 145 | 146 | # Optional REPL history 147 | .node_repl_history 148 | 149 | # Output of 'npm pack' 150 | *.tgz 151 | 152 | # Yarn Integrity file 153 | .yarn-integrity 154 | 155 | # dotenv environment variables file 156 | .env 157 | 158 | 159 | ### SublimeText ### 160 | # cache files for sublime text 161 | *.tmlanguage.cache 162 | *.tmPreferences.cache 163 | *.stTheme.cache 164 | 165 | # workspace files are user-specific 166 | *.sublime-workspace 167 | 168 | # project files should be checked into the repository, unless a significant 169 | # proportion of contributors will probably not be using SublimeText 170 | # *.sublime-project 171 | 172 | # sftp configuration file 173 | sftp-config.json 174 | 175 | # Package control specific files 176 | Package Control.last-run 177 | Package Control.ca-list 178 | Package Control.ca-bundle 179 | Package Control.system-ca-bundle 180 | Package Control.cache/ 181 | Package Control.ca-certs/ 182 | Package Control.merged-ca-bundle 183 | Package Control.user-ca-bundle 184 | oscrypto-ca-bundle.crt 185 | bh_unicode_properties.cache 186 | 187 | # Sublime-github package stores a github token in this file 188 | # https://packagecontrol.io/packages/sublime-github 189 | GitHub.sublime-settings 190 | 191 | ### Vim ### 192 | # swap 193 | [._]*.s[a-v][a-z] 194 | [._]*.sw[a-p] 195 | [._]s[a-v][a-z] 196 | [._]sw[a-p] 197 | # session 198 | Session.vim 199 | # temporary 200 | .netrwhist 201 | # auto-generated tag files 202 | tags 203 | 204 | ### VisualStudioCode ### 205 | .vscode/* 206 | 207 | ### Windows ### 208 | # Windows thumbnail cache files 209 | Thumbs.db 210 | ehthumbs.db 211 | ehthumbs_vista.db 212 | 213 | # Folder config file 214 | Desktop.ini 215 | 216 | # Recycle Bin used on file shares 217 | $RECYCLE.BIN/ 218 | 219 | # Windows Installer files 220 | *.cab 221 | *.msi 222 | *.msm 223 | *.msp 224 | 225 | # Windows shortcuts 226 | *.lnk 227 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.15.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .vscode/ 3 | coverage/ 4 | lib/ 5 | sandbox/ 6 | types/ 7 | tmp/ 8 | node_modules 9 | package.json 10 | -------------------------------------------------------------------------------- /.stylelintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - stylelint-config-standard-scss 3 | 4 | rules: 5 | at-rule-no-unknown: 6 | - null 7 | color-function-notation: 8 | - null 9 | scss/at-rule-no-unknown: 10 | - true 11 | value-keyword-case: 12 | - lower 13 | - ignoreProperties: 14 | - /^--/ 15 | no-invalid-position-at-import-rule: 16 | - null 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Marp team (marp-team@marp.app) 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 | # @marp-team/marp-core 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/github/marp-team/marp-core/main.svg?style=flat-square&logo=circleci)](https://circleci.com/gh/marp-team/marp-core/) 4 | [![Codecov](https://img.shields.io/codecov/c/github/marp-team/marp-core/main.svg?style=flat-square&logo=codecov)](https://codecov.io/gh/marp-team/marp-core) 5 | [![npm](https://img.shields.io/npm/v/@marp-team/marp-core.svg?style=flat-square&logo=npm)](https://www.npmjs.com/package/@marp-team/marp-core) 6 | [![LICENSE](https://img.shields.io/github/license/marp-team/marp-core.svg?style=flat-square)](./LICENSE) 7 | 8 | **The core of [Marp](https://github.com/marp-team/marp) converter.** 9 | 10 | In order to use on Marp tools, we have extended from the slide deck framework **[Marpit](https://github.com/marp-team/marpit)**. You can use the practical Markdown syntax, advanced features, and official themes. 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm install --save @marp-team/marp-core 16 | ``` 17 | 18 | ## Usage 19 | 20 | We provide `Marp` class, that is inherited from [Marpit](https://github.com/marp-team/marpit). 21 | 22 | ```javascript 23 | import { Marp } from '@marp-team/marp-core' 24 | 25 | // Convert Markdown slide deck into HTML and CSS 26 | const marp = new Marp() 27 | const { html, css } = marp.render('# Hello, marp-core!') 28 | ``` 29 | 30 | ## Features 31 | 32 | _We will only explain features extended in marp-core._ Please refer to [Marpit framework](https://marpit.marp.app) if you want to know the basic features. 33 | 34 | --- 35 | 36 | ### Marp Markdown 37 | 38 | **Marp Markdown** is a custom Markdown flavor based on [Marpit](https://marpit.marp.app) and [CommonMark](https://commonmark.org/). Following are principle differences from the original: 39 | 40 | - **Marpit** 41 | - Enabled [inline SVG slide](https://marpit.marp.app/inline-svg), [CSS container query support and loose YAML parsing](https://marpit-api.marp.app/marpit#Marpit) by default. 42 | 43 | * **CommonMark** 44 | - For making secure, using some insecure HTML elements and attributes are denied by default. 45 | - Support [table](https://github.github.com/gfm/#tables-extension-) and [strikethrough](https://github.github.com/gfm/#strikethrough-extension-) syntax, based on [GitHub Flavored Markdown](https://github.github.com/gfm/). 46 | - Line breaks in paragraph will convert to `
` tag. 47 | - Slugification for headings (assigning auto-generated `id` attribute for `

` - `

`) is enabled by default. 48 | 49 | --- 50 | 51 | ### [Built-in official themes][themes] 52 | 53 | We provide bulit-in official themes for Marp. See more details in [themes]. 54 | 55 | | Default | Gaia | Uncover | 56 | | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | 57 | | [![](https://bit.ly/2Op7Bp6)][themes] | [![](https://bit.ly/2QhDq4S)][themes] | [![](https://bit.ly/2DqZvvh)][themes] | 58 | | `` | `` | `` | 59 | 60 | [themes]: ./themes/ 61 | 62 | --- 63 | 64 | ### `size` global directive 65 | 66 | Do you want a traditional 4:3 slide size? Marp Core adds the support of `size` global directive. The extended theming system can switch the slide size easier. 67 | 68 | ```markdown 69 | --- 70 | theme: gaia 71 | size: 4:3 72 | --- 73 | 74 | # A traditional 4:3 slide 75 | ``` 76 | 77 | [Bulit-in themes for Marp][themes] have provided `16:9` (1280x720) and `4:3` (960x720) preset sizes. 78 | 79 | #### Define size presets in custom theme CSS 80 | 81 | If you want to use more size presets in your own theme, you have to define `@size` metadata(s) in theme CSS. [Learn in the document of theme metadata for Marp Core][metadata]. 82 | 83 | Theme author does not have to worry an unintended design being used with unexpected slide size because user only can use pre-defined presets by author. 84 | 85 | [metadata]: ./themes#metadata-for-additional-features 86 | 87 | --- 88 | 89 | ### Emoji support 90 | 91 | Emoji shortcode (like `:smile:`) and Unicode emoji 😄 will convert into the SVG vector image provided by [twemoji](https://github.com/jdecked/twemoji) 😄. It could render emoji with high resolution. 92 | 93 | --- 94 | 95 | ### Math typesetting 96 | 97 | We have [Pandoc's Markdown style](https://pandoc.org/MANUAL.html#math) math typesetting support. Surround your formula by `$...$` to render math as inline, and `$$...$$` to render as block. 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 123 | 128 | 129 |
MarkdownRendered slide
109 | 110 | ```tex 111 | Render inline math such as $ax^2+bc+c$. 112 | 113 | $$ I_{xx}=\int\int_Ry^2f(x,y)\cdot{}dydx $$ 114 | 115 | $$ 116 | f(x) = \int_{-\infty}^\infty 117 | \hat f(\xi)\,e^{2 \pi i \xi x} 118 | \,d\xi 119 | $$ 120 | ``` 121 | 122 | 124 | 125 | ![Math typesetting support](https://user-images.githubusercontent.com/3993388/142782335-15bce585-68f1-4c89-8747-8d11533f3ca6.png) 126 | 127 |
130 | 131 | You can choose using library for math from [MathJax](https://www.mathjax.org/) and [KaTeX](https://khan.github.io/KaTeX/) in [`math` global directive](#math-global-directive) (or [JS constructor option](#math-constructor-option)). By default, we prefer MathJax for better rendering and syntax support, but KaTeX is faster rendering if you had a lot of formulas. 132 | 133 | #### `math` global directive 134 | 135 | Through `math` global directive, Marp Core is supporting to declare math library that will be used within current Markdown. 136 | 137 | Set **`mathjax`** or **`katex`** in the `math` global directive like this: 138 | 139 | ```markdown 140 | --- 141 | # Declare to use KaTeX in this Markdown 142 | math: katex 143 | --- 144 | 145 | $$ 146 | \begin{align} 147 | x &= 1+1 \tag{1} \\ 148 | &= 2 149 | \end{align} 150 | $$ 151 | ``` 152 | 153 | If not declared, Marp Core will use MathJax to render math. But we recommend to declare the library whenever to use math typesetting. 154 | 155 | > [!WARNING] 156 | > The declaration of math library is given priority over [`math` JS constructor option](#math-constructor-option), but you cannot turn on again via `math` global directive if disabled math typesetting by the constructor. 157 | 158 | --- 159 | 160 | ### Auto-scaling features 161 | 162 | Marp Core has some auto-scaling features: 163 | 164 | - [**Fitting header**](#fitting-header): Get bigger heading that fit onto the slide by `# `. 165 | - [**Auto-shrink the code block and KaTeX block**](#auto-shrink-block): Prevent sticking out the block from the right of the slide. 166 | 167 | Auto-scaling is available if defined [`@auto-scaling` metadata][metadata] in an using theme CSS. 168 | 169 | ```css 170 | /* 171 | * @theme foobar 172 | * @auto-scaling true 173 | */ 174 | ``` 175 | 176 | All of [Marp Core's built-in themes][themes] are ready to use full-featured auto scalings. If you're the theme author, you can control target elements which enable auto-scaling [by using metadata keyword(s).][metadata] 177 | 178 | This feature depends to inline SVG, so note that it will not working if disabled [Marpit's `inlineSVG` mode](https://github.com/marp-team/marpit#inline-svg-slide-experimental) by setting `inlineSVG: false` in constructor option. 179 | 180 | > [!WARNING] 181 | > Auto-scaling is designed for horizontal scaling. In vertical, the scaled element still may stick out from bottom of slide if there are a lot of contents around it. 182 | 183 | #### Fitting header 184 | 185 | When the headings contains `` comment, the size of headings will resize to fit onto the slide size. 186 | 187 | ```markdown 188 | # Fitting header 189 | ``` 190 | 191 | This syntax is similar to [Deckset's `[fit]` keyword](https://docs.decksetapp.com/English.lproj/Formatting/01-headings.html), but we use HTML comment to hide a fit keyword on Markdown rendered as document. 192 | 193 | #### Auto-shrink the block 194 | 195 | Some of blocks will be shrunk to fit onto the slide. It is useful preventing stuck out the block from the right of the slide. 196 | 197 | | | Traditional rendering | Auto-scaling | 198 | | :------------------: | :----------------------------------------------: | :-------------------------------------: | 199 | | **Code block** | ![Traditional rendering](https://bit.ly/2LyEnmi) | ![Auto-scaling](https://bit.ly/2N4yWQZ) | 200 | | **KaTeX math block** | ![Traditional rendering](https://bit.ly/2NXoHuW) | ![Auto-scaling](https://bit.ly/2M6LyCk) | 201 | 202 | > [!NOTE] 203 | > MathJax math block will always be scaled without even setting `@auto-scaling` metadata. 204 | 205 | --- 206 | 207 | ## Constructor options 208 | 209 | You can customize a behavior of Marp parser by passing an options object to the constructor. You can also pass together with [Marpit constructor options](https://marpit-api.marp.app/marpit#Marpit). 210 | 211 | > [!NOTE] 212 | > 213 | > [Marpit's `markdown` option](https://marpit-api.marp.app/marpit#Marpit) is accepted only object options because of always using CommonMark. 214 | 215 | ```javascript 216 | const marp = new Marp({ 217 | // marp-core constructor options 218 | html: true, 219 | emoji: { 220 | shortcode: true, 221 | unicode: false, 222 | twemoji: { 223 | base: '/resources/twemoji/', 224 | }, 225 | }, 226 | math: 'katex', 227 | minifyCSS: true, 228 | script: { 229 | source: 'cdn', 230 | nonce: 'xxxxxxxxxxxxxxx', 231 | }, 232 | slug: false, 233 | 234 | // It can be included Marpit constructor options 235 | looseYAML: false, 236 | markdown: { 237 | breaks: false, 238 | }, 239 | }) 240 | ``` 241 | 242 | ### `html`: _`boolean`_ | _`object`_ 243 | 244 | Setting whether to render raw HTML in Markdown. It's an alias to `markdown.html` ([markdown-it option](https://markdown-it.github.io/markdown-it/#MarkdownIt.new)) but has additional feature about HTML allowlist. 245 | 246 | - (default): Use Marp's default allowlist. 247 | - `true`: The all HTML will be allowed. 248 | - `false`: All HTML except supported in Marpit Markdown will be disallowed. 249 | 250 | By passing `object`, you can set the allowlist to specify allowed tags and attributes. 251 | 252 | ```javascript 253 | // Specify tag name as key, and attributes to allow as string array. 254 | { 255 | a: ['href', 'target'], 256 | br: [], 257 | } 258 | ``` 259 | 260 | ```javascript 261 | // You may use custom attribute sanitizer by passing object. 262 | { 263 | img: { 264 | src: (value) => (value.startsWith('https://') ? value : '') 265 | } 266 | } 267 | ``` 268 | 269 | By default, Marp Core allows known HTML elements and attributes that are considered as safe. That is defined as a readonly `html` member in `Marp` class. [See the full default allowlist in the source code.](src/html/allowlist.ts) 270 | 271 | > [!NOTE] 272 | > Whatever any option is selected, `` and ` 50 |
51 | 52 | 53 | 54 |
55 | ` 56 | .split(/\n\s*/) 57 | .join('') 58 | 59 | this.wrapper = 60 | this.shadowRoot.querySelector(`div[${dataWrapper}]`) ?? 61 | undefined 62 | 63 | const previousSvg = this.svg 64 | 65 | this.svg = 66 | this.wrapper?.querySelector(`svg[${dataSvg}]`) ?? undefined 67 | 68 | if (this.svg !== previousSvg) { 69 | this.svgComputedStyle = this.svg 70 | ? window.getComputedStyle(this.svg) 71 | : undefined 72 | } 73 | 74 | this.container = 75 | this.svg?.querySelector(`span[${dataContainer}]`) ?? 76 | undefined 77 | 78 | this.observe() 79 | } 80 | 81 | disconnectedCallback() { 82 | this.svg = undefined 83 | this.svgComputedStyle = undefined 84 | this.wrapper = undefined 85 | this.container = undefined 86 | 87 | this.observe() 88 | } 89 | 90 | attributeChangedCallback() { 91 | this.observe() 92 | } 93 | 94 | // Workaround for Chromium 105+ 95 | private flushSvgDisplay() { 96 | const { svg: connectedSvg } = this 97 | 98 | if (connectedSvg) { 99 | connectedSvg.style.display = 'inline' 100 | 101 | requestAnimationFrame(() => { 102 | connectedSvg.style.display = '' 103 | }) 104 | } 105 | } 106 | 107 | private observe() { 108 | this.containerObserver.disconnect() 109 | this.wrapperObserver.disconnect() 110 | 111 | if (this.wrapper) this.wrapperObserver.observe(this.wrapper) 112 | if (this.container) this.containerObserver.observe(this.container) 113 | 114 | if (this.svgComputedStyle) this.observeSVGStyle(this.svgComputedStyle) 115 | } 116 | 117 | private observeSVGStyle(style: CSSStyleDeclaration) { 118 | const frame = () => { 119 | const newPreserveAspectRatio = (() => { 120 | const custom = style.getPropertyValue('--preserve-aspect-ratio') 121 | if (custom) return custom.trim() 122 | 123 | const xAlign = (({ textAlign, direction }) => { 124 | if (textAlign.endsWith('left')) return 'Min' 125 | if (textAlign.endsWith('right')) return 'Max' 126 | 127 | if (textAlign === 'start' || textAlign === 'end') { 128 | let rAlign = direction === 'rtl' 129 | if (textAlign === 'end') rAlign = !rAlign 130 | 131 | return rAlign ? 'Max' : 'Min' 132 | } 133 | return 'Mid' 134 | })(style) 135 | 136 | return `x${xAlign}YMid meet` 137 | })() 138 | 139 | if (newPreserveAspectRatio !== this.svgPreserveAspectRatio) { 140 | this.svgPreserveAspectRatio = newPreserveAspectRatio 141 | this.updateSVGRect() 142 | } 143 | 144 | if (style === this.svgComputedStyle) requestAnimationFrame(frame) 145 | } 146 | frame() 147 | } 148 | 149 | private updateSVGRect() { 150 | let width = Math.ceil(this.containerSize?.width ?? 0) 151 | const height = Math.ceil(this.containerSize?.height ?? 0) 152 | 153 | if (this.dataset.downscaleOnly !== undefined) { 154 | width = Math.max(width, this.wrapperSize?.width ?? 0) 155 | } 156 | 157 | const foreignObject = this.svg?.querySelector(':scope > foreignObject') 158 | foreignObject?.setAttribute('width', `${width}`) 159 | foreignObject?.setAttribute('height', `${height}`) 160 | 161 | if (this.svg) { 162 | this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`) 163 | this.svg.setAttribute('preserveAspectRatio', this.svgPreserveAspectRatio) 164 | this.svg.style.height = width <= 0 || height <= 0 ? '0' : '' 165 | } 166 | 167 | if (this.container) { 168 | const svgPar = this.svgPreserveAspectRatio.toLowerCase() 169 | 170 | this.container.style.marginLeft = 171 | svgPar.startsWith('xmid') || svgPar.startsWith('xmax') ? 'auto' : '0' 172 | this.container.style.marginRight = svgPar.startsWith('xmi') ? 'auto' : '0' 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/custom-elements/browser/marp-custom-element.ts: -------------------------------------------------------------------------------- 1 | type Constructor = new (...args: any[]) => T 2 | 3 | export const createMarpCustomElement = >( 4 | Base: T, 5 | { attrs = {}, style }: { attrs?: Record; style?: string }, 6 | ) => 7 | class MarpCustomElement extends Base { 8 | declare readonly shadowRoot: ShadowRoot | null 9 | 10 | constructor(...args: any[]) { 11 | super(...args) 12 | 13 | for (const [key, value] of Object.entries(attrs)) { 14 | if (!this.hasAttribute(key)) this.setAttribute(key, value) 15 | } 16 | 17 | this._shadow() 18 | } 19 | 20 | static get observedAttributes() { 21 | return ['data-auto-scaling'] 22 | } 23 | 24 | connectedCallback() { 25 | this._update() 26 | } 27 | 28 | attributeChangedCallback() { 29 | this._update() 30 | } 31 | 32 | _shadow() { 33 | if (!this.shadowRoot) { 34 | try { 35 | this.attachShadow({ mode: 'open' }) 36 | } catch (e) { 37 | if (!(e instanceof Error && e.name === 'NotSupportedError')) throw e 38 | } 39 | } 40 | return this.shadowRoot 41 | } 42 | 43 | _update() { 44 | const shadowRoot = this._shadow() 45 | 46 | if (shadowRoot) { 47 | const styleTag = style ? `` : '' 48 | let slotTag = '' 49 | 50 | const { autoScaling } = this.dataset 51 | 52 | if (autoScaling !== undefined) { 53 | const downscale = 54 | autoScaling === 'downscale-only' ? 'data-downscale-only' : '' 55 | 56 | slotTag = `${slotTag}` 57 | } 58 | 59 | shadowRoot.innerHTML = styleTag + slotTag 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/custom-elements/definitions.ts: -------------------------------------------------------------------------------- 1 | export const elements = { 2 | h1: { 3 | proto: () => HTMLHeadingElement, // Returns function for delay (Node.js does not have DOM values) 4 | attrs: { role: 'heading', 'aria-level': '1' }, 5 | style: 6 | 'display: block; font-size: 2em; margin-block-start: 0.67em; margin-block-end: 0.67em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;', 7 | }, 8 | h2: { 9 | proto: () => HTMLHeadingElement, 10 | attrs: { role: 'heading', 'aria-level': '2' }, 11 | style: 12 | 'display: block; font-size: 1.5em; margin-block-start: 0.83em; margin-block-end: 0.83em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;', 13 | }, 14 | h3: { 15 | proto: () => HTMLHeadingElement, 16 | attrs: { role: 'heading', 'aria-level': '3' }, 17 | style: 18 | 'display: block; font-size: 1.17em; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;', 19 | }, 20 | h4: { 21 | proto: () => HTMLHeadingElement, 22 | attrs: { role: 'heading', 'aria-level': '4' }, 23 | style: 24 | 'display: block; margin-block-start: 1.33em; margin-block-end: 1.33em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;', 25 | }, 26 | h5: { 27 | proto: () => HTMLHeadingElement, 28 | attrs: { role: 'heading', 'aria-level': '5' }, 29 | style: 30 | 'display: block; font-size: 0.83em; margin-block-start: 1.67em; margin-block-end: 1.67em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;', 31 | }, 32 | h6: { 33 | proto: () => HTMLHeadingElement, 34 | attrs: { role: 'heading', 'aria-level': '6' }, 35 | style: 36 | 'display: block; font-size: 0.67em; margin-block-start: 2.33em; margin-block-end: 2.33em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;', 37 | }, 38 | span: { 39 | proto: () => HTMLSpanElement, 40 | }, 41 | 42 | // HTMLPreElement cannot attach shadow DOM by security reason 43 | pre: { 44 | proto: () => HTMLElement, 45 | style: 46 | 'display: block; font-family: monospace; white-space: pre; margin: 1em 0; --marp-auto-scaling-white-space: pre;', 47 | }, 48 | } as const 49 | -------------------------------------------------------------------------------- /src/custom-elements/index.ts: -------------------------------------------------------------------------------- 1 | export { customElementsPostCSSPlugin as css } from './postcss-plugin' 2 | -------------------------------------------------------------------------------- /src/custom-elements/postcss-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Root } from 'postcss' 2 | import postcssSelectorParser, { Container } from 'postcss-selector-parser' 3 | import { elements } from './definitions' 4 | 5 | const findClosest = ( 6 | container: Container | undefined, 7 | finder: (container: Container) => boolean, 8 | ) => { 9 | let current: Container | undefined = container 10 | 11 | while (current) { 12 | if (finder(current)) return current 13 | current = current.parent 14 | } 15 | 16 | return undefined 17 | } 18 | 19 | export const customElementsPostCSSPlugin = (root: Root) => { 20 | const targetElements = Object.keys(elements) 21 | 22 | root.walkRules(new RegExp(targetElements.join('|'), 'i'), (rule) => { 23 | postcssSelectorParser((selectorRoot) => { 24 | selectorRoot.walkTags((tag) => { 25 | const normalizedTagName = tag.value.toLowerCase() 26 | 27 | if (targetElements.includes(normalizedTagName)) { 28 | // Check if there is inside of a valid pseudo element 29 | const closestPseudo = findClosest( 30 | tag.parent, 31 | ({ type }) => type === 'pseudo', 32 | ) 33 | if (closestPseudo?.value === '::part') return 34 | 35 | // Replace 36 | tag.value = `:is(${normalizedTagName}, marp-${normalizedTagName})` 37 | } 38 | }) 39 | }).processSync(rule, { updateSelector: true }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/emoji/emoji.ts: -------------------------------------------------------------------------------- 1 | import marpitPlugin from '@marp-team/marpit/plugin' 2 | import twemoji from '@twemoji/api' 3 | import emojiRegex from 'emoji-regex' 4 | import { full as markdownItEmoji } from 'markdown-it-emoji' 5 | import twemojiCSS from './twemoji.scss' 6 | 7 | export interface EmojiOptions { 8 | shortcode?: boolean | 'twemoji' 9 | twemoji?: TwemojiOptions 10 | unicode?: boolean | 'twemoji' 11 | } 12 | 13 | interface TwemojiOptions { 14 | base?: string 15 | ext?: 'svg' | 'png' 16 | } 17 | 18 | const regexForSplit = new RegExp(`(${emojiRegex().source})(?!\uFE0E)`, 'g') 19 | 20 | export const css = (opts: EmojiOptions) => 21 | opts.shortcode === 'twemoji' || opts.unicode === 'twemoji' 22 | ? twemojiCSS 23 | : undefined 24 | 25 | export const markdown = marpitPlugin((md) => { 26 | const opts: EmojiOptions = md.marpit.options.emoji 27 | const twemojiOpts = opts.twemoji || {} 28 | const twemojiExt = twemojiOpts.ext || 'svg' 29 | 30 | const twemojiParse = (content: string): string => 31 | twemoji.parse(content, { 32 | attributes: () => ({ 'data-marp-twemoji': '' }), 33 | base: twemojiOpts.base || undefined, 34 | ext: `.${twemojiExt}`, 35 | size: twemojiExt === 'svg' ? 'svg' : undefined, 36 | }) 37 | 38 | const twemojiRenderer = (token: any[], idx: number): string => 39 | twemojiParse(token[idx].content) 40 | 41 | if (opts.shortcode) { 42 | // Pick rules to avoid collision with other markdown-it plugin 43 | const picker = { 44 | core: { 45 | ruler: { 46 | push: (_, rule) => (picker.rule = rule), // for markdown-it-emoji <= v2.0.0 47 | after: (_, __, rule) => (picker.rule = rule), // for markdown-it-emoji >= v2.0.1 48 | }, 49 | }, 50 | renderer: { rules: { emoji: () => {} } }, 51 | rule: (() => {}) as (...args: any[]) => void, 52 | utils: md.utils, 53 | } 54 | 55 | markdownItEmoji(picker, { shortcuts: {} }) 56 | 57 | // TODO: use md.core.ruler.after 58 | md.core.ruler.push('marp_emoji', (state) => { 59 | const { Token } = state 60 | 61 | state.Token = function replacedToken(name, ...args) { 62 | return new Token(name === 'emoji' ? 'marp_emoji' : name, ...args) 63 | } 64 | 65 | picker.rule(state) 66 | state.Token = Token 67 | }) 68 | 69 | md.renderer.rules.marp_emoji = 70 | opts.shortcode === 'twemoji' 71 | ? twemojiRenderer 72 | : picker.renderer.rules.emoji 73 | } 74 | 75 | if (opts.unicode) { 76 | md.core.ruler.after('inline', 'marp_unicode_emoji', ({ tokens, Token }) => { 77 | for (const token of tokens) { 78 | if (token.type === 'inline') { 79 | const newChildren: any[] = [] 80 | 81 | for (const t of token.children) { 82 | if (t.type === 'text') { 83 | const splittedByEmoji = t.content.split(regexForSplit) 84 | 85 | newChildren.push( 86 | ...splittedByEmoji.reduce( 87 | (splitedArr, text, idx) => 88 | text.length === 0 89 | ? splitedArr 90 | : [ 91 | ...splitedArr, 92 | Object.assign(new Token(), { 93 | ...t, 94 | content: text, 95 | type: idx % 2 ? 'marp_unicode_emoji' : 'text', 96 | }), 97 | ], 98 | [], 99 | ), 100 | ) 101 | } else { 102 | newChildren.push(t) 103 | } 104 | } 105 | 106 | token.children = newChildren 107 | } 108 | } 109 | }) 110 | 111 | md.renderer.rules.marp_unicode_emoji = ( 112 | token: any[], 113 | idx: number, 114 | ): string => token[idx].content 115 | 116 | const { code_block, code_inline, fence } = md.renderer.rules 117 | 118 | if (opts.unicode === 'twemoji') { 119 | const wrap = (text) => 120 | text 121 | .split(/(<[^>]*>)/g) 122 | .reduce( 123 | (ret, part, idx) => 124 | `${ret}${ 125 | idx % 2 126 | ? part 127 | : part.replace(regexForSplit, ([emoji]) => 128 | twemojiParse(emoji), 129 | ) 130 | }`, 131 | '', 132 | ) 133 | 134 | md.renderer.rules.marp_unicode_emoji = twemojiRenderer 135 | 136 | md.renderer.rules.code_inline = (...args) => wrap(code_inline(...args)) 137 | md.renderer.rules.code_block = (...args) => wrap(code_block(...args)) 138 | md.renderer.rules.fence = (...args) => wrap(fence(...args)) 139 | } 140 | } 141 | }) 142 | -------------------------------------------------------------------------------- /src/emoji/twemoji.scss: -------------------------------------------------------------------------------- 1 | img[data-marp-twemoji] { 2 | background: transparent; 3 | height: 1em; 4 | margin: 0 0.05em 0 0.1em; 5 | vertical-align: -0.1em; 6 | width: 1em; 7 | } 8 | -------------------------------------------------------------------------------- /src/highlightjs.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | import hljsCore from 'highlight.js/lib/core' 3 | 4 | export const generateHighlightJSInstance = () => { 5 | // Create a new instance to avoid polluation to global highlight.js module by users. 6 | const hljs = hljsCore.newInstance() 7 | 8 | // Ported from highlight.js/lib/index.js 9 | hljs.registerLanguage('1c', require('highlight.js/lib/languages/1c')) 10 | hljs.registerLanguage('abnf', require('highlight.js/lib/languages/abnf')) 11 | hljs.registerLanguage( 12 | 'accesslog', 13 | require('highlight.js/lib/languages/accesslog'), 14 | ) 15 | hljs.registerLanguage( 16 | 'actionscript', 17 | require('highlight.js/lib/languages/actionscript'), 18 | ) 19 | hljs.registerLanguage('ada', require('highlight.js/lib/languages/ada')) 20 | hljs.registerLanguage( 21 | 'angelscript', 22 | require('highlight.js/lib/languages/angelscript'), 23 | ) 24 | hljs.registerLanguage('apache', require('highlight.js/lib/languages/apache')) 25 | hljs.registerLanguage( 26 | 'applescript', 27 | require('highlight.js/lib/languages/applescript'), 28 | ) 29 | hljs.registerLanguage('arcade', require('highlight.js/lib/languages/arcade')) 30 | hljs.registerLanguage( 31 | 'arduino', 32 | require('highlight.js/lib/languages/arduino'), 33 | ) 34 | hljs.registerLanguage('armasm', require('highlight.js/lib/languages/armasm')) 35 | hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml')) 36 | hljs.registerLanguage( 37 | 'asciidoc', 38 | require('highlight.js/lib/languages/asciidoc'), 39 | ) 40 | hljs.registerLanguage( 41 | 'aspectj', 42 | require('highlight.js/lib/languages/aspectj'), 43 | ) 44 | hljs.registerLanguage( 45 | 'autohotkey', 46 | require('highlight.js/lib/languages/autohotkey'), 47 | ) 48 | hljs.registerLanguage('autoit', require('highlight.js/lib/languages/autoit')) 49 | hljs.registerLanguage('avrasm', require('highlight.js/lib/languages/avrasm')) 50 | hljs.registerLanguage('awk', require('highlight.js/lib/languages/awk')) 51 | hljs.registerLanguage('axapta', require('highlight.js/lib/languages/axapta')) 52 | hljs.registerLanguage('bash', require('highlight.js/lib/languages/bash')) 53 | hljs.registerLanguage('basic', require('highlight.js/lib/languages/basic')) 54 | hljs.registerLanguage('bnf', require('highlight.js/lib/languages/bnf')) 55 | hljs.registerLanguage( 56 | 'brainfuck', 57 | require('highlight.js/lib/languages/brainfuck'), 58 | ) 59 | hljs.registerLanguage('c', require('highlight.js/lib/languages/c')) 60 | hljs.registerLanguage('cal', require('highlight.js/lib/languages/cal')) 61 | hljs.registerLanguage( 62 | 'capnproto', 63 | require('highlight.js/lib/languages/capnproto'), 64 | ) 65 | hljs.registerLanguage('ceylon', require('highlight.js/lib/languages/ceylon')) 66 | hljs.registerLanguage('clean', require('highlight.js/lib/languages/clean')) 67 | hljs.registerLanguage( 68 | 'clojure', 69 | require('highlight.js/lib/languages/clojure'), 70 | ) 71 | hljs.registerLanguage( 72 | 'clojure-repl', 73 | require('highlight.js/lib/languages/clojure-repl'), 74 | ) 75 | hljs.registerLanguage('cmake', require('highlight.js/lib/languages/cmake')) 76 | hljs.registerLanguage( 77 | 'coffeescript', 78 | require('highlight.js/lib/languages/coffeescript'), 79 | ) 80 | hljs.registerLanguage('coq', require('highlight.js/lib/languages/coq')) 81 | hljs.registerLanguage('cos', require('highlight.js/lib/languages/cos')) 82 | hljs.registerLanguage('cpp', require('highlight.js/lib/languages/cpp')) 83 | hljs.registerLanguage('crmsh', require('highlight.js/lib/languages/crmsh')) 84 | hljs.registerLanguage( 85 | 'crystal', 86 | require('highlight.js/lib/languages/crystal'), 87 | ) 88 | hljs.registerLanguage('csharp', require('highlight.js/lib/languages/csharp')) 89 | hljs.registerLanguage('csp', require('highlight.js/lib/languages/csp')) 90 | hljs.registerLanguage('css', require('highlight.js/lib/languages/css')) 91 | hljs.registerLanguage('d', require('highlight.js/lib/languages/d')) 92 | hljs.registerLanguage( 93 | 'markdown', 94 | require('highlight.js/lib/languages/markdown'), 95 | ) 96 | hljs.registerLanguage('dart', require('highlight.js/lib/languages/dart')) 97 | hljs.registerLanguage('delphi', require('highlight.js/lib/languages/delphi')) 98 | hljs.registerLanguage('diff', require('highlight.js/lib/languages/diff')) 99 | hljs.registerLanguage('django', require('highlight.js/lib/languages/django')) 100 | hljs.registerLanguage('dns', require('highlight.js/lib/languages/dns')) 101 | hljs.registerLanguage( 102 | 'dockerfile', 103 | require('highlight.js/lib/languages/dockerfile'), 104 | ) 105 | hljs.registerLanguage('dos', require('highlight.js/lib/languages/dos')) 106 | hljs.registerLanguage( 107 | 'dsconfig', 108 | require('highlight.js/lib/languages/dsconfig'), 109 | ) 110 | hljs.registerLanguage('dts', require('highlight.js/lib/languages/dts')) 111 | hljs.registerLanguage('dust', require('highlight.js/lib/languages/dust')) 112 | hljs.registerLanguage('ebnf', require('highlight.js/lib/languages/ebnf')) 113 | hljs.registerLanguage('elixir', require('highlight.js/lib/languages/elixir')) 114 | hljs.registerLanguage('elm', require('highlight.js/lib/languages/elm')) 115 | hljs.registerLanguage('ruby', require('highlight.js/lib/languages/ruby')) 116 | hljs.registerLanguage('erb', require('highlight.js/lib/languages/erb')) 117 | hljs.registerLanguage( 118 | 'erlang-repl', 119 | require('highlight.js/lib/languages/erlang-repl'), 120 | ) 121 | hljs.registerLanguage('erlang', require('highlight.js/lib/languages/erlang')) 122 | hljs.registerLanguage('excel', require('highlight.js/lib/languages/excel')) 123 | hljs.registerLanguage('fix', require('highlight.js/lib/languages/fix')) 124 | hljs.registerLanguage('flix', require('highlight.js/lib/languages/flix')) 125 | hljs.registerLanguage( 126 | 'fortran', 127 | require('highlight.js/lib/languages/fortran'), 128 | ) 129 | hljs.registerLanguage('fsharp', require('highlight.js/lib/languages/fsharp')) 130 | hljs.registerLanguage('gams', require('highlight.js/lib/languages/gams')) 131 | hljs.registerLanguage('gauss', require('highlight.js/lib/languages/gauss')) 132 | hljs.registerLanguage('gcode', require('highlight.js/lib/languages/gcode')) 133 | hljs.registerLanguage( 134 | 'gherkin', 135 | require('highlight.js/lib/languages/gherkin'), 136 | ) 137 | hljs.registerLanguage('glsl', require('highlight.js/lib/languages/glsl')) 138 | hljs.registerLanguage('gml', require('highlight.js/lib/languages/gml')) 139 | hljs.registerLanguage('go', require('highlight.js/lib/languages/go')) 140 | hljs.registerLanguage('golo', require('highlight.js/lib/languages/golo')) 141 | hljs.registerLanguage('gradle', require('highlight.js/lib/languages/gradle')) 142 | hljs.registerLanguage( 143 | 'graphql', 144 | require('highlight.js/lib/languages/graphql'), 145 | ) 146 | hljs.registerLanguage('groovy', require('highlight.js/lib/languages/groovy')) 147 | hljs.registerLanguage('haml', require('highlight.js/lib/languages/haml')) 148 | hljs.registerLanguage( 149 | 'handlebars', 150 | require('highlight.js/lib/languages/handlebars'), 151 | ) 152 | hljs.registerLanguage( 153 | 'haskell', 154 | require('highlight.js/lib/languages/haskell'), 155 | ) 156 | hljs.registerLanguage('haxe', require('highlight.js/lib/languages/haxe')) 157 | hljs.registerLanguage('hsp', require('highlight.js/lib/languages/hsp')) 158 | hljs.registerLanguage('http', require('highlight.js/lib/languages/http')) 159 | hljs.registerLanguage('hy', require('highlight.js/lib/languages/hy')) 160 | hljs.registerLanguage( 161 | 'inform7', 162 | require('highlight.js/lib/languages/inform7'), 163 | ) 164 | hljs.registerLanguage('ini', require('highlight.js/lib/languages/ini')) 165 | hljs.registerLanguage('irpf90', require('highlight.js/lib/languages/irpf90')) 166 | hljs.registerLanguage('isbl', require('highlight.js/lib/languages/isbl')) 167 | hljs.registerLanguage('java', require('highlight.js/lib/languages/java')) 168 | hljs.registerLanguage( 169 | 'javascript', 170 | require('highlight.js/lib/languages/javascript'), 171 | ) 172 | hljs.registerLanguage( 173 | 'jboss-cli', 174 | require('highlight.js/lib/languages/jboss-cli'), 175 | ) 176 | hljs.registerLanguage('json', require('highlight.js/lib/languages/json')) 177 | hljs.registerLanguage('julia', require('highlight.js/lib/languages/julia')) 178 | hljs.registerLanguage( 179 | 'julia-repl', 180 | require('highlight.js/lib/languages/julia-repl'), 181 | ) 182 | hljs.registerLanguage('kotlin', require('highlight.js/lib/languages/kotlin')) 183 | hljs.registerLanguage('lasso', require('highlight.js/lib/languages/lasso')) 184 | hljs.registerLanguage('latex', require('highlight.js/lib/languages/latex')) 185 | hljs.registerLanguage('ldif', require('highlight.js/lib/languages/ldif')) 186 | hljs.registerLanguage('leaf', require('highlight.js/lib/languages/leaf')) 187 | hljs.registerLanguage('less', require('highlight.js/lib/languages/less')) 188 | hljs.registerLanguage('lisp', require('highlight.js/lib/languages/lisp')) 189 | hljs.registerLanguage( 190 | 'livecodeserver', 191 | require('highlight.js/lib/languages/livecodeserver'), 192 | ) 193 | hljs.registerLanguage( 194 | 'livescript', 195 | require('highlight.js/lib/languages/livescript'), 196 | ) 197 | hljs.registerLanguage('llvm', require('highlight.js/lib/languages/llvm')) 198 | hljs.registerLanguage('lsl', require('highlight.js/lib/languages/lsl')) 199 | hljs.registerLanguage('lua', require('highlight.js/lib/languages/lua')) 200 | hljs.registerLanguage( 201 | 'makefile', 202 | require('highlight.js/lib/languages/makefile'), 203 | ) 204 | hljs.registerLanguage( 205 | 'mathematica', 206 | require('highlight.js/lib/languages/mathematica'), 207 | ) 208 | hljs.registerLanguage('matlab', require('highlight.js/lib/languages/matlab')) 209 | hljs.registerLanguage('maxima', require('highlight.js/lib/languages/maxima')) 210 | hljs.registerLanguage('mel', require('highlight.js/lib/languages/mel')) 211 | hljs.registerLanguage( 212 | 'mercury', 213 | require('highlight.js/lib/languages/mercury'), 214 | ) 215 | hljs.registerLanguage( 216 | 'mipsasm', 217 | require('highlight.js/lib/languages/mipsasm'), 218 | ) 219 | hljs.registerLanguage('mizar', require('highlight.js/lib/languages/mizar')) 220 | hljs.registerLanguage('perl', require('highlight.js/lib/languages/perl')) 221 | hljs.registerLanguage( 222 | 'mojolicious', 223 | require('highlight.js/lib/languages/mojolicious'), 224 | ) 225 | hljs.registerLanguage('monkey', require('highlight.js/lib/languages/monkey')) 226 | hljs.registerLanguage( 227 | 'moonscript', 228 | require('highlight.js/lib/languages/moonscript'), 229 | ) 230 | hljs.registerLanguage('n1ql', require('highlight.js/lib/languages/n1ql')) 231 | hljs.registerLanguage( 232 | 'nestedtext', 233 | require('highlight.js/lib/languages/nestedtext'), 234 | ) 235 | hljs.registerLanguage('nginx', require('highlight.js/lib/languages/nginx')) 236 | hljs.registerLanguage('nim', require('highlight.js/lib/languages/nim')) 237 | hljs.registerLanguage('nix', require('highlight.js/lib/languages/nix')) 238 | hljs.registerLanguage( 239 | 'node-repl', 240 | require('highlight.js/lib/languages/node-repl'), 241 | ) 242 | hljs.registerLanguage('nsis', require('highlight.js/lib/languages/nsis')) 243 | hljs.registerLanguage( 244 | 'objectivec', 245 | require('highlight.js/lib/languages/objectivec'), 246 | ) 247 | hljs.registerLanguage('ocaml', require('highlight.js/lib/languages/ocaml')) 248 | hljs.registerLanguage( 249 | 'openscad', 250 | require('highlight.js/lib/languages/openscad'), 251 | ) 252 | hljs.registerLanguage( 253 | 'oxygene', 254 | require('highlight.js/lib/languages/oxygene'), 255 | ) 256 | hljs.registerLanguage( 257 | 'parser3', 258 | require('highlight.js/lib/languages/parser3'), 259 | ) 260 | hljs.registerLanguage('pf', require('highlight.js/lib/languages/pf')) 261 | hljs.registerLanguage('pgsql', require('highlight.js/lib/languages/pgsql')) 262 | hljs.registerLanguage('php', require('highlight.js/lib/languages/php')) 263 | hljs.registerLanguage( 264 | 'php-template', 265 | require('highlight.js/lib/languages/php-template'), 266 | ) 267 | hljs.registerLanguage( 268 | 'plaintext', 269 | require('highlight.js/lib/languages/plaintext'), 270 | ) 271 | hljs.registerLanguage('pony', require('highlight.js/lib/languages/pony')) 272 | hljs.registerLanguage( 273 | 'powershell', 274 | require('highlight.js/lib/languages/powershell'), 275 | ) 276 | hljs.registerLanguage( 277 | 'processing', 278 | require('highlight.js/lib/languages/processing'), 279 | ) 280 | hljs.registerLanguage( 281 | 'profile', 282 | require('highlight.js/lib/languages/profile'), 283 | ) 284 | hljs.registerLanguage('prolog', require('highlight.js/lib/languages/prolog')) 285 | hljs.registerLanguage( 286 | 'properties', 287 | require('highlight.js/lib/languages/properties'), 288 | ) 289 | hljs.registerLanguage( 290 | 'protobuf', 291 | require('highlight.js/lib/languages/protobuf'), 292 | ) 293 | hljs.registerLanguage('puppet', require('highlight.js/lib/languages/puppet')) 294 | hljs.registerLanguage( 295 | 'purebasic', 296 | require('highlight.js/lib/languages/purebasic'), 297 | ) 298 | hljs.registerLanguage('python', require('highlight.js/lib/languages/python')) 299 | hljs.registerLanguage( 300 | 'python-repl', 301 | require('highlight.js/lib/languages/python-repl'), 302 | ) 303 | hljs.registerLanguage('q', require('highlight.js/lib/languages/q')) 304 | hljs.registerLanguage('qml', require('highlight.js/lib/languages/qml')) 305 | hljs.registerLanguage('r', require('highlight.js/lib/languages/r')) 306 | hljs.registerLanguage( 307 | 'reasonml', 308 | require('highlight.js/lib/languages/reasonml'), 309 | ) 310 | hljs.registerLanguage('rib', require('highlight.js/lib/languages/rib')) 311 | hljs.registerLanguage( 312 | 'roboconf', 313 | require('highlight.js/lib/languages/roboconf'), 314 | ) 315 | hljs.registerLanguage( 316 | 'routeros', 317 | require('highlight.js/lib/languages/routeros'), 318 | ) 319 | hljs.registerLanguage('rsl', require('highlight.js/lib/languages/rsl')) 320 | hljs.registerLanguage( 321 | 'ruleslanguage', 322 | require('highlight.js/lib/languages/ruleslanguage'), 323 | ) 324 | hljs.registerLanguage('rust', require('highlight.js/lib/languages/rust')) 325 | hljs.registerLanguage('sas', require('highlight.js/lib/languages/sas')) 326 | hljs.registerLanguage('scala', require('highlight.js/lib/languages/scala')) 327 | hljs.registerLanguage('scheme', require('highlight.js/lib/languages/scheme')) 328 | hljs.registerLanguage('scilab', require('highlight.js/lib/languages/scilab')) 329 | hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss')) 330 | hljs.registerLanguage('shell', require('highlight.js/lib/languages/shell')) 331 | hljs.registerLanguage('smali', require('highlight.js/lib/languages/smali')) 332 | hljs.registerLanguage( 333 | 'smalltalk', 334 | require('highlight.js/lib/languages/smalltalk'), 335 | ) 336 | hljs.registerLanguage('sml', require('highlight.js/lib/languages/sml')) 337 | hljs.registerLanguage('sqf', require('highlight.js/lib/languages/sqf')) 338 | hljs.registerLanguage('sql', require('highlight.js/lib/languages/sql')) 339 | hljs.registerLanguage('stan', require('highlight.js/lib/languages/stan')) 340 | hljs.registerLanguage('stata', require('highlight.js/lib/languages/stata')) 341 | hljs.registerLanguage('step21', require('highlight.js/lib/languages/step21')) 342 | hljs.registerLanguage('stylus', require('highlight.js/lib/languages/stylus')) 343 | hljs.registerLanguage( 344 | 'subunit', 345 | require('highlight.js/lib/languages/subunit'), 346 | ) 347 | hljs.registerLanguage('swift', require('highlight.js/lib/languages/swift')) 348 | hljs.registerLanguage( 349 | 'taggerscript', 350 | require('highlight.js/lib/languages/taggerscript'), 351 | ) 352 | hljs.registerLanguage('yaml', require('highlight.js/lib/languages/yaml')) 353 | hljs.registerLanguage('tap', require('highlight.js/lib/languages/tap')) 354 | hljs.registerLanguage('tcl', require('highlight.js/lib/languages/tcl')) 355 | hljs.registerLanguage('thrift', require('highlight.js/lib/languages/thrift')) 356 | hljs.registerLanguage('tp', require('highlight.js/lib/languages/tp')) 357 | hljs.registerLanguage('twig', require('highlight.js/lib/languages/twig')) 358 | hljs.registerLanguage( 359 | 'typescript', 360 | require('highlight.js/lib/languages/typescript'), 361 | ) 362 | hljs.registerLanguage('vala', require('highlight.js/lib/languages/vala')) 363 | hljs.registerLanguage('vbnet', require('highlight.js/lib/languages/vbnet')) 364 | hljs.registerLanguage( 365 | 'vbscript', 366 | require('highlight.js/lib/languages/vbscript'), 367 | ) 368 | hljs.registerLanguage( 369 | 'vbscript-html', 370 | require('highlight.js/lib/languages/vbscript-html'), 371 | ) 372 | hljs.registerLanguage( 373 | 'verilog', 374 | require('highlight.js/lib/languages/verilog'), 375 | ) 376 | hljs.registerLanguage('vhdl', require('highlight.js/lib/languages/vhdl')) 377 | hljs.registerLanguage('vim', require('highlight.js/lib/languages/vim')) 378 | hljs.registerLanguage('wasm', require('highlight.js/lib/languages/wasm')) 379 | hljs.registerLanguage('wren', require('highlight.js/lib/languages/wren')) 380 | hljs.registerLanguage('x86asm', require('highlight.js/lib/languages/x86asm')) 381 | hljs.registerLanguage('xl', require('highlight.js/lib/languages/xl')) 382 | hljs.registerLanguage('xquery', require('highlight.js/lib/languages/xquery')) 383 | hljs.registerLanguage('zephir', require('highlight.js/lib/languages/zephir')) 384 | 385 | return hljs 386 | } 387 | -------------------------------------------------------------------------------- /src/html/allowlist.ts: -------------------------------------------------------------------------------- 1 | export type HTMLAllowList = { 2 | [tag: string]: 3 | | string[] 4 | | { [attr: string]: boolean | ((value: string) => string) } 5 | } 6 | 7 | const globalAttrs = { 8 | class: true, 9 | dir: (value) => { 10 | const normalized = value.toLowerCase() 11 | return ['rtl', 'ltr', 'auto'].includes(normalized) ? normalized : '' 12 | }, 13 | lang: true, 14 | title: true, 15 | } as const satisfies HTMLAllowList[string] 16 | 17 | const generateUrlSanitizer = 18 | (schemas: string[]) => 19 | (value: string): string => { 20 | if (value.includes(':')) { 21 | // Check the URL schema if it exists 22 | const trimmed = value.trim().toLowerCase() 23 | const schema = trimmed.split(':', 1)[0] 24 | 25 | for (const allowedSchema of schemas) { 26 | if (schema === allowedSchema) return value 27 | if (allowedSchema.includes(':') && trimmed.startsWith(allowedSchema)) 28 | return value 29 | } 30 | 31 | return '' 32 | } 33 | return value 34 | } 35 | 36 | const webUrlSanitizer = generateUrlSanitizer(['http', 'https']) 37 | const imageUrlSanitizer = generateUrlSanitizer(['http', 'https', 'data:image/']) 38 | const srcSetSanitizer = (value: string): string => { 39 | for (const src of value.split(',')) { 40 | if (!imageUrlSanitizer(src)) return '' 41 | } 42 | return value 43 | } 44 | 45 | export const defaultHTMLAllowList: HTMLAllowList = Object.assign( 46 | Object.create(null), 47 | { 48 | a: { 49 | ...globalAttrs, 50 | href: webUrlSanitizer, 51 | name: true, // deprecated attribute, but still useful in Marp for making stable anchor link 52 | rel: true, 53 | target: true, 54 | }, 55 | abbr: globalAttrs, 56 | address: globalAttrs, 57 | article: globalAttrs, 58 | aside: globalAttrs, 59 | audio: { 60 | ...globalAttrs, 61 | autoplay: true, 62 | controls: true, 63 | loop: true, 64 | muted: true, 65 | preload: true, 66 | src: webUrlSanitizer, 67 | }, 68 | b: globalAttrs, 69 | bdi: globalAttrs, 70 | bdo: globalAttrs, 71 | big: globalAttrs, 72 | blockquote: { 73 | ...globalAttrs, 74 | cite: webUrlSanitizer, 75 | }, 76 | br: globalAttrs, 77 | caption: globalAttrs, 78 | center: globalAttrs, // deprecated 79 | cite: globalAttrs, 80 | code: globalAttrs, 81 | col: { 82 | ...globalAttrs, 83 | align: true, 84 | valign: true, 85 | span: true, 86 | width: true, 87 | }, 88 | colgroup: { 89 | ...globalAttrs, 90 | align: true, 91 | valign: true, 92 | span: true, 93 | width: true, 94 | }, 95 | dd: globalAttrs, 96 | del: { 97 | ...globalAttrs, 98 | cite: webUrlSanitizer, 99 | datetime: true, 100 | }, 101 | details: { 102 | ...globalAttrs, 103 | open: true, 104 | }, 105 | div: globalAttrs, 106 | dl: globalAttrs, 107 | dt: globalAttrs, 108 | em: globalAttrs, 109 | figcaption: globalAttrs, 110 | figure: globalAttrs, 111 | // footer: globalAttrs, // Inserted by Marpit directives so disallowed to avoid confusion 112 | h1: globalAttrs, 113 | h2: globalAttrs, 114 | h3: globalAttrs, 115 | h4: globalAttrs, 116 | h5: globalAttrs, 117 | h6: globalAttrs, 118 | // header: globalAttrs, // Inserted by Marpit directives so disallowed to avoid confusion 119 | hr: globalAttrs, 120 | i: globalAttrs, 121 | img: { 122 | ...globalAttrs, 123 | align: true, // deprecated attribute, but still useful in Marp for aligning image 124 | alt: true, 125 | decoding: true, 126 | height: true, 127 | loading: true, 128 | src: imageUrlSanitizer, 129 | srcset: srcSetSanitizer, 130 | title: true, 131 | width: true, 132 | }, 133 | ins: { 134 | ...globalAttrs, 135 | cite: webUrlSanitizer, 136 | datetime: true, 137 | }, 138 | kbd: globalAttrs, 139 | li: { 140 | ...globalAttrs, 141 | type: true, 142 | value: true, 143 | }, 144 | mark: globalAttrs, 145 | nav: globalAttrs, 146 | ol: { 147 | ...globalAttrs, 148 | reversed: true, 149 | start: true, 150 | type: true, 151 | }, 152 | p: globalAttrs, 153 | picture: globalAttrs, 154 | pre: globalAttrs, 155 | source: { 156 | height: true, 157 | media: true, 158 | sizes: true, 159 | src: imageUrlSanitizer, 160 | srcset: srcSetSanitizer, 161 | type: true, 162 | width: true, 163 | }, 164 | q: { 165 | ...globalAttrs, 166 | cite: webUrlSanitizer, 167 | }, 168 | rp: globalAttrs, 169 | rt: globalAttrs, 170 | ruby: globalAttrs, 171 | s: globalAttrs, 172 | section: globalAttrs, 173 | small: globalAttrs, 174 | span: globalAttrs, 175 | sub: globalAttrs, 176 | summary: globalAttrs, 177 | sup: globalAttrs, 178 | strong: globalAttrs, 179 | strike: globalAttrs, 180 | table: { 181 | ...globalAttrs, 182 | width: true, 183 | border: true, 184 | align: true, 185 | valign: true, 186 | }, 187 | tbody: { 188 | ...globalAttrs, 189 | align: true, 190 | valign: true, 191 | }, 192 | td: { 193 | ...globalAttrs, 194 | width: true, 195 | rowspan: true, 196 | colspan: true, 197 | align: true, 198 | valign: true, 199 | }, 200 | tfoot: { 201 | ...globalAttrs, 202 | align: true, 203 | valign: true, 204 | }, 205 | th: { 206 | ...globalAttrs, 207 | width: true, 208 | rowspan: true, 209 | colspan: true, 210 | align: true, 211 | valign: true, 212 | }, 213 | thead: { 214 | ...globalAttrs, 215 | align: true, 216 | valign: true, 217 | }, 218 | time: { 219 | ...globalAttrs, 220 | datetime: true, 221 | }, 222 | tr: { 223 | ...globalAttrs, 224 | rowspan: true, 225 | align: true, 226 | valign: true, 227 | }, 228 | u: globalAttrs, 229 | ul: globalAttrs, 230 | video: { 231 | ...globalAttrs, 232 | autoplay: true, 233 | controls: true, 234 | loop: true, 235 | muted: true, 236 | playsinline: true, 237 | poster: imageUrlSanitizer, 238 | preload: true, 239 | src: webUrlSanitizer, 240 | height: true, 241 | width: true, 242 | }, 243 | wbr: globalAttrs, 244 | } as const satisfies HTMLAllowList, 245 | ) 246 | -------------------------------------------------------------------------------- /src/html/html.ts: -------------------------------------------------------------------------------- 1 | import selfClosingTags from 'self-closing-tags' 2 | import { FilterXSS, friendlyAttrValue, escapeAttrValue } from 'xss' 3 | import type { SafeAttrValueHandler, IWhiteList } from 'xss' 4 | import { MarpOptions } from '../marp' 5 | 6 | const selfClosingRegexp = /\s*\/?>$/ 7 | const xhtmlOutFilter = new FilterXSS({ 8 | onIgnoreTag: (tag, html, { isClosing }: any) => { 9 | if (selfClosingTags.includes(tag)) { 10 | const attrs = html.slice(tag.length + (isClosing ? 2 : 1), -1).trim() 11 | return `<${tag} ${attrs}>`.replace(selfClosingRegexp, ' />') 12 | } 13 | return html 14 | }, 15 | allowList: {}, 16 | }) 17 | 18 | // Prevent breaking JavaScript special characters such as `<` and `>` by HTML 19 | // escape process only if the entire content of HTML block is consisted of 20 | // script tag (The case of matching the case 1 of https://spec.commonmark.org/0.31.2/#html-blocks, 21 | // with special condition for `, that will not exclude from sanitizing. 25 | // 26 | const scriptBlockRegexp = 27 | /^|[ \t\f\n\r][\s\S]*?>)([\s\S]*)<\/script>[ \t\f\n\r]*$/i 28 | 29 | const scriptBlockContentUnexpectedCloseRegexp = /<\/script[>/\t\f\n\r ]/i 30 | 31 | const isValidScriptBlock = (htmlBlockContent: string) => { 32 | const m = htmlBlockContent.match(scriptBlockRegexp) 33 | return !!(m && !scriptBlockContentUnexpectedCloseRegexp.test(m[1])) 34 | } 35 | 36 | export function markdown(md): void { 37 | const { html_inline, html_block } = md.renderer.rules 38 | 39 | const fetchHtmlOption = (): MarpOptions['html'] => md.options.html 40 | const fetchAllowList = (html = fetchHtmlOption()): IWhiteList => { 41 | const allowList: IWhiteList = Object.create(null) 42 | 43 | if (typeof html === 'object') { 44 | for (const tag of Object.keys(html)) { 45 | const attrs = html[tag] 46 | 47 | if (Array.isArray(attrs)) { 48 | allowList[tag] = attrs 49 | } else if (typeof attrs === 'object') { 50 | allowList[tag] = Object.keys(attrs).filter( 51 | (attr) => attrs[attr] !== false, 52 | ) 53 | } 54 | } 55 | } 56 | return allowList 57 | } 58 | 59 | const generateSafeAttrValueHandler = 60 | (html = fetchHtmlOption()): SafeAttrValueHandler => 61 | (tag, attr, value) => { 62 | let ret = friendlyAttrValue(value) 63 | 64 | if ( 65 | typeof html === 'object' && 66 | html[tag] && 67 | !Array.isArray(html[tag]) && 68 | typeof html[tag][attr] === 'function' 69 | ) { 70 | ret = html[tag][attr](ret) 71 | } 72 | 73 | return escapeAttrValue(ret) 74 | } 75 | 76 | const sanitize = (ret: string) => { 77 | const html = fetchHtmlOption() 78 | const filter = new FilterXSS({ 79 | allowList: fetchAllowList(html), 80 | onIgnoreTag: (_, rawHtml) => (html === true ? rawHtml : undefined), 81 | safeAttrValue: generateSafeAttrValueHandler(html), 82 | }) 83 | 84 | const sanitized = filter.process(ret) 85 | return md.options.xhtmlOut ? xhtmlOutFilter.process(sanitized) : sanitized 86 | } 87 | 88 | md.renderer.rules.html_inline = (...args) => sanitize(html_inline(...args)) 89 | md.renderer.rules.html_block = (...args) => { 90 | const ret = html_block(...args) 91 | const html = fetchHtmlOption() 92 | 93 | const scriptAllowAttrs = (() => { 94 | if (html === true) return [] 95 | if (typeof html === 'object' && html['script']) 96 | return fetchAllowList({ script: html.script }).script 97 | })() 98 | 99 | // If the entire content of HTML block is consisted of script tag when the 100 | // script tag is allowed, we will not escape the content of the script tag. 101 | if (scriptAllowAttrs && isValidScriptBlock(ret)) { 102 | const scriptFilter = new FilterXSS({ 103 | allowList: { script: scriptAllowAttrs || [] }, 104 | allowCommentTag: true, 105 | onIgnoreTagAttr: (_, name, value) => { 106 | if (html === true) return `${name}="${escapeAttrValue(value)}"` 107 | }, 108 | escapeHtml: (s) => s, 109 | safeAttrValue: generateSafeAttrValueHandler(html), 110 | }) 111 | 112 | return scriptFilter.process(ret) 113 | } 114 | 115 | return sanitize(ret) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/marp.ts: -------------------------------------------------------------------------------- 1 | import postcssMinify from '@csstools/postcss-minify' 2 | import { Marpit, Options, ThemeSetPackOptions } from '@marp-team/marpit' 3 | import type { HLJSApi } from 'highlight.js' 4 | import postcss, { AcceptedPlugin } from 'postcss' 5 | import defaultTheme from '../themes/default.scss' 6 | import gaiaTheme from '../themes/gaia.scss' 7 | import uncoverTheme from '../themes/uncover.scss' 8 | import * as autoScalingPlugin from './auto-scaling' 9 | import * as customElements from './custom-elements' 10 | import * as emojiPlugin from './emoji/emoji' 11 | import { generateHighlightJSInstance } from './highlightjs' 12 | import { defaultHTMLAllowList, type HTMLAllowList } from './html/allowlist' 13 | import * as htmlPlugin from './html/html' 14 | import * as mathPlugin from './math/math' 15 | import * as scriptPlugin from './script/script' 16 | import * as sizePlugin from './size/size' 17 | import * as slugPlugin from './slug/slug' 18 | 19 | export interface MarpOptions extends Options { 20 | emoji?: emojiPlugin.EmojiOptions 21 | html?: boolean | HTMLAllowList 22 | markdown?: object 23 | math?: mathPlugin.MathOptions 24 | minifyCSS?: boolean 25 | script?: boolean | scriptPlugin.ScriptOptions 26 | slug?: slugPlugin.SlugOptions 27 | } 28 | 29 | export class Marp extends Marpit { 30 | declare readonly options: Required 31 | 32 | private _highlightjs: HLJSApi | undefined 33 | 34 | static readonly html = defaultHTMLAllowList 35 | 36 | constructor(opts: MarpOptions = {}) { 37 | const mdOpts: Record = { 38 | breaks: true, 39 | linkify: true, 40 | highlight: (code, lang, attrs) => this.highlighter(code, lang, attrs), 41 | html: opts.html ?? Marp.html, 42 | ...(typeof opts.markdown === 'object' ? opts.markdown : {}), 43 | } 44 | 45 | super({ 46 | cssContainerQuery: true, 47 | inlineSVG: true, 48 | looseYAML: true, 49 | math: true, 50 | minifyCSS: true, 51 | script: true, 52 | slug: true, 53 | ...opts, 54 | emoji: { 55 | shortcode: 'twemoji', 56 | unicode: 'twemoji', 57 | ...(opts.emoji || {}), 58 | }, 59 | markdown: ['commonmark', mdOpts], 60 | } as MarpOptions) 61 | 62 | this.markdown.enable(['table', 'linkify', 'strikethrough']) 63 | this.markdown.linkify.set({ fuzzyLink: false }) 64 | 65 | if (mdOpts.typographer) { 66 | this.markdown.enable(['replacements', 'smartquotes']) 67 | } 68 | 69 | // Theme support 70 | this.themeSet.metaType = Object.freeze({ 71 | 'auto-scaling': String, 72 | size: Array, 73 | }) 74 | 75 | this.themeSet.default = this.themeSet.add(defaultTheme) 76 | this.themeSet.add(gaiaTheme) 77 | this.themeSet.add(uncoverTheme) 78 | } 79 | 80 | protected applyMarkdownItPlugins(md) { 81 | super.applyMarkdownItPlugins(md) 82 | 83 | md.use(htmlPlugin.markdown) 84 | .use(emojiPlugin.markdown) 85 | .use(mathPlugin.markdown) 86 | .use(autoScalingPlugin.markdown) 87 | .use(sizePlugin.markdown) 88 | .use(scriptPlugin.markdown) 89 | .use(slugPlugin.markdown) 90 | } 91 | 92 | get highlightjs() { 93 | if (!this._highlightjs) { 94 | this._highlightjs = generateHighlightJSInstance() 95 | } 96 | return this._highlightjs 97 | } 98 | 99 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 100 | highlighter(code: string, lang: string, attrs: string): string { 101 | if (lang && this.highlightjs.getLanguage(lang)) { 102 | return this.highlightjs.highlight(code, { 103 | language: lang, 104 | ignoreIllegals: true, 105 | }).value 106 | } 107 | return '' 108 | } 109 | 110 | protected renderStyle(theme?: string): string { 111 | const original = super.renderStyle(theme) 112 | const postprocessorPlugins: AcceptedPlugin[] = [ 113 | customElements.css, 114 | ...(this.options.minifyCSS ? [postcssMinify()] : []), 115 | ] 116 | 117 | const postprocessor = postcss(postprocessorPlugins) 118 | 119 | return postprocessor.process(original).css 120 | } 121 | 122 | protected themeSetPackOptions(): ThemeSetPackOptions { 123 | const base = { ...super.themeSetPackOptions() } 124 | const prepend = (css) => 125 | css && (base.before = `${css}\n${base.before || ''}`) 126 | const { emoji } = this.options 127 | 128 | prepend(emojiPlugin.css(emoji)) 129 | 130 | const mathCss = mathPlugin.css(this) 131 | if (mathCss) prepend(mathCss) 132 | 133 | return base 134 | } 135 | } 136 | 137 | export default Marp 138 | -------------------------------------------------------------------------------- /src/math/context.ts: -------------------------------------------------------------------------------- 1 | import type { MathOptionsInterface } from './math' 2 | 3 | type MathContext = { 4 | /** Whether Markdown is using math syntax */ 5 | enabled: boolean 6 | 7 | /** Math options that have passed into Marp Core instance */ 8 | options: MathOptionsInterface 9 | 10 | /** Whether Math plugin is processing in the context for current render */ 11 | processing: boolean 12 | 13 | // Library specific contexts 14 | katexMacroContext: Record 15 | mathjaxContext: any 16 | } 17 | 18 | const contextSymbol = Symbol('marp-math-context') 19 | 20 | export const setMathContext = ( 21 | target: any, 22 | setter: (current: MathContext) => MathContext, 23 | ) => { 24 | if (!Object.prototype.hasOwnProperty.call(target, contextSymbol)) { 25 | Object.defineProperty(target, contextSymbol, { writable: true }) 26 | } 27 | target[contextSymbol] = setter(target[contextSymbol]) 28 | } 29 | 30 | export const getMathContext = (target: any): MathContext => ({ 31 | ...target[contextSymbol], 32 | }) 33 | -------------------------------------------------------------------------------- /src/math/katex.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:meta'; 2 | @include meta.load-css('pkg:katex/dist/katex.min.css'); 3 | 4 | .katex-display { 5 | margin: 0; 6 | } 7 | 8 | .katex { 9 | /** 10 | * Chrome browser may not render a symbol with .op-symbol class due to 11 | * relative positioning for the inline element. 12 | * 13 | * https://github.com/marp-team/marp-vscode/issues/393 14 | */ 15 | .delimcenter, 16 | .op-symbol { 17 | display: inline-block; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/math/katex.ts: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'katex' 2 | import { version } from 'katex/package.json' 3 | import { isEnabledAutoScaling } from '../auto-scaling/utils' 4 | import { getMathContext } from './context' 5 | import katexScss from './katex.scss' 6 | 7 | const convertedCSS = Object.create(null) 8 | const katexMatcher = /url\(['"]?fonts\/(.*?)['"]?\)/g 9 | 10 | export const inline = (marpit: any) => (tokens, idx) => { 11 | const { content } = tokens[idx] 12 | const { 13 | options: { katexOption }, 14 | katexMacroContext, 15 | } = getMathContext(marpit) 16 | 17 | try { 18 | return renderToString(content, { 19 | throwOnError: false, 20 | ...(katexOption || {}), 21 | macros: katexMacroContext, 22 | displayMode: false, 23 | }) 24 | } catch (e) { 25 | console.warn(e) 26 | return content 27 | } 28 | } 29 | 30 | export const block = (marpit: any) => (tokens, idx) => { 31 | const { content } = tokens[idx] 32 | const { 33 | options: { katexOption }, 34 | katexMacroContext, 35 | } = getMathContext(marpit) 36 | 37 | try { 38 | let rendered = renderToString(content, { 39 | throwOnError: false, 40 | ...(katexOption || {}), 41 | macros: katexMacroContext, 42 | displayMode: true, 43 | }) 44 | 45 | if (marpit.options.inlineSVG && isEnabledAutoScaling(marpit, 'math')) { 46 | rendered = rendered.replace( 47 | /^${rendered}

` 53 | } catch (e) { 54 | console.warn(e) 55 | return `

${content}

` 56 | } 57 | } 58 | 59 | export const css = (path?: string | false): string => { 60 | if (path === false) return katexScss 61 | 62 | const fontPath = 63 | path || `https://cdn.jsdelivr.net/npm/katex@${version}/dist/fonts/` 64 | 65 | return (convertedCSS[fontPath] = 66 | convertedCSS[fontPath] || 67 | katexScss.replace( 68 | katexMatcher, 69 | (_, matched) => `url('${fontPath}${matched}')`, 70 | )) 71 | } 72 | -------------------------------------------------------------------------------- /src/math/math.ts: -------------------------------------------------------------------------------- 1 | import marpitPlugin from '@marp-team/marpit/plugin' 2 | import { Marp } from '../marp' 3 | import { getMathContext, setMathContext } from './context' 4 | import * as katex from './katex' 5 | import * as mathjax from './mathjax' 6 | 7 | export type MathPreferredLibrary = 'mathjax' | 'katex' 8 | 9 | export interface MathOptionsInterface { 10 | lib?: MathPreferredLibrary 11 | katexOption?: Record 12 | katexFontPath?: string | false 13 | } 14 | 15 | export type MathOptions = boolean | MathPreferredLibrary | MathOptionsInterface 16 | 17 | const defaultLibrary = 'mathjax' as const 18 | const getLibrary = (opts: MathOptionsInterface) => opts.lib ?? defaultLibrary 19 | 20 | export const markdown = marpitPlugin((md) => { 21 | const marp: Marp = md.marpit 22 | const opts: MathOptions | undefined = marp.options.math 23 | 24 | if (!opts) return 25 | 26 | const parsedOpts = 27 | typeof opts !== 'object' 28 | ? { lib: typeof opts === 'string' ? opts : undefined } 29 | : opts 30 | 31 | // Define `math` global directive to choose preferred library 32 | Object.defineProperty(marp.customDirectives.global, 'math', { 33 | value: (math: unknown): { math?: MathPreferredLibrary } => { 34 | if (math === 'katex' || math === 'mathjax') return { math } 35 | return {} 36 | }, 37 | }) 38 | 39 | // Initialize 40 | const { parse, parseInline } = md 41 | 42 | const initializeMathContext = () => { 43 | if (getMathContext(marp).processing) return false 44 | 45 | setMathContext(marp, () => ({ 46 | enabled: false, 47 | options: parsedOpts, 48 | processing: true, 49 | katexMacroContext: { 50 | ...((parsedOpts.katexOption?.macros as any) || {}), 51 | }, 52 | mathjaxContext: null, 53 | })) 54 | 55 | return true 56 | } 57 | 58 | const parseWithMath = any>( 59 | func: F, 60 | ) => { 61 | return function (this: ThisType, ...args: Parameters) { 62 | const initialized = initializeMathContext() 63 | 64 | try { 65 | return func.apply(this, args) 66 | } finally { 67 | if (initialized) { 68 | setMathContext(marp, (ctx) => ({ ...ctx, processing: false })) 69 | } 70 | } 71 | } 72 | } 73 | 74 | md.parse = parseWithMath(parse) 75 | md.parseInline = parseWithMath(parseInline) 76 | 77 | const enableMath = () => 78 | setMathContext(marp, (ctx) => ({ ...ctx, enabled: true })) 79 | 80 | // Inline 81 | md.inline.ruler.after('escape', 'marp_math_inline', (state, silent) => { 82 | const ret = parseInlineMath(state, silent) 83 | if (ret) enableMath() 84 | 85 | return ret 86 | }) 87 | 88 | // Block 89 | md.block.ruler.after( 90 | 'blockquote', 91 | 'marp_math_block', 92 | (state, start, end, silent) => { 93 | const ret = parseMathBlock(state, start, end, silent) 94 | if (ret) enableMath() 95 | 96 | return ret 97 | }, 98 | { alt: ['paragraph', 'reference', 'blockquote', 'list'] }, 99 | ) 100 | 101 | // Renderer 102 | md.core.ruler.after( 103 | 'marpit_directives_global_parse', 104 | 'marp_math_directive', 105 | () => { 106 | const { enabled } = getMathContext(marp) 107 | if (!enabled) return 108 | 109 | const preffered: MathPreferredLibrary | undefined = (marp as any) 110 | .lastGlobalDirectives.math 111 | 112 | setMathContext(marp, (ctx) => ({ 113 | ...ctx, 114 | options: { 115 | ...ctx.options, 116 | lib: preffered ?? parsedOpts.lib ?? defaultLibrary, 117 | }, 118 | })) 119 | }, 120 | ) 121 | 122 | const getPreferredLibrary = () => { 123 | const { options } = getMathContext(marp) 124 | return getLibrary(options) === 'mathjax' ? mathjax : katex 125 | } 126 | 127 | const getRenderer = (type: 'inline' | 'block') => (tokens: any, idx: any) => 128 | getPreferredLibrary()[type](marp)(tokens, idx) 129 | 130 | md.renderer.rules.marp_math_inline = getRenderer('inline') 131 | md.renderer.rules.marp_math_block = getRenderer('block') 132 | }) 133 | 134 | export const css = (marpit: any): string | null => { 135 | const { enabled, options } = getMathContext(marpit) 136 | if (!enabled) return null 137 | 138 | switch (getLibrary(options)) { 139 | case 'mathjax': 140 | return mathjax.css(marpit) 141 | case 'katex': 142 | return katex.css(options.katexFontPath) 143 | } 144 | } 145 | 146 | // --- 147 | 148 | function isValidDelim(state, pos = state.pos) { 149 | const ret = { openable: true, closable: true } 150 | const { posMax, src } = state 151 | const prev = pos > 0 ? src.charCodeAt(pos - 1) : -1 152 | const next = pos + 1 <= posMax ? src.charCodeAt(pos + 1) : -1 153 | 154 | if (next === 0x20 || next === 0x09) ret.openable = false 155 | if (prev === 0x20 || prev === 0x09 || (next >= 0x30 && next <= 0x39)) { 156 | ret.closable = false 157 | } 158 | 159 | return ret 160 | } 161 | 162 | function parseInlineMath(state, silent) { 163 | const { src, pos } = state 164 | if (src[pos] !== '$') return false 165 | 166 | const addPending = (stt: string) => (state.pending += stt) 167 | const found = (manipulation: () => void, newPos: number) => { 168 | if (!silent) manipulation() 169 | state.pos = newPos 170 | return true 171 | } 172 | 173 | const start = pos + 1 174 | if (!isValidDelim(state).openable) return found(() => addPending('$'), start) 175 | 176 | let match = start 177 | while ((match = src.indexOf('$', match)) !== -1) { 178 | let dollarPos = match - 1 179 | while (src[dollarPos] === '\\') dollarPos -= 1 180 | 181 | if ((match - dollarPos) % 2 === 1) break 182 | match += 1 183 | } 184 | 185 | if (match === -1) return found(() => addPending('$'), start) 186 | if (match - start === 0) return found(() => addPending('$$'), start + 1) 187 | if (!isValidDelim(state, match).closable) { 188 | return found(() => addPending('$'), start) 189 | } 190 | 191 | return found(() => { 192 | const token = state.push('marp_math_inline', 'math', 0) 193 | token.markup = '$' 194 | token.content = src.slice(start, match) 195 | }, match + 1) 196 | } 197 | 198 | function parseMathBlock(state, start, end, silent) { 199 | const { blkIndent, bMarks, eMarks, src, tShift } = state 200 | let pos = bMarks[start] + tShift[start] 201 | let max = eMarks[start] 202 | 203 | if (pos + 2 > max || src.slice(pos, pos + 2) !== '$$') return false 204 | if (silent) return true 205 | 206 | pos += 2 207 | 208 | let firstLine = src.slice(pos, max) 209 | let lastLine 210 | let found = firstLine.trim().slice(-2) === '$$' 211 | 212 | if (found) firstLine = firstLine.trim().slice(0, -2) 213 | 214 | let next = start 215 | for (; !found; ) { 216 | next += 1 217 | if (next >= end) break 218 | 219 | pos = bMarks[next] + tShift[next] 220 | max = eMarks[next] 221 | if (pos < max && tShift[next] < blkIndent) break 222 | 223 | const target = src.slice(pos, max).trim() 224 | 225 | if (target.slice(-2) === '$$') { 226 | found = true 227 | lastLine = src.slice(pos, src.slice(0, max).lastIndexOf('$$')) 228 | } 229 | } 230 | 231 | state.line = next + 1 232 | 233 | const token = state.push('marp_math_block', 'math', 0) 234 | token.block = true 235 | token.content = '' 236 | token.map = [start, state.line] 237 | token.markup = '$$' 238 | 239 | if (firstLine?.trim()) token.content += `${firstLine}\n` 240 | token.content += state.getLines(start + 1, next, tShift[start], true) 241 | if (lastLine?.trim()) token.content += lastLine 242 | 243 | return true 244 | } 245 | -------------------------------------------------------------------------------- /src/math/mathjax.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Disable thickening strokes in print media (e.g. PDF). 3 | * 4 | * https://github.com/mathjax/MathJax-src/blob/2dd53ce6c8af3c9cceba0baf014ea9b065130774/ts/output/svg/Wrappers/TextNode.ts#L49-L53 5 | * https://github.com/marp-team/marp-core/issues/287 6 | */ 7 | @media print { 8 | mjx-container[jax='SVG'] { 9 | use[data-c], 10 | path[data-c] { 11 | stroke-width: 0; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/math/mathjax.ts: -------------------------------------------------------------------------------- 1 | import { liteAdaptor, LiteAdaptor } from 'mathjax-full/js/adaptors/liteAdaptor' 2 | import { RegisterHTMLHandler } from 'mathjax-full/js/handlers/html' 3 | import { TeX } from 'mathjax-full/js/input/tex' 4 | import { AllPackages } from 'mathjax-full/js/input/tex/AllPackages' 5 | import { mathjax } from 'mathjax-full/js/mathjax' 6 | import { SVG } from 'mathjax-full/js/output/svg' 7 | import { getMathContext, setMathContext } from './context' 8 | import mathjaxScss from './mathjax.scss' 9 | 10 | interface MathJaxContext { 11 | adaptor: LiteAdaptor 12 | css: string 13 | document: ReturnType<(typeof mathjax)['document']> 14 | } 15 | 16 | const context = (marpit: any): MathJaxContext => { 17 | let { mathjaxContext } = getMathContext(marpit) 18 | 19 | if (!mathjaxContext) { 20 | const adaptor = liteAdaptor() 21 | RegisterHTMLHandler(adaptor) 22 | 23 | const tex = new TeX({ packages: AllPackages }) 24 | const svg = new SVG({ fontCache: 'none' }) 25 | const document = mathjax.document('', { InputJax: tex, OutputJax: svg }) 26 | const css = adaptor.textContent(svg.styleSheet(document) as any) 27 | 28 | mathjaxContext = { adaptor, css, document } 29 | setMathContext(marpit, (ctx) => ({ ...ctx, mathjaxContext })) 30 | } 31 | 32 | return mathjaxContext 33 | } 34 | 35 | export const inline = (marpit: any) => (tokens, idx) => { 36 | const { adaptor, document } = context(marpit) 37 | const { content } = tokens[idx] 38 | 39 | try { 40 | return adaptor.outerHTML(document.convert(content, { display: false })) 41 | } catch (e) { 42 | console.warn(e) 43 | return content 44 | } 45 | } 46 | 47 | export const block = (marpit: any) => 48 | Object.assign( 49 | (tokens, idx) => { 50 | const { adaptor, document } = context(marpit) 51 | const { content } = tokens[idx] 52 | 53 | try { 54 | const converted = document.convert(content, { display: true }) 55 | const svg: any = adaptor.firstChild(converted) 56 | const svgHeight = adaptor.getAttribute(svg, 'height') 57 | 58 | adaptor.setStyle(converted, 'margin', '0') 59 | adaptor.setStyle(svg, 'display', 'block') 60 | adaptor.setStyle(svg, 'width', '100%') 61 | adaptor.setStyle(svg, 'height', 'auto') 62 | adaptor.setStyle(svg, 'max-height', svgHeight) 63 | 64 | return `

${adaptor.outerHTML(converted)}

` 65 | } catch (e) { 66 | console.warn(e) 67 | return `

${content}

` 68 | } 69 | }, 70 | { scaled: true }, 71 | ) 72 | 73 | export const css = (marpit: any) => context(marpit).css + '\n' + mathjaxScss 74 | -------------------------------------------------------------------------------- /src/observer.ts: -------------------------------------------------------------------------------- 1 | import { observe } from '@marp-team/marpit-svg-polyfill' 2 | 3 | type ObserverOptions = { 4 | once?: boolean 5 | target?: ParentNode 6 | } 7 | 8 | export function observer({ 9 | once = false, 10 | target = document, 11 | }: ObserverOptions = {}): () => void { 12 | const cleanup = observe(target) 13 | 14 | if (once) { 15 | cleanup() 16 | 17 | return () => { 18 | /* no ops */ 19 | } 20 | } 21 | 22 | return cleanup 23 | } 24 | 25 | export default observer 26 | -------------------------------------------------------------------------------- /src/script/browser-script.ts: -------------------------------------------------------------------------------- 1 | const placeholder = 2 | 'This is a placeholder for the content of built browser script.' 3 | 4 | export default placeholder 5 | -------------------------------------------------------------------------------- /src/script/script.ts: -------------------------------------------------------------------------------- 1 | import { name, version } from '../../package.json' 2 | import { Marp } from '../marp' 3 | import browserScript from './browser-script' 4 | 5 | interface ScriptOptionsInternal { 6 | nonce?: string 7 | source: 'inline' | 'cdn' 8 | } 9 | 10 | export type ScriptOptions = Partial 11 | 12 | const defaultOptions = { source: 'inline' } as const 13 | 14 | export function markdown(md): void { 15 | const marp: Marp = md.marpit 16 | const opts = ((): false | ScriptOptionsInternal => { 17 | if (marp.options.script === false) return false 18 | if (marp.options.script === true) return defaultOptions 19 | 20 | return { ...defaultOptions, ...marp.options.script } 21 | })() 22 | 23 | md.core.ruler.before('marpit_collect', 'marp_core_script', (state) => { 24 | if (opts === false) return 25 | 26 | const lastSlideCloseIdxRev = [...state.tokens] 27 | .reverse() 28 | .findIndex((t) => t.type === 'marpit_slide_close') 29 | 30 | if (lastSlideCloseIdxRev < 0) return 31 | 32 | // Inject script token to the last page 33 | const token = state.tokens[state.tokens.length - lastSlideCloseIdxRev - 1] 34 | const { Token } = state 35 | const scriptToken = new Token('marp_core_script', 'script', 0) 36 | 37 | scriptToken.block = true 38 | scriptToken.nesting = 0 39 | 40 | if (opts.source === 'inline') { 41 | scriptToken.content = browserScript 42 | } else if (opts.source === 'cdn') { 43 | scriptToken.attrSet( 44 | 'src', 45 | `https://cdn.jsdelivr.net/npm/${name}@${version}/lib/browser.js`, 46 | ) 47 | 48 | // defer attribute would have no effect in inline script 49 | scriptToken.attrSet('defer', '') 50 | } 51 | 52 | if (opts.nonce) scriptToken.attrSet('nonce', opts.nonce) 53 | 54 | token.meta = token.meta || {} 55 | token.meta.marpCoreScriptTokens = token.meta.marpCoreScriptTokens || [] 56 | token.meta.marpCoreScriptTokens.push(scriptToken) 57 | }) 58 | 59 | const { marpit_slide_close } = md.renderer.rules 60 | 61 | md.renderer.rules.marpit_slide_close = (tokens, idx, opts, env, self) => { 62 | const renderer = marpit_slide_close || self.renderToken 63 | const original = renderer.call(self, tokens, idx, opts, env, self) 64 | 65 | // Append scripts 66 | const token = tokens[idx] 67 | 68 | if (token?.meta?.marpCoreScriptTokens) { 69 | return `${original}${token.meta.marpCoreScriptTokens 70 | .filter((t) => t.type === 'marp_core_script') 71 | .map((t) => `${t.content || ''}`) 72 | .join('')}` 73 | } 74 | 75 | return original 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/size/size.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@marp-team/marpit' 2 | import marpitPlugin from '@marp-team/marpit/plugin' 3 | import { Marp } from '../marp' 4 | 5 | interface DefinedSize { 6 | width: string 7 | height: string 8 | } 9 | 10 | interface RestorableThemes { 11 | default: Theme | undefined 12 | themes: Set 13 | } 14 | 15 | const sizePluginSymbol = Symbol('marp-size-plugin') 16 | 17 | export const markdown = marpitPlugin((md) => { 18 | const marp: Marp = md.marpit 19 | const { render } = marp 20 | 21 | const definedSizes = (theme: Theme): ReadonlyMap => { 22 | const sizes = (marp.themeSet.getThemeMeta(theme, 'size') as string[]) || [] 23 | const map = new Map() 24 | 25 | for (const value of sizes) { 26 | const args = value.split(/\s+/) 27 | 28 | if (args.length === 3) { 29 | map.set(args[0], { width: args[1], height: args[2] }) 30 | } else if (args.length === 2 && args[1] === 'false') { 31 | map.delete(args[0]) 32 | } 33 | } 34 | 35 | return map 36 | } 37 | 38 | const forRestore: RestorableThemes = { 39 | themes: new Set(), 40 | default: undefined, 41 | } 42 | 43 | // Define `size` global directive 44 | Object.defineProperty(marp.customDirectives.global, 'size', { 45 | value: (size) => (typeof size === 'string' ? { size } : {}), 46 | }) 47 | 48 | // Override render method to restore original theme set 49 | marp.render = (...args) => { 50 | try { 51 | return render.apply(marp, args) 52 | } finally { 53 | forRestore.themes.forEach((theme) => marp.themeSet.addTheme(theme)) 54 | 55 | if (forRestore.default) marp.themeSet.default = forRestore.default 56 | } 57 | } 58 | 59 | md.core.ruler.after( 60 | 'marpit_directives_global_parse', 61 | 'marp_size', 62 | (state) => { 63 | if (state.inlineMode) return 64 | 65 | forRestore.themes.clear() 66 | forRestore.default = undefined 67 | 68 | const { theme, size } = (marp as any).lastGlobalDirectives 69 | if (!size) return 70 | 71 | const themeInstance = marp.themeSet.get(theme, true) as Theme 72 | const customSize = definedSizes(themeInstance).get(size) 73 | 74 | if (customSize) { 75 | state[sizePluginSymbol] = size 76 | 77 | const { width, height } = customSize 78 | const css = `${themeInstance.css}\nsection{width:${width};height:${height};}` 79 | 80 | const overrideTheme = Object.assign(new (Theme as any)(), { 81 | ...themeInstance, 82 | ...customSize, 83 | css, 84 | }) 85 | 86 | forRestore.themes.add(themeInstance) 87 | 88 | if (themeInstance === marp.themeSet.default) { 89 | forRestore.default = themeInstance 90 | marp.themeSet.default = overrideTheme 91 | } 92 | 93 | if (marp.themeSet.has(overrideTheme.name)) { 94 | marp.themeSet.addTheme(overrideTheme) 95 | } 96 | } 97 | }, 98 | ) 99 | 100 | md.core.ruler.after('marpit_directives_apply', 'marp_size_apply', (state) => { 101 | if (state.inlineMode || !state[sizePluginSymbol]) return 102 | 103 | for (const token of state.tokens) { 104 | const { marpitDirectives } = token.meta || {} 105 | if (marpitDirectives) token.attrSet('data-size', state[sizePluginSymbol]) 106 | } 107 | }) 108 | 109 | md.core.ruler.after( 110 | 'marpit_advanced_background', 111 | 'marp_size_apply_advanced_background', 112 | (state) => { 113 | if (state.inlineMode || !state[sizePluginSymbol]) return 114 | 115 | for (const token of state.tokens) { 116 | if (token.type === 'marpit_advanced_pseudo_section_open') { 117 | token.attrSet('data-size', state[sizePluginSymbol]) 118 | } 119 | } 120 | }, 121 | ) 122 | }) 123 | -------------------------------------------------------------------------------- /src/slug/slug.ts: -------------------------------------------------------------------------------- 1 | import marpitPlugin from '@marp-team/marpit/plugin' 2 | import type { Marp } from '../marp' 3 | 4 | export type Slugifier = (text: string) => string 5 | export type PostSlugify = (slug: string, index: number) => string 6 | 7 | export type SlugOptions = boolean | Slugifier | SlugOptionsObject 8 | 9 | type SlugOptionsObject = { 10 | slugifier?: Slugifier 11 | postSlugify?: PostSlugify 12 | } 13 | 14 | const textTokenTypes = [ 15 | 'text', 16 | 'code_inline', 17 | 'image', 18 | 'html_inline', 19 | 'marp_emoji', 20 | 'marp_unicode_emoji', 21 | ] 22 | 23 | const defaultPostSlugify: PostSlugify = (slug, index) => 24 | index > 0 ? `${slug}-${index}` : slug 25 | 26 | const parseSlugOptions = ( 27 | options: SlugOptions, 28 | ): false | Required => { 29 | if (options === false) return false 30 | 31 | if (typeof options === 'function') { 32 | return { slugifier: options, postSlugify: defaultPostSlugify } 33 | } 34 | 35 | const defaultSlugOptions: Required = { 36 | slugifier: githubSlugify, 37 | postSlugify: defaultPostSlugify, 38 | } 39 | 40 | return options === true 41 | ? defaultSlugOptions 42 | : { ...defaultSlugOptions, ...options } 43 | } 44 | 45 | export const markdown = marpitPlugin((md) => { 46 | const marp: Marp = md.marpit 47 | 48 | md.core.ruler.push('marp_slug', (state) => { 49 | const opts = parseSlugOptions(marp.options.slug ?? true) 50 | if (!opts) return 51 | 52 | const slugs = new Map() 53 | 54 | for (const token of state.tokens) { 55 | if (token.type === 'marpit_slide_open') { 56 | const tokenId = token.attrGet('id') 57 | if (tokenId != null) slugs.set(tokenId, 0) 58 | } 59 | } 60 | 61 | let targetHeading 62 | let targetHeadingContents: any[] = [] 63 | 64 | for (const token of state.tokens) { 65 | if (!targetHeading && token.type === 'heading_open') { 66 | targetHeading = token 67 | targetHeadingContents = [] 68 | } else if (targetHeading) { 69 | if (token.type === 'heading_close') { 70 | let slug = token.attrGet('id') 71 | 72 | if (slug == null) { 73 | slug = opts.slugifier( 74 | targetHeadingContents 75 | .map((contentToken) => { 76 | if (contentToken.type === 'inline') { 77 | return contentToken.children 78 | .map((t) => { 79 | if (t.hidden) return '' 80 | if (textTokenTypes.includes(t.type)) return t.content 81 | 82 | return '' 83 | }) 84 | .join('') 85 | } 86 | 87 | return '' 88 | }) 89 | .join(''), 90 | ) 91 | } 92 | 93 | const index = slugs.has(slug) ? slugs.get(slug)! + 1 : 0 94 | targetHeading.attrSet('id', opts.postSlugify(slug, index)) 95 | 96 | slugs.set(slug, index) 97 | targetHeading = undefined 98 | } else if (!token.hidden) { 99 | targetHeadingContents.push(token) 100 | } 101 | } 102 | } 103 | }) 104 | }) 105 | 106 | // Convert given text to GitHub-style slug. This is compatible with Markdown language service. 107 | export const githubSlugify: Slugifier = (text: string): string => 108 | encodeURI( 109 | text 110 | .trim() 111 | .toLowerCase() 112 | .replace(/\s+/g, '-') 113 | .replace( 114 | /[\][!/'"#$%&()*+,./:;<=>?@\\^{|}~`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, 115 | '', 116 | ) 117 | .replace(/(?:^-+|-+$)/, ''), 118 | ) 119 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const scss: string 3 | export default scss 4 | } 5 | 6 | declare module 'katex/package.json' { 7 | export const version: string 8 | } 9 | -------------------------------------------------------------------------------- /test/__snapshots__/marp.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Marp math option with KaTeX injects KaTeX css with replacing web font URL to CDN: katex-css-cdn 1`] = ` 4 | [ 5 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_AMS-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_AMS-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_AMS-Regular.ttf') format("truetype")", 6 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Caligraphic-Bold.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Caligraphic-Bold.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Caligraphic-Bold.ttf') format("truetype")", 7 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Caligraphic-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Caligraphic-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Caligraphic-Regular.ttf') format("truetype")", 8 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Fraktur-Bold.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Fraktur-Bold.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Fraktur-Bold.ttf') format("truetype")", 9 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Fraktur-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Fraktur-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Fraktur-Regular.ttf') format("truetype")", 10 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Bold.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Bold.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Bold.ttf') format("truetype")", 11 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-BoldItalic.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-BoldItalic.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-BoldItalic.ttf') format("truetype")", 12 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Italic.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Italic.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Italic.ttf') format("truetype")", 13 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Main-Regular.ttf') format("truetype")", 14 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Math-BoldItalic.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Math-BoldItalic.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Math-BoldItalic.ttf') format("truetype")", 15 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Math-Italic.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Math-Italic.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Math-Italic.ttf') format("truetype")", 16 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Bold.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Bold.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Bold.ttf') format("truetype")", 17 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Italic.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Italic.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Italic.ttf') format("truetype")", 18 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_SansSerif-Regular.ttf') format("truetype")", 19 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Script-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Script-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Script-Regular.ttf') format("truetype")", 20 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size1-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size1-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size1-Regular.ttf') format("truetype")", 21 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size2-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size2-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size2-Regular.ttf') format("truetype")", 22 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size3-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size3-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size3-Regular.ttf') format("truetype")", 23 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size4-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size4-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Size4-Regular.ttf') format("truetype")", 24 | "url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Typewriter-Regular.woff2') format("woff2"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Typewriter-Regular.woff') format("woff"), url('https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/fonts/KaTeX_Typewriter-Regular.ttf') format("truetype")", 25 | ] 26 | `; 27 | 28 | exports[`Marp math option with KaTeX with katexFontPath as false does not replace KaTeX web font URL: katex-css-noops 1`] = ` 29 | [ 30 | "url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"), url(fonts/KaTeX_AMS-Regular.woff) format("woff"), url(fonts/KaTeX_AMS-Regular.ttf) format("truetype")", 31 | "url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"), url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"), url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype")", 32 | "url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"), url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"), url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype")", 33 | "url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"), url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"), url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype")", 34 | "url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"), url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"), url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype")", 35 | "url(fonts/KaTeX_Main-Bold.woff2) format("woff2"), url(fonts/KaTeX_Main-Bold.woff) format("woff"), url(fonts/KaTeX_Main-Bold.ttf) format("truetype")", 36 | "url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"), url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"), url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype")", 37 | "url(fonts/KaTeX_Main-Italic.woff2) format("woff2"), url(fonts/KaTeX_Main-Italic.woff) format("woff"), url(fonts/KaTeX_Main-Italic.ttf) format("truetype")", 38 | "url(fonts/KaTeX_Main-Regular.woff2) format("woff2"), url(fonts/KaTeX_Main-Regular.woff) format("woff"), url(fonts/KaTeX_Main-Regular.ttf) format("truetype")", 39 | "url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"), url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"), url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype")", 40 | "url(fonts/KaTeX_Math-Italic.woff2) format("woff2"), url(fonts/KaTeX_Math-Italic.woff) format("woff"), url(fonts/KaTeX_Math-Italic.ttf) format("truetype")", 41 | "url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"), url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"), url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype")", 42 | "url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"), url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"), url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype")", 43 | "url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"), url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"), url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype")", 44 | "url(fonts/KaTeX_Script-Regular.woff2) format("woff2"), url(fonts/KaTeX_Script-Regular.woff) format("woff"), url(fonts/KaTeX_Script-Regular.ttf) format("truetype")", 45 | "url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"), url(fonts/KaTeX_Size1-Regular.woff) format("woff"), url(fonts/KaTeX_Size1-Regular.ttf) format("truetype")", 46 | "url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"), url(fonts/KaTeX_Size2-Regular.woff) format("woff"), url(fonts/KaTeX_Size2-Regular.ttf) format("truetype")", 47 | "url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"), url(fonts/KaTeX_Size3-Regular.woff) format("woff"), url(fonts/KaTeX_Size3-Regular.ttf) format("truetype")", 48 | "url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"), url(fonts/KaTeX_Size4-Regular.woff) format("woff"), url(fonts/KaTeX_Size4-Regular.ttf) format("truetype")", 49 | "url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"), url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"), url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype")", 50 | ] 51 | `; 52 | 53 | exports[`Marp math option with KaTeX with katexFontPath replaces KaTeX web font URL with specified path: katex-css-replace 1`] = ` 54 | [ 55 | "url('/resources/fonts/KaTeX_AMS-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_AMS-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_AMS-Regular.ttf') format("truetype")", 56 | "url('/resources/fonts/KaTeX_Caligraphic-Bold.woff2') format("woff2"), url('/resources/fonts/KaTeX_Caligraphic-Bold.woff') format("woff"), url('/resources/fonts/KaTeX_Caligraphic-Bold.ttf') format("truetype")", 57 | "url('/resources/fonts/KaTeX_Caligraphic-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Caligraphic-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Caligraphic-Regular.ttf') format("truetype")", 58 | "url('/resources/fonts/KaTeX_Fraktur-Bold.woff2') format("woff2"), url('/resources/fonts/KaTeX_Fraktur-Bold.woff') format("woff"), url('/resources/fonts/KaTeX_Fraktur-Bold.ttf') format("truetype")", 59 | "url('/resources/fonts/KaTeX_Fraktur-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Fraktur-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Fraktur-Regular.ttf') format("truetype")", 60 | "url('/resources/fonts/KaTeX_Main-Bold.woff2') format("woff2"), url('/resources/fonts/KaTeX_Main-Bold.woff') format("woff"), url('/resources/fonts/KaTeX_Main-Bold.ttf') format("truetype")", 61 | "url('/resources/fonts/KaTeX_Main-BoldItalic.woff2') format("woff2"), url('/resources/fonts/KaTeX_Main-BoldItalic.woff') format("woff"), url('/resources/fonts/KaTeX_Main-BoldItalic.ttf') format("truetype")", 62 | "url('/resources/fonts/KaTeX_Main-Italic.woff2') format("woff2"), url('/resources/fonts/KaTeX_Main-Italic.woff') format("woff"), url('/resources/fonts/KaTeX_Main-Italic.ttf') format("truetype")", 63 | "url('/resources/fonts/KaTeX_Main-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Main-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Main-Regular.ttf') format("truetype")", 64 | "url('/resources/fonts/KaTeX_Math-BoldItalic.woff2') format("woff2"), url('/resources/fonts/KaTeX_Math-BoldItalic.woff') format("woff"), url('/resources/fonts/KaTeX_Math-BoldItalic.ttf') format("truetype")", 65 | "url('/resources/fonts/KaTeX_Math-Italic.woff2') format("woff2"), url('/resources/fonts/KaTeX_Math-Italic.woff') format("woff"), url('/resources/fonts/KaTeX_Math-Italic.ttf') format("truetype")", 66 | "url('/resources/fonts/KaTeX_SansSerif-Bold.woff2') format("woff2"), url('/resources/fonts/KaTeX_SansSerif-Bold.woff') format("woff"), url('/resources/fonts/KaTeX_SansSerif-Bold.ttf') format("truetype")", 67 | "url('/resources/fonts/KaTeX_SansSerif-Italic.woff2') format("woff2"), url('/resources/fonts/KaTeX_SansSerif-Italic.woff') format("woff"), url('/resources/fonts/KaTeX_SansSerif-Italic.ttf') format("truetype")", 68 | "url('/resources/fonts/KaTeX_SansSerif-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_SansSerif-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_SansSerif-Regular.ttf') format("truetype")", 69 | "url('/resources/fonts/KaTeX_Script-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Script-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Script-Regular.ttf') format("truetype")", 70 | "url('/resources/fonts/KaTeX_Size1-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Size1-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Size1-Regular.ttf') format("truetype")", 71 | "url('/resources/fonts/KaTeX_Size2-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Size2-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Size2-Regular.ttf') format("truetype")", 72 | "url('/resources/fonts/KaTeX_Size3-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Size3-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Size3-Regular.ttf') format("truetype")", 73 | "url('/resources/fonts/KaTeX_Size4-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Size4-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Size4-Regular.ttf') format("truetype")", 74 | "url('/resources/fonts/KaTeX_Typewriter-Regular.woff2') format("woff2"), url('/resources/fonts/KaTeX_Typewriter-Regular.woff') format("woff"), url('/resources/fonts/KaTeX_Typewriter-Regular.ttf') format("truetype")", 75 | ] 76 | `; 77 | -------------------------------------------------------------------------------- /test/_transformers/sass.js: -------------------------------------------------------------------------------- 1 | const { compile, compileAsync, NodePackageImporter } = require('sass') 2 | 3 | /** @type {import('sass').Options} */ 4 | const sassOptions = { 5 | importers: [new NodePackageImporter()], 6 | } 7 | 8 | const generateCode = (transformed) => ({ 9 | code: `module.exports = ${JSON.stringify(transformed)};`, 10 | }) 11 | 12 | module.exports = { 13 | processAsync: async (_, file) => { 14 | const transformed = await compileAsync(file, sassOptions) 15 | return generateCode(transformed.css) 16 | }, 17 | process: (_, file) => { 18 | const transformed = compile(file, sassOptions) 19 | return generateCode(transformed.css) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /test/browser.ts: -------------------------------------------------------------------------------- 1 | /** @jest-environment jsdom */ 2 | import { observe } from '@marp-team/marpit-svg-polyfill' 3 | import { browser, observer } from '../src/browser' 4 | import { applyCustomElements } from '../src/custom-elements/browser' 5 | 6 | const polyfillCleanup = jest.fn() 7 | 8 | jest.mock('@marp-team/marpit-svg-polyfill') 9 | jest.mock('../src/custom-elements/browser') 10 | 11 | beforeEach(() => { 12 | jest.clearAllMocks() 13 | ;(observe as jest.Mock).mockReturnValue(polyfillCleanup) 14 | }) 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | describe('Browser script', () => { 18 | it('executes polyfill observer and set-up for custom elements', () => { 19 | const browserInterface = browser() 20 | 21 | expect(observe).toHaveBeenCalledTimes(1) 22 | expect(applyCustomElements).toHaveBeenCalledTimes(1) 23 | 24 | expect(browser()).toStrictEqual(browserInterface) 25 | expect(browserInterface).toStrictEqual(expect.any(Function)) 26 | expect(browserInterface).toStrictEqual(browserInterface.cleanup) 27 | 28 | polyfillCleanup.mockClear() 29 | browserInterface.cleanup() 30 | expect(polyfillCleanup).toHaveBeenCalled() 31 | }) 32 | 33 | describe('with passed shadow root', () => { 34 | it('calls polyfill observer and custom elements set-up with specific target', () => { 35 | const root = document.createElement('div').attachShadow({ mode: 'open' }) 36 | const browserInterface = browser(root) 37 | 38 | expect(observe).toHaveBeenCalledWith(root) 39 | expect(applyCustomElements).toHaveBeenCalledWith(root) 40 | browserInterface.cleanup() 41 | }) 42 | }) 43 | 44 | describe('#update', () => { 45 | it('calls itself again to update custom element DOMs', () => { 46 | const browserInterface = browser() 47 | 48 | expect(applyCustomElements).toHaveBeenCalledTimes(1) 49 | expect(browserInterface.update()).toStrictEqual(browserInterface) 50 | expect(applyCustomElements).toHaveBeenCalledTimes(2) 51 | browserInterface.cleanup() 52 | 53 | // For shadow root 54 | const root = document.createElement('div').attachShadow({ mode: 'open' }) 55 | const interfaceShadowRoot = browser(root) 56 | 57 | expect(applyCustomElements).toHaveBeenNthCalledWith(3, root) 58 | expect(interfaceShadowRoot.update()).toStrictEqual(interfaceShadowRoot) 59 | expect(applyCustomElements).toHaveBeenNthCalledWith(4, root) 60 | interfaceShadowRoot.cleanup() 61 | }) 62 | }) 63 | }) 64 | 65 | describe('Observer', () => { 66 | describe('with once option', () => { 67 | it('does not call window.requestAnimationFrame', () => { 68 | const spy = jest.spyOn(window, 'requestAnimationFrame') 69 | const cleanup = observer({ once: true }) 70 | 71 | expect(spy).not.toHaveBeenCalled() 72 | cleanup() 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/custom-elements/browser.ts: -------------------------------------------------------------------------------- 1 | /** @jest-environment jsdom */ 2 | import * as browser from '../../src/custom-elements/browser/index' 3 | import { MarpAutoScaling } from '../../src/custom-elements/browser/marp-auto-scaling' 4 | import { elements } from '../../src/custom-elements/definitions' 5 | 6 | beforeAll(() => { 7 | window.ResizeObserver = jest.fn(function ( 8 | this: ResizeObserver, 9 | cb: ResizeObserverCallback, 10 | ) { 11 | this.observe = jest.fn((target) => { 12 | cb( 13 | [ 14 | { 15 | target, 16 | contentRect: { 17 | x: 0, 18 | y: 0, 19 | width: 100, 20 | height: 50, 21 | left: 0, 22 | top: 0, 23 | right: 100, 24 | bottom: 50, 25 | toJSON: () => ({}), 26 | }, 27 | // Sizes are not used because Safari doesn't support them 28 | borderBoxSize: [], 29 | contentBoxSize: [], 30 | devicePixelContentBoxSize: [], 31 | }, 32 | ], 33 | this, 34 | ) 35 | }) 36 | this.unobserve = jest.fn() 37 | this.disconnect = jest.fn() 38 | }) as any 39 | }) 40 | 41 | afterEach(() => { 42 | window[browser.marpCustomElementsRegisteredSymbol] = false 43 | 44 | // Reset custom elements defined in JSDOM 45 | const [implSymbol] = Object.getOwnPropertySymbols(customElements) 46 | const impl: any = customElements[implSymbol] 47 | impl._customElementDefinitions = [] 48 | impl._whenDefinedPromiseMap = Object.create(null) 49 | }) 50 | 51 | describe('The hydration script for custom elements', () => { 52 | describe('#applyCustomElements', () => { 53 | it('defines custom elements', () => { 54 | const elms = [ 55 | ...Object.keys(elements).map((el) => `marp-${el}`), 56 | 'marp-auto-scaling', 57 | ] 58 | 59 | for (const el of elms) { 60 | expect(customElements.get(el)).toBeUndefined() 61 | } 62 | 63 | browser.applyCustomElements() 64 | 65 | for (const el of elms) { 66 | const customElm = customElements.get(el) as CustomElementConstructor 67 | 68 | expect(customElm).toBeTruthy() 69 | expect(new customElm() instanceof HTMLElement).toBe(true) 70 | } 71 | 72 | // If applied twice, it should not define new elements 73 | browser.applyCustomElements() 74 | }) 75 | 76 | it('replaces
 to ', () => {
 77 |       document.body.innerHTML =
 78 |         '
1
2
' 79 | 80 | browser.applyCustomElements() 81 | 82 | expect(document.body.innerHTML).toMatchInlineSnapshot( 83 | `"12"`, 84 | ) 85 | }) 86 | 87 | it('does not throw known DOMException error while upgrading
 to  (for Firefox)', () => {
 88 |       document.body.innerHTML = '
1
' 89 | 90 | jest 91 | .spyOn(HTMLElement.prototype, 'attachShadow') 92 | .mockImplementationOnce(() => { 93 | throw new DOMException( 94 | 'Element.attachShadow: Unable to attach ShadowDOM', 95 | 'NotSupportedError', 96 | ) 97 | }) 98 | 99 | expect(() => browser.applyCustomElements()).not.toThrow() 100 | }) 101 | 102 | it.skip('throws error if unknown error occured while upgrading
 to ', () => {
103 |       document.body.innerHTML = '
1
' 104 | 105 | jest.spyOn(console, 'error').mockImplementation(() => {}) 106 | jest 107 | .spyOn(HTMLElement.prototype, 'attachShadow') 108 | .mockImplementationOnce(() => { 109 | throw new Error('Unknown error while attaching shadow') 110 | }) 111 | 112 | expect(() => browser.applyCustomElements()).toThrow() 113 | }) 114 | 115 | it('does not replace

to ', () => { 116 | const html = '

test

' 117 | document.body.innerHTML = html 118 | 119 | browser.applyCustomElements() 120 | expect(document.body.innerHTML).toBe(html) 121 | }) 122 | 123 | describe('when the browser is not supported "is" attribute for customized built-in elements', () => { 124 | beforeEach(() => { 125 | jest 126 | .spyOn(browser, 'isSupportedCustomizedBuiltInElements') 127 | .mockReturnValue(false) 128 | }) 129 | 130 | afterEach(() => { 131 | ;( 132 | browser.isSupportedCustomizedBuiltInElements as jest.Mock 133 | ).mockRestore() 134 | }) 135 | 136 | it('replaces all of elements that are using "is" attribute to the standalone custom element', () => { 137 | document.body.innerHTML = Object.keys(elements) 138 | .map((elm) => `<${elm} is="marp-${elm}">`) 139 | .join('\n') 140 | 141 | browser.applyCustomElements() 142 | expect(document.body.innerHTML).toMatchInlineSnapshot(` 143 | " 144 | 145 | 146 | 147 | 148 | 149 | 150 | " 151 | `) 152 | }) 153 | }) 154 | }) 155 | 156 | describe('Customized built-in elements', () => { 157 | it('uses custom element if set data-auto-scaling attr', () => { 158 | document.body.innerHTML = '

test

' 159 | browser.applyCustomElements() 160 | 161 | const h1 = document.body.firstElementChild as HTMLElement 162 | expect(h1.shadowRoot?.querySelector('marp-auto-scaling')).toBeTruthy() 163 | 164 | // Track the change of attribute 165 | h1.removeAttribute('data-auto-scaling') 166 | expect(h1.shadowRoot?.querySelector('marp-auto-scaling')).toBeFalsy() 167 | 168 | h1.setAttribute('data-auto-scaling', 'downscale-only') 169 | expect( 170 | h1.shadowRoot?.querySelector('marp-auto-scaling[data-downscale-only]'), 171 | ).toBeTruthy() 172 | }) 173 | }) 174 | 175 | describe('', () => { 176 | it("applies the size of contents to SVG's viewbox", () => { 177 | browser.applyCustomElements() 178 | 179 | document.body.innerHTML = 'test' 180 | 181 | const autoScaling = document.querySelector( 182 | 'marp-auto-scaling', 183 | ) as MarpAutoScaling 184 | const svg = autoScaling.shadowRoot.querySelector('svg') as SVGElement 185 | 186 | expect(svg.getAttribute('viewBox')).toBe('0 0 100 50') 187 | }) 188 | 189 | it('sets the correct alignment by text-align style inherited from the parent', () => { 190 | const getComputedStyle = jest.spyOn(window, 'getComputedStyle') 191 | browser.applyCustomElements() 192 | 193 | const getElementsWithStyle = ( 194 | style: 195 | | Record 196 | | { getPropertyValue?: (prop: string) => string | undefined }, 197 | ) => { 198 | getComputedStyle.mockImplementationOnce(() => ({ 199 | getPropertyValue: () => undefined, 200 | ...style, 201 | })) 202 | 203 | document.body.innerHTML = 'test' 204 | 205 | const el = document.querySelector( 206 | 'marp-auto-scaling', 207 | ) as MarpAutoScaling 208 | const svg = el.shadowRoot?.querySelector('svg') as SVGElement 209 | const container = svg.querySelector( 210 | '[data-marp-auto-scaling-container]', 211 | ) as HTMLElement 212 | 213 | return { el, svg, container } 214 | } 215 | 216 | // Left-aligned 217 | const leftAligned = getElementsWithStyle({ textAlign: 'left' }) 218 | 219 | expect(leftAligned.svg.getAttribute('preserveAspectRatio')).toBe( 220 | 'xMinYMid meet', 221 | ) 222 | expect(leftAligned.container.style.marginRight).toBe('auto') 223 | expect(leftAligned.container.style.marginLeft).toBe('0px') 224 | 225 | // Center-aligned 226 | const centerAligned = getElementsWithStyle({ textAlign: 'center' }) 227 | 228 | expect(centerAligned.svg.getAttribute('preserveAspectRatio')).toBe( 229 | 'xMidYMid meet', 230 | ) 231 | expect(centerAligned.container.style.marginRight).toBe('auto') 232 | expect(centerAligned.container.style.marginLeft).toBe('auto') 233 | 234 | // Right-aligned 235 | const rightAligned = getElementsWithStyle({ textAlign: 'right' }) 236 | 237 | expect(rightAligned.svg.getAttribute('preserveAspectRatio')).toBe( 238 | 'xMaxYMid meet', 239 | ) 240 | expect(rightAligned.container.style.marginRight).toBe('0px') 241 | expect(rightAligned.container.style.marginLeft).toBe('auto') 242 | 243 | // Logical alignment 244 | const startAligned = getElementsWithStyle({ textAlign: 'start' }) 245 | 246 | expect(startAligned.svg.getAttribute('preserveAspectRatio')).toBe( 247 | 'xMinYMid meet', 248 | ) 249 | expect(startAligned.container.style.marginRight).toBe('auto') 250 | expect(startAligned.container.style.marginLeft).toBe('0px') 251 | 252 | const startAlignedRtl = getElementsWithStyle({ 253 | textAlign: 'start', 254 | direction: 'rtl', 255 | }) 256 | 257 | expect(startAlignedRtl.svg.getAttribute('preserveAspectRatio')).toBe( 258 | 'xMaxYMid meet', 259 | ) 260 | expect(startAlignedRtl.container.style.marginRight).toBe('0px') 261 | expect(startAlignedRtl.container.style.marginLeft).toBe('auto') 262 | 263 | const endAligned = getElementsWithStyle({ 264 | textAlign: 'end', 265 | }) 266 | 267 | expect(endAligned.svg.getAttribute('preserveAspectRatio')).toBe( 268 | 'xMaxYMid meet', 269 | ) 270 | expect(endAligned.container.style.marginRight).toBe('0px') 271 | expect(endAligned.container.style.marginLeft).toBe('auto') 272 | 273 | const endAlignedRtl = getElementsWithStyle({ 274 | textAlign: 'end', 275 | direction: 'rtl', 276 | }) 277 | 278 | expect(endAlignedRtl.svg.getAttribute('preserveAspectRatio')).toBe( 279 | 'xMinYMid meet', 280 | ) 281 | expect(endAlignedRtl.container.style.marginRight).toBe('auto') 282 | expect(endAlignedRtl.container.style.marginLeft).toBe('0px') 283 | 284 | // Overloading preserveAspectRatio by CSS custom property 285 | const overloaded = getElementsWithStyle({ 286 | textAlign: 'left', 287 | getPropertyValue: (prop: string) => { 288 | if (prop === '--preserve-aspect-ratio') return 'xMaxYMid meet' 289 | return undefined 290 | }, 291 | }) 292 | 293 | expect(overloaded.svg.getAttribute('preserveAspectRatio')).toBe( 294 | 'xMaxYMid meet', 295 | ) 296 | expect(overloaded.container.style.marginRight).toBe('0px') 297 | expect(overloaded.container.style.marginLeft).toBe('auto') 298 | }) 299 | 300 | describe('Rendering workaround for Chromium 105+', () => { 301 | const waitNextRendering = () => 302 | new Promise((resolve) => requestAnimationFrame(() => resolve())) 303 | 304 | it("flushes SVG's display style when resized", async () => { 305 | expect.hasAssertions() 306 | 307 | browser.applyCustomElements() 308 | document.body.innerHTML = 'test' 309 | 310 | const autoScaling = document.querySelector( 311 | 'marp-auto-scaling', 312 | ) as MarpAutoScaling 313 | const svg = autoScaling.shadowRoot.querySelector('svg') as SVGElement 314 | 315 | // display style sets as `inline` by an initial callback of ResizeObserver 316 | // (If not yet rendered DOM, running callback would be delayed until the component was painted) 317 | expect(svg.style.display).toBe('inline') 318 | 319 | // After that, display style is reverted to empty string 320 | await waitNextRendering() 321 | expect(svg.style.display).toBe('') 322 | }) 323 | }) 324 | }) 325 | }) 326 | -------------------------------------------------------------------------------- /test/math/__snapshots__/katex.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`markdown-it math plugin for KaTeX allows to place text immediately after inline math 1`] = ` 4 | "

nn-th order

5 | " 6 | `; 7 | 8 | exports[`markdown-it math plugin for KaTeX can appear both maths in lists 1`] = ` 9 | "
    10 |
  • 1+1=21+1 = 2
  • 11 |
  • 12 |

    1+1=21+1 = 2 13 |

  • 14 |
15 | " 16 | `; 17 | 18 | exports[`markdown-it math plugin for KaTeX does not allow paragraph break in inline math 1`] = ` 19 | "

foo $1+1

20 |

= 2$ bar

21 | " 22 | `; 23 | 24 | exports[`markdown-it math plugin for KaTeX does not process apparent markup in inline math 1`] = ` 25 | "

foo 1i11 *i* 1 bar

26 | " 27 | `; 28 | 29 | exports[`markdown-it math plugin for KaTeX does not recognize inline block math 1`] = ` 30 | "

It's well know that $$1 + 1 = 3$$ for sufficiently large 1.

31 | " 32 | `; 33 | 34 | exports[`markdown-it math plugin for KaTeX does not render block math with indented up to 4 spaces (code block) 1`] = ` 35 | "
$$
 36 | 1+1 = 2
 37 | $$
 38 | 
39 | " 40 | `; 41 | 42 | exports[`markdown-it math plugin for KaTeX does not render math when delimiters are escaped 1`] = ` 43 | "

Foo $1$ bar 44 | $$ 45 | 1 46 | $$

47 | " 48 | `; 49 | 50 | exports[`markdown-it math plugin for KaTeX does not render math when numbers are followed closing inline math 1`] = ` 51 | "

Thus, $20,000 and USD$30,000 won't parse as math.

52 | " 53 | `; 54 | 55 | exports[`markdown-it math plugin for KaTeX does not render with empty content 1`] = ` 56 | "

aaa $$ bbb

57 | " 58 | `; 59 | 60 | exports[`markdown-it math plugin for KaTeX recognizes escaped delimiters in math mode 1`] = ` 61 | "

Money adds: $X+$Y=$Z\\$X + \\$Y = \\$Z.

62 | " 63 | `; 64 | 65 | exports[`markdown-it math plugin for KaTeX recognizes multiline escaped delimiters in math module 1`] = ` 66 | "

Weird-o: ($1$)\\displaystyle{\\begin{pmatrix} \\$ & 1\\\\\\$ \\end{pmatrix}}.

67 | " 68 | `; 69 | 70 | exports[`markdown-it math plugin for KaTeX renders block math composed multiple lines with starting/ending expression on delimited lines 1`] = ` 71 | "

[[1,2][3,4]][ 72 | [1, 2] 73 | [3, 4] 74 | ]

" 75 | `; 76 | 77 | exports[`markdown-it math plugin for KaTeX renders block math with indented up to 3 spaces 1`] = ` 78 | "

1+1=21+1 = 2 79 |

" 80 | `; 81 | 82 | exports[`markdown-it math plugin for KaTeX renders block math with self-closing at the end of document 1`] = `"

1+1=21+1 = 2

"`; 83 | 84 | exports[`markdown-it math plugin for KaTeX renders block math written in one line 1`] = ` 85 | "

1+1=21+1 = 2 86 |

" 87 | `; 88 | 89 | exports[`markdown-it math plugin for KaTeX renders math even when it starts with a negative sign 1`] = ` 90 | "

foo1+1=0-1+1 = 0bar

91 | " 92 | `; 93 | 94 | exports[`markdown-it math plugin for KaTeX renders math without whitespace before and after delimiter 1`] = ` 95 | "

foo1+1=21+1 = 2bar

96 | " 97 | `; 98 | 99 | exports[`markdown-it math plugin for KaTeX renders multiline display math 1`] = ` 100 | "

1+1=2 101 | 1 102 | + 1 103 | 104 | = 2 105 | 106 |

" 107 | `; 108 | 109 | exports[`markdown-it math plugin for KaTeX renders multiline inline math 1`] = ` 110 | "

foo 1+1=21 + 1 111 | = 2 bar

112 | " 113 | `; 114 | 115 | exports[`markdown-it math plugin for KaTeX renders simple block math 1`] = ` 116 | "

1+1=21+1 = 2 117 |

" 118 | `; 119 | 120 | exports[`markdown-it math plugin for KaTeX renders simple inline math 1`] = ` 121 | "

1+1=21+1 = 2

122 | " 123 | `; 124 | 125 | exports[`markdown-it math plugin for KaTeX requires a closing delimiter to render math 1`] = ` 126 | "

aaa $5.99 bbb

127 | " 128 | `; 129 | 130 | exports[`markdown-it math plugin for KaTeX requires non whitespace to left of closing inline math 1`] = ` 131 | "

I will give you $20 today, if you give me more $ tomorrow.

132 | " 133 | `; 134 | 135 | exports[`markdown-it math plugin for KaTeX requires non whitespace to right of opening inline math 1`] = ` 136 | "

For some Europeans, it is 2$ for a can of soda, not 1$.

137 | " 138 | `; 139 | -------------------------------------------------------------------------------- /test/math/katex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * All test cases are ported from markdown-it-katex. 3 | * 4 | * @see https://github.com/waylonflinn/markdown-it-katex/blob/master/test/fixtures/default.txt 5 | */ 6 | 7 | import MarkdownIt from 'markdown-it' 8 | import { markdown as mathPlugin } from '../../src/math/math' 9 | 10 | const countMath = (stt) => stt.split('class="katex"').length - 1 11 | const countBlockMath = (stt) => stt.split('class="katex-display"').length - 1 12 | 13 | describe('markdown-it math plugin for KaTeX', () => { 14 | const md = new MarkdownIt() 15 | 16 | md.marpit = { options: { math: 'katex' } } 17 | 18 | // Mock Marpit instance 19 | md.use(() => { 20 | Object.assign(md.marpit, { 21 | customDirectives: { global: {} }, 22 | lastGlobalDirectives: {}, 23 | }) 24 | md.core.ruler.push('marpit_directives_global_parse', () => { 25 | // no ops 26 | }) 27 | }) 28 | md.use(mathPlugin) 29 | 30 | it('renders simple inline math', () => { 31 | const rendered = md.render('$1+1 = 2$') 32 | expect(countMath(rendered)).toBe(1) 33 | expect(rendered).toMatchSnapshot() 34 | }) 35 | 36 | it('renders simple block math', () => { 37 | const rendered = md.render('$$1+1 = 2$$') 38 | expect(countBlockMath(rendered)).toBe(1) 39 | expect(rendered).toMatchSnapshot() 40 | }) 41 | 42 | it('renders math without whitespace before and after delimiter', () => { 43 | const rendered = md.render('foo$1+1 = 2$bar') 44 | expect(countMath(rendered)).toBe(1) 45 | expect(rendered).toMatchSnapshot() 46 | }) 47 | 48 | it('renders math even when it starts with a negative sign', () => { 49 | const rendered = md.render('foo$-1+1 = 0$bar') 50 | expect(countMath(rendered)).toBe(1) 51 | expect(rendered).toMatchSnapshot() 52 | }) 53 | 54 | it('does not render with empty content', () => { 55 | const rendered = md.render('aaa $$ bbb') 56 | expect(countMath(rendered)).toBe(0) 57 | expect(rendered).toMatchSnapshot() 58 | }) 59 | 60 | it('requires a closing delimiter to render math', () => { 61 | const rendered = md.render('aaa $5.99 bbb') 62 | expect(countMath(rendered)).toBe(0) 63 | expect(rendered).toMatchSnapshot() 64 | }) 65 | 66 | it('does not allow paragraph break in inline math', () => { 67 | const rendered = md.render('foo $1+1\n\n= 2$ bar') 68 | expect(countMath(rendered)).toBe(0) 69 | expect(rendered).toMatchSnapshot() 70 | }) 71 | 72 | it('does not process apparent markup in inline math', () => { 73 | const rendered = md.render('foo $1 *i* 1$ bar') 74 | expect(countMath(rendered)).toBe(1) 75 | expect(rendered).not.toContain('') 76 | expect(rendered).toMatchSnapshot() 77 | }) 78 | 79 | it('renders block math with indented up to 3 spaces', () => { 80 | const rendered = md.render(' $$\n 1+1 = 2\n $$') 81 | expect(countBlockMath(rendered)).toBe(1) 82 | expect(rendered).toMatchSnapshot() 83 | }) 84 | 85 | it('does not render block math with indented up to 4 spaces (code block)', () => { 86 | const rendered = md.render(' $$\n 1+1 = 2\n $$') 87 | expect(countBlockMath(rendered)).toBe(0) 88 | expect(rendered).toContain('') 89 | expect(rendered).toMatchSnapshot() 90 | }) 91 | 92 | it('renders multiline inline math', () => { 93 | const rendered = md.render('foo $1 + 1\n= 2$ bar') 94 | expect(countMath(rendered)).toBe(1) 95 | expect(rendered).toMatchSnapshot() 96 | }) 97 | 98 | it('renders multiline display math', () => { 99 | const rendered = md.render('$$\n\n 1\n+ 1\n\n= 2\n\n$$') 100 | expect(countBlockMath(rendered)).toBe(1) 101 | expect(rendered).toMatchSnapshot() 102 | }) 103 | 104 | it('allows to place text immediately after inline math', () => { 105 | const rendered = md.render('$n$-th order') 106 | expect(countMath(rendered)).toBe(1) 107 | expect(rendered).toMatchSnapshot() 108 | }) 109 | 110 | it('renders block math with self-closing at the end of document', () => { 111 | const rendered = md.render('$$\n1+1 = 2') 112 | expect(countBlockMath(rendered)).toBe(1) 113 | expect(rendered).toMatchSnapshot() 114 | }) 115 | 116 | it('can appear both maths in lists', () => { 117 | const rendered = md.render('* $1+1 = 2$\n* $$\n 1+1 = 2\n $$') 118 | expect(countMath(rendered)).toBe(2) 119 | expect(countBlockMath(rendered)).toBe(1) 120 | expect(rendered).toMatchSnapshot() 121 | }) 122 | 123 | it('renders block math written in one line', () => { 124 | const rendered = md.render('$$1+1 = 2$$') 125 | expect(countBlockMath(rendered)).toBe(1) 126 | expect(rendered).toMatchSnapshot() 127 | }) 128 | 129 | it('renders block math composed multiple lines with starting/ending expression on delimited lines', () => { 130 | const rendered = md.render('$$[\n[1, 2]\n[3, 4]\n]$$') 131 | expect(countBlockMath(rendered)).toBe(1) 132 | expect(rendered).toMatchSnapshot() 133 | }) 134 | 135 | it('does not render math when delimiters are escaped', () => { 136 | const rendered = md.render('Foo \\$1$ bar\n\\$\\$\n1\n\\$\\$') 137 | expect(countMath(rendered)).toBe(0) 138 | expect(rendered).toMatchSnapshot() 139 | }) 140 | 141 | it('does not render math when numbers are followed closing inline math', () => { 142 | const rendered = md.render( 143 | "Thus, $20,000 and USD$30,000 won't parse as math.", 144 | ) 145 | expect(countMath(rendered)).toBe(0) 146 | expect(rendered).toMatchSnapshot() 147 | }) 148 | 149 | it('requires non whitespace to right of opening inline math', () => { 150 | const rendered = md.render( 151 | 'For some Europeans, it is 2$ for a can of soda, not 1$.', 152 | ) 153 | expect(countMath(rendered)).toBe(0) 154 | expect(rendered).toMatchSnapshot() 155 | }) 156 | 157 | it('requires non whitespace to left of closing inline math', () => { 158 | const rendered = md.render( 159 | 'I will give you $20 today, if you give me more $ tomorrow.', 160 | ) 161 | expect(countMath(rendered)).toBe(0) 162 | expect(rendered).toMatchSnapshot() 163 | }) 164 | 165 | it('does not recognize inline block math', () => { 166 | const rendered = md.render( 167 | "It's well know that $$1 + 1 = 3$$ for sufficiently large 1.", 168 | ) 169 | expect(countMath(rendered)).toBe(0) 170 | expect(rendered).toMatchSnapshot() 171 | }) 172 | 173 | it('recognizes escaped delimiters in math mode', () => { 174 | const rendered = md.render('Money adds: $\\$X + \\$Y = \\$Z$.') 175 | expect(countMath(rendered)).toBe(1) 176 | expect(rendered).toMatchSnapshot() 177 | }) 178 | 179 | it('recognizes multiline escaped delimiters in math module', () => { 180 | const rendered = md.render( 181 | 'Weird-o: $\\displaystyle{\\begin{pmatrix} \\$ & 1\\\\\\$ \\end{pmatrix}}$.', 182 | ) 183 | expect(countMath(rendered)).toBe(1) 184 | expect(rendered).toMatchSnapshot() 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /test/size/size.ts: -------------------------------------------------------------------------------- 1 | import { Marpit, Options, Theme } from '@marp-team/marpit' 2 | import { load } from 'cheerio' 3 | import postcss, { AtRule, Rule } from 'postcss' 4 | import { markdown as sizePlugin } from '../../src/size/size' 5 | 6 | interface CollectedDecls { 7 | [k: string]: string | CollectedDecls 8 | } 9 | 10 | const metaType = { size: Array } 11 | 12 | describe('Size plugin', () => { 13 | const marpit = ( 14 | callback: (marpit: Marpit) => void = () => {}, 15 | opts?: Options, 16 | ) => 17 | new Marpit(opts).use(sizePlugin).use(({ marpit }) => { 18 | marpit.themeSet.metaType = metaType 19 | callback(marpit) 20 | }) 21 | 22 | const collectDecls = async ( 23 | css: string, 24 | selector = 'div.marpit > section', 25 | ) => { 26 | const collectedDecls: CollectedDecls = {} 27 | 28 | await postcss([ 29 | { 30 | postcssPlugin: 'postcss-collect-decl-walker', 31 | Root: (root) => { 32 | const collect = (rule: Rule | AtRule, to: CollectedDecls) => 33 | rule.walkDecls(({ prop, value }) => { 34 | to[prop] = value 35 | }) 36 | 37 | root.walkRules(selector, (rule) => collect(rule, collectedDecls)) 38 | root.walkAtRules((atRule) => { 39 | const name = `@${atRule.name}` 40 | const current = collectedDecls[name] 41 | const obj = 42 | typeof current === 'object' ? current : ({} as CollectedDecls) 43 | 44 | collectedDecls[name] = obj 45 | collect(atRule, obj) 46 | }) 47 | }, 48 | }, 49 | ]).process(css, { from: undefined }) 50 | 51 | return collectedDecls 52 | } 53 | 54 | it('defines size custom global directive', () => 55 | expect(marpit().customDirectives.global.size).toBeTruthy()) 56 | 57 | describe('when specified theme has theme metadata', () => { 58 | const initializeTheme = (m) => { 59 | m.themeSet.add('/* @theme a *//* @size test 640px 480px */') 60 | m.themeSet.add( 61 | '/* @theme b *//* @size test2 800px 600px */\n@import "a";', 62 | ) 63 | m.themeSet.add('/* @theme c *//* @size test 6px 4px */\n@import "a";') 64 | m.themeSet.add( 65 | '/* @theme d *//* @size test false *//* @size test2 - invalid defintion */\n@import "b";', 66 | ) 67 | } 68 | 69 | const instance = marpit(initializeTheme) 70 | const inlineSVGinstance = marpit(initializeTheme, { inlineSVG: true }) 71 | 72 | it('adds width and height style for section and @page rule', async () => { 73 | const { css } = instance.render('\n') 74 | expect(css).not.toBe(instance.render('').css) 75 | 76 | const decls = await collectDecls(css) 77 | expect(decls.width).toBe('640px') 78 | expect(decls.height).toBe('480px') 79 | expect(decls['@page']).toHaveProperty('size', '640px 480px') 80 | }) 81 | 82 | it('reverts manipulated theme after rendering', () => { 83 | const baseWidth = instance.themeSet.getThemeProp('', 'width') 84 | const baseHeight = instance.themeSet.getThemeProp('', 'height') 85 | 86 | instance.render('\n') 87 | 88 | expect(instance.themeSet.getThemeProp('a', 'width')).toBe(baseWidth) 89 | expect(instance.themeSet.getThemeProp('a', 'height')).toBe(baseHeight) 90 | }) 91 | 92 | it('exposes selected size into
element as data-size attribute', () => { 93 | const { html } = instance.render('\n\n---') 94 | const $ = load(html) 95 | const attrs = $('section') 96 | .map((_, e) => $(e).data('size')) 97 | .get() 98 | 99 | expect(attrs).toStrictEqual(['test', 'test']) 100 | 101 | // Apply data attribute to each layers of advanced background in inline SVG mode 102 | const { html: htmlAdv } = inlineSVGinstance.render( 103 | '\n\n![bg](dummy)', 104 | ) 105 | const $adv = load(htmlAdv) 106 | const attrsAdv = $adv('section') 107 | .map((_, e) => $adv(e).data('size')) 108 | .get() 109 | 110 | expect(attrsAdv).toStrictEqual(['test', 'test', 'test']) 111 | }) 112 | 113 | it('ignores undefined size name', () => { 114 | const { css } = instance.render('\n') 115 | expect(css).toBe(instance.render('').css) 116 | }) 117 | 118 | it('does not expose undefined size as data-size attribute', () => { 119 | const { html } = instance.render('') 120 | const $ = load(html) 121 | 122 | expect($('section').data('size')).toBeUndefined() 123 | }) 124 | 125 | it('ignores invalid size directive', () => { 126 | const { css } = instance.render( 127 | '\n', 128 | ) 129 | expect(css).toBe(instance.render('').css) 130 | }) 131 | 132 | it('allows using defined size in imported theme', async () => { 133 | const { css } = instance.render('\n') 134 | const decls = await collectDecls(css) 135 | 136 | expect(decls.width).toBe('640px') 137 | expect(decls.height).toBe('480px') 138 | expect(decls['@page']).toHaveProperty('size', '640px 480px') 139 | }) 140 | 141 | it('can override defined size in inherited theme', async () => { 142 | const { css } = instance.render('\n') 143 | const decls = await collectDecls(css) 144 | 145 | expect(decls.width).toBe('6px') 146 | expect(decls.height).toBe('4px') 147 | expect(decls['@page']).toHaveProperty('size', '6px 4px') 148 | }) 149 | 150 | it('can disable defined size in inherited theme by `@size [name] false`', async () => { 151 | const { css } = instance.render('\n') 152 | expect(css).toBe(instance.render('').css) 153 | }) 154 | }) 155 | 156 | describe('when default theme has size metadata', () => { 157 | const defaultCSS = '/* @theme a *//* @size test 640px 480px */' 158 | const defaultTheme = Theme.fromCSS(defaultCSS, { metaType }) 159 | 160 | const instance = marpit((m) => { 161 | m.themeSet.default = defaultTheme 162 | }) 163 | 164 | it('adds width and height style for section', async () => { 165 | const { css } = instance.render('') 166 | const { width, height } = await collectDecls(css) 167 | 168 | expect(width).toBe('640px') 169 | expect(height).toBe('480px') 170 | }) 171 | 172 | it('reverts manipulated theme after rendering', () => { 173 | instance.render('') 174 | 175 | const defaultTheme = instance.themeSet.default! 176 | 177 | expect(defaultTheme.css).toBe(defaultCSS) 178 | expect(defaultTheme.width).toBeUndefined() 179 | expect(defaultTheme.height).toBeUndefined() 180 | }) 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /themes/README.md: -------------------------------------------------------------------------------- 1 | # Marp Core built-in themes 2 | 3 | We provide some nice built-in themes in Marp Core. You can choose a favorite theme by using [Marpit `theme` directive](https://marpit.marp.app/directives?id=theme) in your Markdown. 4 | 5 | 6 | 7 | [example]: example.md 8 | 9 | ### Common feature 10 | 11 | These can use in the all of built-in themes. 12 | 13 | #### 4:3 slide 14 | 15 | We have `4:3` slide size preset (`960x720`) for a traditional presentation. 16 | 17 | ```markdown 18 | 19 | ``` 20 | 21 | #### `invert` class 22 | 23 | By using `invert` class, you can change to use the inverted color scheme. 24 | 25 | ```markdown 26 | 27 | ``` 28 | 29 | --- 30 | 31 | ## Default 32 | 33 | [![](https://user-images.githubusercontent.com/3993388/48039490-53be1b80-e1b8-11e8-8179-0e6c11d285e2.png)][example] 34 | [![invert](https://user-images.githubusercontent.com/3993388/48039492-5456b200-e1b8-11e8-9975-c9e4029d9036.png)][example] 35 | 36 | The default theme of Marp. It is based on [GitHub markdown style](https://github.com/sindresorhus/github-markdown-css), but optimized to the slide deck. Slide contents will be always vertically centered. 37 | 38 | ```markdown 39 | 40 | ``` 41 | 42 | ### Custom color (CSS variables) 43 | 44 | The default theme has followed GitHub style provided by [`github-markdown-css` package](https://github.com/sindresorhus/github-markdown-css), and the most of CSS variables are defined in the upstream. [Please refer to the source code of that to inspect appliable variables.](https://github.com/sindresorhus/github-markdown-css/blob/main/github-markdown.css) 45 | 46 | ```html 47 | 54 | ``` 55 | 56 | [We also have a little of additional variables to set colors for Marp specifics.](./default.scss) 57 | 58 | ## Gaia 59 | 60 | [![](https://user-images.githubusercontent.com/3993388/48039493-5456b200-e1b8-11e8-9c49-dd5d66d76c0d.png)][example] 61 | [![invert](https://user-images.githubusercontent.com/3993388/48039494-5456b200-e1b8-11e8-8bb5-f4a250e902e1.png)][example] 62 | 63 | Gaia theme is based on the classic design of [yhatt/marp](https://github.com/yhatt/marp). 64 | 65 | Originally, this theme was created for a maintainer to use, and it's inspired from [azusa-colors](https://github.com/sanographix/azusa-colors/) keynote template. 66 | 67 | ```markdown 68 | 69 | ``` 70 | 71 | ### Features 72 | 73 | #### `lead` class 74 | 75 | ![lead](https://user-images.githubusercontent.com/3993388/48040058-c62ffb00-e1ba-11e8-876d-c182a30714c6.png) 76 | 77 | Contents of the slide will align to left-top by Gaia's default. But you can use `lead` class to be centering like [uncover theme](#uncover). It is useful for the leading page like a title slide. 78 | 79 | ```markdown 80 | 84 | ``` 85 | 86 | > :information_source: Marpit's [scoped local directive](https://marpit.marp.app/directives?id=apply-to-a-single-page-spot-directives) would be useful to apply `lead` class only into a current page. 87 | > 88 | > ```markdown 89 | > 90 | > ``` 91 | 92 | #### Color scheme 93 | 94 | ![gaia](https://user-images.githubusercontent.com/3993388/48040059-c62ffb00-e1ba-11e8-8026-fa3511844ec7.png) 95 | 96 | Gaia theme supports an additional color scheme by `gaia` class. 97 | 98 | ```markdown 99 | 100 | ``` 101 | 102 | > :information_source: You may use multiple classes, by YAML array or separated string by space. 103 | > 104 | > ```markdown 105 | > --- 106 | > theme: gaia 107 | > class: 108 | > - lead 109 | > - invert 110 | > --- 111 | > 112 | > # Lead + invert 113 | > 114 | > --- 115 | > 116 | > 117 | > 118 | > # Lead + gaia 119 | > ``` 120 | 121 | ### Custom color (CSS variables) 122 | 123 | Color scheme for Gaia theme has defined by CSS variables. You also can use the custom color scheme by inline style. 124 | 125 | ```html 126 | 134 | ``` 135 | 136 | ## Uncover 137 | 138 | [![](https://user-images.githubusercontent.com/3993388/48039495-5456b200-e1b8-11e8-8c82-ca7f7842b34d.png)][example] 139 | [![invert](https://user-images.githubusercontent.com/3993388/48039496-54ef4880-e1b8-11e8-9c22-f3309b101e3c.png)][example] 140 | 141 | Uncover theme has three design concepts: simple, minimal, and modern. It's inspired from many slide deck frameworks, especially [reveal.js](https://revealjs.com/). 142 | 143 | ```markdown 144 | 145 | ``` 146 | 147 | ### Custom color (CSS variables) 148 | 149 | Color scheme for Uncover theme has defined by CSS variables. You also can use the custom color scheme by inline style. 150 | 151 | ```html 152 | 165 | ``` 166 | 167 | # Metadata for additional features 168 | 169 | Marp Core's extended theming system will recognize the metadata to be able to enable extra features whose a side effect to the original DOM structure/the slide design through the manipulation. 170 | 171 | In other words, the enabled feature requires taking care of the manipulated DOM and the view when styling. 172 | 173 | **_If you never want to think complex styling, it's better to define no extra metadata._** Your theme would work as same as a simple [Marpit theme CSS](https://marpit.marp.app/theme-css) if you do nothing. 174 | 175 | ## `@auto-scaling [flag(s)]` 176 | 177 | Enable [auto-scaling features](https://github.com/marp-team/marp-core#auto-scaling-features). 178 | 179 | - `true`: Enable all features. 180 | - `fittingHeader`: Enable fitting header. 181 | - `math`: Enable scaling for KaTeX math block. _Please note that MathJax math block always will apply auto scaling down._ 182 | - `code`: Enable scaling for code block. 183 | 184 | Through separating by comma, it can select multiple keywords for individual features. 185 | 186 | ```css 187 | /** 188 | * @theme foobar 189 | * @auto-scaling fittingHeader,math 190 | */ 191 | ``` 192 | 193 | ## `@size [name] [width] [height]` 194 | 195 | Define size preset(s) for usable in [`size` global directive](https://github.com/marp-team/marp-core#size-global-directive). 196 | 197 | ```css 198 | /** 199 | * @theme foobar 200 | * @size 4:3 960px 720px 201 | * @size 16:9 1280px 720px 202 | * @size 4K 3840px 2160px 203 | */ 204 | 205 | section { 206 | /* A way to define default size is as same as Marpit theme CSS. */ 207 | width: 960px; 208 | height: 720px; 209 | } 210 | ``` 211 | 212 | User can choose a customized size of slide deck (`section`) from defined presets via `size` global directive. 213 | 214 | ```markdown 215 | --- 216 | theme: foobar 217 | size: 4K 218 | --- 219 | 220 | # Slide deck for 4K screen (3840x2160) 221 | ``` 222 | 223 | When the imported theme through [`@import "foo";`](https://marpit.marp.app/theme-css?id=import-rule) or [`@import-theme "bar";`](https://marpit.marp.app/theme-css?id=import-theme-rule) has `@size` metadata(s), these presets still can use in an inherited theme. 224 | 225 | Or you can use `@size [name] false` in the inherited theme if you need to disable specific preset. 226 | 227 | ```css 228 | /** 229 | * gaia-16-9 theme is based on Gaia theme, but 4:3 slide cannot use. 230 | * 231 | * @theme inherited-from-gaia 232 | * @size 4:3 false 233 | */ 234 | 235 | @import 'gaia'; 236 | ``` 237 | -------------------------------------------------------------------------------- /themes/assets/uncover-quote.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /themes/default.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable no-descending-specificity -- https://github.com/stylelint/stylelint/issues/5065 */ 2 | 3 | /*! 4 | * Marp default theme. 5 | * 6 | * @theme default 7 | * @author Yuki Hattori 8 | * 9 | * @auto-scaling true 10 | * @size 16:9 1280px 720px 11 | * @size 4:3 960px 720px 12 | */ 13 | 14 | @use 'sass:meta'; 15 | @include meta.load-css('pkg:github-markdown-css/github-markdown.css'); 16 | 17 | h1 { 18 | border-bottom: none; 19 | color: var(--h1-color); 20 | font-size: 1.6em; 21 | } 22 | 23 | h2 { 24 | border-bottom: none; 25 | font-size: 1.3em; 26 | } 27 | 28 | h3 { 29 | font-size: 1.1em; 30 | } 31 | 32 | h4 { 33 | font-size: 1.05em; 34 | } 35 | 36 | h5 { 37 | font-size: 1em; 38 | } 39 | 40 | h6 { 41 | font-size: 0.9em; 42 | } 43 | 44 | h1, 45 | h2, 46 | h3, 47 | h4, 48 | h5, 49 | h6 { 50 | strong { 51 | font-weight: inherit; 52 | color: var(--heading-strong-color); 53 | } 54 | 55 | &::part(auto-scaling) { 56 | max-height: 563px; // Slide height - padding * 2 57 | } 58 | } 59 | 60 | hr { 61 | height: 0; 62 | padding-top: 0.25em; 63 | } 64 | 65 | img { 66 | background-color: transparent; 67 | } 68 | 69 | pre { 70 | /* stylelint-disable-next-line custom-property-pattern */ 71 | border: 1px solid var(--borderColor-default); 72 | line-height: 1.15; 73 | overflow: visible; 74 | 75 | &::part(auto-scaling) { 76 | // Slide height - padding * 2 - code's padding * 3 - code's border * 2 77 | max-height: 529px; 78 | } 79 | 80 | // GitHub's prettylights -> Highlight.js 81 | /* stylelint-disable selector-class-pattern */ 82 | :where(.hljs) { 83 | color: var(--color-prettylights-syntax-storage-modifier-import); 84 | } 85 | 86 | :where(.hljs-doctag), 87 | :where(.hljs-keyword), 88 | :where(.hljs-meta .hljs-keyword), 89 | :where(.hljs-template-tag), 90 | :where(.hljs-template-variable), 91 | :where(.hljs-type), 92 | :where(.hljs-variable.language_) { 93 | color: var(--color-prettylights-syntax-keyword); 94 | } 95 | 96 | :where(.hljs-title), 97 | :where(.hljs-title.class_), 98 | :where(.hljs-title.class_.inherited__), 99 | :where(.hljs-title.function_) { 100 | color: var(--color-prettylights-syntax-entity); 101 | } 102 | 103 | :where(.hljs-attr), 104 | :where(.hljs-attribute), 105 | :where(.hljs-literal), 106 | :where(.hljs-meta), 107 | :where(.hljs-number), 108 | :where(.hljs-operator), 109 | :where(.hljs-selector-attr), 110 | :where(.hljs-selector-class), 111 | :where(.hljs-selector-id), 112 | :where(.hljs-variable) { 113 | color: var(--color-prettylights-syntax-constant); 114 | } 115 | 116 | :where(.hljs-string), 117 | :where(.hljs-meta .hljs-string), 118 | :where(.hljs-regexp) { 119 | color: var(--color-prettylights-syntax-string); 120 | } 121 | 122 | :where(.hljs-built_in), 123 | :where(.hljs-symbol) { 124 | color: var(--color-prettylights-syntax-variable); 125 | } 126 | 127 | :where(.hljs-code), 128 | :where(.hljs-comment), 129 | :where(.hljs-formula) { 130 | color: var(--color-prettylights-syntax-comment); 131 | } 132 | 133 | :where(.hljs-name), 134 | :where(.hljs-quote), 135 | :where(.hljs-selector-pseudo), 136 | :where(.hljs-selector-tag) { 137 | color: var(--color-prettylights-syntax-entity-tag); 138 | } 139 | 140 | :where(.hljs-subst) { 141 | color: var(--color-prettylights-syntax-storage-modifier-import); 142 | } 143 | 144 | :where(.hljs-section) { 145 | font-weight: bold; 146 | color: var(--color-prettylights-syntax-markup-heading); 147 | } 148 | 149 | :where(.hljs-bullet) { 150 | color: var(--color-prettylights-syntax-markup-list); 151 | } 152 | 153 | :where(.hljs-emphasis) { 154 | font-style: italic; 155 | color: var(--color-prettylights-syntax-markup-italic); 156 | } 157 | 158 | :where(.hljs-strong) { 159 | font-weight: bold; 160 | color: var(--color-prettylights-syntax-markup-bold); 161 | } 162 | 163 | :where(.hljs-addition) { 164 | color: var(--color-prettylights-syntax-markup-inserted-text); 165 | background-color: var(--color-prettylights-syntax-markup-inserted-bg); 166 | } 167 | 168 | :where(.hljs-deletion) { 169 | color: var(--color-prettylights-syntax-markup-deleted-text); 170 | background-color: var(--color-prettylights-syntax-markup-deleted-bg); 171 | } 172 | /* stylelint-enable selector-class-pattern */ 173 | } 174 | 175 | header, 176 | footer { 177 | margin: 0; 178 | position: absolute; 179 | left: 30px; 180 | color: var(--header-footer-color); 181 | font-size: 18px; 182 | } 183 | 184 | header { 185 | top: 21px; 186 | } 187 | 188 | footer { 189 | bottom: 21px; 190 | } 191 | 192 | section { 193 | /* stylelint-disable-next-line scss/at-extend-no-missing-placeholder */ 194 | @extend .markdown-body; 195 | 196 | --h1-color: #246; 197 | --header-footer-color: #{rgba(#666, 0.75)}; 198 | --heading-strong-color: #48c; 199 | --paginate-color: #777; 200 | 201 | /* 202 | * GitHub Markdown CSS has defined space sizes with the rem unit. The root 203 | * font size was changed by `font-size` property on `section` element so we 204 | * have to revert them to the default value. 205 | */ 206 | --base-size-4: 4px; 207 | --base-size-8: 8px; 208 | --base-size-16: 16px; 209 | --base-size-24: 24px; 210 | --base-size-40: 40px; 211 | 212 | display: block; 213 | place-content: safe center center; 214 | font-size: 29px; 215 | height: 720px; 216 | padding: 78.5px; 217 | width: 1280px; 218 | 219 | /* Definitions for classic bhavior: Users can adopt flex centering by tweaking style `section { display: flex }` */ 220 | flex-flow: column nowrap; 221 | align-items: stretch; 222 | 223 | &:where(.invert) { 224 | --h1-color: #cee7ff; 225 | --header-footer-color: #{rgba(#999, 0.75)}; 226 | --heading-strong-color: #7bf; 227 | --paginate-color: #999; 228 | } 229 | 230 | > *:last-child, 231 | &[data-footer] > :nth-last-child(2) { 232 | margin-bottom: 0; 233 | } 234 | 235 | > *:first-child, 236 | > header:first-child + * { 237 | margin-top: 0; 238 | } 239 | 240 | &::after { 241 | position: absolute; 242 | padding: 0; 243 | right: 30px; 244 | bottom: 21px; 245 | font-size: 24px; 246 | color: var(--paginate-color); 247 | } 248 | 249 | &[data-color] { 250 | h1, 251 | h2, 252 | h3, 253 | h4, 254 | h5, 255 | h6 { 256 | color: currentcolor; 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /themes/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | theme: default 3 | class: 4 | --- 5 | 6 | # marp-core 7 | 8 | The core of Marp converter 9 | 10 | ###### Created by [marp-team](https://github.com/marp-team/) 11 | 12 | ![bg right 50%](https://github.com/marp-team.png) 13 | -------------------------------------------------------------------------------- /themes/gaia.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'sass:meta'; 3 | 4 | /*! 5 | * Marp / Marpit Gaia theme. 6 | * 7 | * @theme gaia 8 | * @author Yuki Hattori 9 | * 10 | * @auto-scaling true 11 | * @size 16:9 1280px 720px 12 | * @size 4:3 960px 720px 13 | */ 14 | 15 | $color-light: #fff8e1; 16 | $color-dark: #455a64; 17 | $color-primary: #0288d1; 18 | $color-secondary: #81d4fa; 19 | 20 | @import 'https://fonts.bunny.net/css?family=Lato:400,900|Roboto+Mono:400,700&display=swap'; 21 | @include meta.load-css('pkg:highlight.js/styles/sunburst.css'); 22 | 23 | @mixin color-scheme($bg, $text, $highlight) { 24 | --color-background: #{$bg}; 25 | --color-background-stripe: #{rgba($text, 0.1)}; 26 | --color-foreground: #{$text}; 27 | --color-dimmed: #{color.mix($text, $bg, 80%)}; 28 | --color-highlight: #{$highlight}; 29 | } 30 | 31 | h1, 32 | h2, 33 | h3, 34 | h4, 35 | h5, 36 | h6 { 37 | margin: 0.5em 0 0; 38 | 39 | strong { 40 | font-weight: inherit; 41 | } 42 | 43 | &::part(auto-scaling) { 44 | max-height: 580px; // Slide height - padding * 2 45 | } 46 | } 47 | 48 | h1 { 49 | font-size: 1.8em; 50 | } 51 | 52 | h2 { 53 | font-size: 1.5em; 54 | } 55 | 56 | h3 { 57 | font-size: 1.3em; 58 | } 59 | 60 | h4 { 61 | font-size: 1.1em; 62 | } 63 | 64 | h5 { 65 | font-size: 1em; 66 | } 67 | 68 | h6 { 69 | font-size: 0.9em; 70 | } 71 | 72 | p, 73 | blockquote { 74 | margin: 1em 0 0; 75 | } 76 | 77 | ul, 78 | ol { 79 | > li { 80 | margin: 0.3em 0 0; 81 | 82 | > p { 83 | margin: 0.6em 0 0; 84 | } 85 | } 86 | } 87 | 88 | code { 89 | display: inline-block; 90 | font-family: 'Roboto Mono', monospace; 91 | font-size: 0.8em; 92 | letter-spacing: 0; 93 | margin: -0.1em 0.15em; 94 | padding: 0.1em 0.2em; 95 | vertical-align: baseline; 96 | } 97 | 98 | pre { 99 | display: block; 100 | margin: 1em 0 0; 101 | overflow: visible; 102 | 103 | code { 104 | box-sizing: border-box; 105 | margin: 0; 106 | min-width: 100%; 107 | padding: 0.5em; 108 | font-size: 0.7em; 109 | } 110 | 111 | &::part(auto-scaling) { 112 | max-height: calc(580px - 1em); 113 | } 114 | } 115 | 116 | blockquote { 117 | margin: 1em 0 0; 118 | padding: 0 1em; 119 | position: relative; 120 | 121 | &::after, 122 | &::before { 123 | content: '“'; 124 | display: block; 125 | font-family: 'Times New Roman', serif; 126 | font-weight: bold; 127 | position: absolute; 128 | } 129 | 130 | &::before { 131 | top: 0; 132 | left: 0; 133 | } 134 | 135 | &::after { 136 | right: 0; 137 | bottom: 0; 138 | transform: rotate(180deg); 139 | } 140 | 141 | > *:first-child { 142 | margin-top: 0; 143 | } 144 | } 145 | 146 | mark { 147 | background: transparent; 148 | } 149 | 150 | table { 151 | border-spacing: 0; 152 | border-collapse: collapse; 153 | margin: 1em 0 0; 154 | 155 | th, 156 | td { 157 | padding: 0.2em 0.4em; 158 | border-width: 1px; 159 | border-style: solid; 160 | } 161 | } 162 | 163 | header, 164 | footer, 165 | section::after { 166 | box-sizing: border-box; 167 | font-size: 66%; 168 | height: 70px; 169 | line-height: 50px; 170 | overflow: hidden; 171 | padding: 10px 25px; 172 | position: absolute; 173 | } 174 | 175 | header { 176 | left: 0; 177 | right: 0; 178 | top: 0; 179 | } 180 | 181 | footer { 182 | left: 0; 183 | right: 0; 184 | bottom: 0; 185 | } 186 | 187 | section { 188 | background-color: var(--color-background); 189 | background-image: linear-gradient( 190 | 135deg, 191 | rgba(#888, 0), 192 | rgba(#888, 0.02) 50%, 193 | rgba(#fff, 0) 50%, 194 | rgba(#fff, 0.05) 195 | ); 196 | color: var(--color-foreground); 197 | font-size: 35px; 198 | font-family: 199 | Lato, 'Avenir Next', Avenir, 'Trebuchet MS', 'Segoe UI', sans-serif; 200 | height: 720px; 201 | line-height: 1.35; 202 | letter-spacing: 1.25px; 203 | padding: 70px; 204 | width: 1280px; 205 | word-wrap: break-word; 206 | 207 | @include color-scheme($color-light, $color-dark, $color-primary); 208 | 209 | &::after { 210 | right: 0; 211 | bottom: 0; 212 | font-size: 80%; 213 | } 214 | 215 | a, 216 | mark { 217 | color: var(--color-highlight); 218 | } 219 | 220 | code { 221 | background: var(--color-dimmed); 222 | color: var(--color-background); 223 | } 224 | 225 | h1, 226 | h2, 227 | h3, 228 | h4, 229 | h5, 230 | h6 { 231 | strong { 232 | color: var(--color-highlight); 233 | } 234 | } 235 | 236 | pre { 237 | background: var(--color-foreground); 238 | } 239 | 240 | pre > code { 241 | background: transparent; 242 | } 243 | 244 | header, 245 | footer, 246 | section::after, 247 | blockquote::before, 248 | blockquote::after { 249 | color: var(--color-dimmed); 250 | } 251 | 252 | table { 253 | th, 254 | td { 255 | border-color: var(--color-foreground); 256 | } 257 | 258 | thead th { 259 | background: var(--color-foreground); 260 | color: var(--color-background); 261 | } 262 | 263 | tbody > tr:nth-child(odd) { 264 | td, 265 | th { 266 | background: var(--color-background-stripe, transparent); 267 | } 268 | } 269 | } 270 | 271 | > *:first-child, 272 | > header:first-child + * { 273 | margin-top: 0; 274 | } 275 | 276 | &:where(.invert) { 277 | @include color-scheme($color-dark, $color-light, $color-secondary); 278 | } 279 | 280 | &:where(.gaia) { 281 | @include color-scheme($color-primary, $color-light, $color-secondary); 282 | } 283 | 284 | &:where(.lead) { 285 | place-content: safe center center; 286 | 287 | /* Definitions for classic bhavior: Users can adopt flex centering by tweaking style `section.lead { display: flex }` */ 288 | flex-flow: column nowrap; 289 | align-items: stretch; 290 | 291 | h1, 292 | h2, 293 | h3, 294 | h4, 295 | h5, 296 | h6 { 297 | text-align: center; 298 | } 299 | 300 | /* stylelint-disable-next-line no-descending-specificity */ 301 | p { 302 | text-align: center; 303 | } 304 | 305 | blockquote { 306 | > h1, 307 | > h2, 308 | > h3, 309 | > h4, 310 | > h5, 311 | > h6, 312 | > p { 313 | text-align: left; 314 | } 315 | } 316 | 317 | ul, 318 | ol { 319 | > li > p { 320 | text-align: left; 321 | } 322 | } 323 | 324 | table { 325 | margin-left: auto; 326 | margin-right: auto; 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /themes/uncover.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'sass:meta'; 3 | 4 | /*! 5 | * Marp / Marpit Uncover theme 6 | * 7 | * @theme uncover 8 | * @author Yuki Hattori 9 | * 10 | * @auto-scaling true 11 | * @size 16:9 1280px 720px 12 | * @size 4:3 960px 720px 13 | */ 14 | 15 | @mixin color-scheme($bg: #fdfcff, $text: #202228, $highlight: #009dd5) { 16 | --color-background: #{$bg}; 17 | --color-background-code: #{color.mix($bg, $text, 95%)}; 18 | --color-background-paginate: #{rgba($text, 0.05)}; 19 | --color-foreground: #{$text}; 20 | --color-highlight: #{$highlight}; 21 | --color-highlight-hover: #{color.mix($text, $highlight, 25%)}; 22 | --color-highlight-heading: #{color.mix(#fff, $highlight, 20%)}; 23 | --color-header: #{rgba($text, 0.4)}; 24 | --color-header-shadow: #{rgba($bg, 0.8)}; 25 | } 26 | 27 | section { 28 | @include color-scheme; 29 | 30 | background: var(--color-background); 31 | color: var(--color-foreground); 32 | display: block; 33 | place-content: safe center center; 34 | font-family: 35 | -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 36 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 37 | font-size: 40px; 38 | height: 720px; 39 | letter-spacing: 3px; 40 | line-height: 1.4; 41 | padding: 30px 70px; 42 | position: relative; 43 | text-align: center; 44 | width: 1280px; 45 | word-wrap: break-word; 46 | z-index: 0; 47 | 48 | /* Definitions for classic bhavior: Users can adopt flex centering by tweaking style `section { display: flex }` */ 49 | flex-flow: column nowrap; 50 | align-items: stretch; 51 | 52 | &::after { 53 | align-items: flex-end; 54 | background: linear-gradient( 55 | -45deg, 56 | var(--color-background-paginate) 50%, 57 | transparent 50% 58 | ); 59 | background-size: cover; 60 | color: var(--color-foreground); 61 | display: flex; 62 | font-size: 0.6em; 63 | height: 80px; 64 | justify-content: flex-end; 65 | padding: 30px; 66 | text-align: right; 67 | text-shadow: 0 0 5px var(--color-background); 68 | width: 80px; 69 | } 70 | 71 | &:where(:not(.invert)) { 72 | @include meta.load-css('pkg:highlight.js/styles/color-brewer.css'); 73 | } 74 | 75 | &:where(.invert) { 76 | @include color-scheme(#202228, #fff, #60d0f0); 77 | @include meta.load-css('pkg:highlight.js/styles/codepen-embed.css'); 78 | } 79 | 80 | > *:first-child, 81 | &[data-header] > :nth-child(2) { 82 | margin-top: 0; 83 | } 84 | 85 | > *:last-child, 86 | &[data-footer] > :nth-last-child(2) { 87 | margin-bottom: 0; 88 | } 89 | 90 | p, 91 | blockquote { 92 | margin: 0 0 15px; 93 | } 94 | 95 | h1, 96 | h2, 97 | h3, 98 | h4, 99 | h5, 100 | h6 { 101 | margin: 15px 0 30px; 102 | 103 | strong { 104 | color: var(--color-highlight-heading); 105 | font-weight: inherit; 106 | } 107 | 108 | &::part(auto-scaling) { 109 | max-height: 660px; // Slide height - padding * 2 110 | } 111 | } 112 | 113 | h1 { 114 | font-size: 2em; 115 | } 116 | 117 | h2 { 118 | font-size: 1.7em; 119 | } 120 | 121 | h3 { 122 | font-size: 1.4em; 123 | letter-spacing: 2px; 124 | } 125 | 126 | h4 { 127 | font-size: 1.2em; 128 | letter-spacing: 2px; 129 | } 130 | 131 | h5 { 132 | font-size: 1em; 133 | letter-spacing: 1px; 134 | } 135 | 136 | h6 { 137 | font-size: 0.8em; 138 | letter-spacing: 1px; 139 | } 140 | 141 | header, 142 | footer { 143 | color: var(--color-header); 144 | font-size: 0.45em; 145 | left: 70px; 146 | letter-spacing: 1px; 147 | position: absolute; 148 | right: 70px; 149 | text-shadow: 0 1px 0 var(--color-header-shadow); 150 | z-index: 1; 151 | } 152 | 153 | header { 154 | top: 30px; 155 | } 156 | 157 | footer { 158 | bottom: 30px; 159 | } 160 | 161 | a { 162 | color: var(--color-highlight); 163 | text-decoration: none; 164 | 165 | &:hover { 166 | color: var(--color-highlight-hover); 167 | text-decoration: underline; 168 | } 169 | } 170 | 171 | ul, 172 | ol { 173 | margin: 0 auto; 174 | text-align: left; 175 | } 176 | 177 | > ul, 178 | > ol { 179 | margin-bottom: 15px; 180 | } 181 | 182 | code { 183 | font-family: 184 | SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; 185 | letter-spacing: 0; 186 | } 187 | 188 | & > code, 189 | *:not(pre) > code { 190 | background: var(--color-background-code); 191 | color: var(--color-foreground); 192 | margin: -0.2em 0.2em 0.2em; 193 | padding: 0.2em; 194 | } 195 | 196 | pre { 197 | --preserve-aspect-ratio: xMidYMid meet; 198 | 199 | filter: drop-shadow(0 4px 4px rgba(#000, 0.2)); 200 | font-size: 70%; 201 | line-height: 1.15; 202 | margin: 15px 0 30px; 203 | text-align: left; 204 | 205 | &::part(auto-scaling) { 206 | max-height: 570px; 207 | } 208 | } 209 | 210 | pre > code { 211 | background: var(--color-background-code); 212 | box-sizing: content-box; 213 | color: var(--color-foreground); 214 | display: block; 215 | margin: 0 auto; 216 | min-width: 456px; // (Slide width - padding * 2) * 40% 217 | padding: 0.4em 0.6em; 218 | } 219 | 220 | &[data-size='4:3'] pre > code { 221 | min-width: 328px; 222 | } 223 | 224 | table { 225 | border-collapse: collapse; 226 | margin: 0 auto 15px; 227 | 228 | > thead, 229 | > tbody { 230 | > tr { 231 | > td, 232 | > th { 233 | padding: 0.15em 0.5em; 234 | } 235 | } 236 | } 237 | 238 | > thead > tr { 239 | > td, 240 | > th { 241 | border-bottom: 3px solid currentcolor; 242 | } 243 | } 244 | 245 | > tbody > tr:not(:last-child) { 246 | > td, 247 | > th { 248 | border-bottom: 1px solid currentcolor; 249 | } 250 | } 251 | } 252 | 253 | blockquote { 254 | font-size: 90%; 255 | line-height: 1.3; 256 | padding: 0 2em; 257 | position: relative; 258 | z-index: 0; 259 | 260 | &::before, 261 | &::after { 262 | content: url('./assets/uncover-quote.svg'); 263 | height: auto; 264 | pointer-events: none; 265 | position: absolute; 266 | width: 1em; 267 | z-index: -1; 268 | } 269 | 270 | &::before { 271 | left: 0; 272 | top: 0; 273 | } 274 | 275 | &::after { 276 | bottom: 0; 277 | right: 0; 278 | transform: rotate(180deg); 279 | } 280 | 281 | > *:last-child { 282 | margin-bottom: 0; 283 | } 284 | } 285 | 286 | mark { 287 | color: var(--color-highlight); 288 | background: transparent; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@tsconfig/recommended/tsconfig", "@tsconfig/node20/tsconfig"], 3 | "compilerOptions": { 4 | "noImplicitAny": false, 5 | "resolveJsonModule": true, 6 | "rootDir": ".", 7 | "sourceMap": true, 8 | "isolatedModules": true 9 | }, 10 | "include": ["src"] 11 | } 12 | --------------------------------------------------------------------------------