├── .circleci └── config.yml ├── .github ├── CONTRIBUTING.md └── workflows │ └── github-release.yml ├── .gitignore ├── .jsdoc.json ├── .nvmrc ├── .prettierignore ├── .stylelintrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── api.md ├── babel.config.js ├── docs ├── _redirects ├── _sidebar.md ├── assets │ ├── directives.png │ ├── hello-marpit-theme.pdf │ ├── hello-marpit.pdf │ ├── how-to-use │ │ ├── example.html │ │ └── example.pdf │ ├── image-syntax │ │ ├── multiple-bg-vertical.png │ │ ├── multiple-bg.png │ │ ├── split-background.jpg │ │ ├── split-bg-with-size.jpg │ │ └── split-multiple-bg.jpg │ └── plugin-custom-container.png ├── directives.md ├── favicon.png ├── fragmented-list.md ├── image-syntax.md ├── index.html ├── inline-svg.md ├── introduction.md ├── jsdoc.css ├── markdown.md ├── marpit-dark.png ├── marpit.png ├── theme-css.md └── usage.md ├── docsify ├── _alert.scss ├── _code.scss ├── _layout.scss ├── build.js ├── docsify.scss ├── modules │ └── build.js └── serve.js ├── eslint.config.mjs ├── index.d.ts ├── jest.config.js ├── jest.setup.js ├── package-lock.json ├── package.json ├── plugin.js ├── src ├── element.js ├── helpers │ ├── inline_style.js │ ├── postcss_plugin.js │ ├── split.js │ ├── wrap_array.js │ └── wrap_tokens.js ├── index.js ├── markdown │ ├── background_image.js │ ├── background_image │ │ ├── advanced.js │ │ ├── apply.js │ │ └── parse.js │ ├── collect.js │ ├── comment.js │ ├── container.js │ ├── directives │ │ ├── apply.js │ │ ├── directives.js │ │ ├── parse.js │ │ └── yaml.js │ ├── fragment.js │ ├── header_and_footer.js │ ├── heading_divider.js │ ├── image.js │ ├── image │ │ ├── apply.js │ │ └── parse.js │ ├── inline_svg.js │ ├── slide.js │ ├── slide_container.js │ ├── style │ │ ├── assign.js │ │ └── parse.js │ └── sweep.js ├── marpit.js ├── plugin.js ├── postcss │ ├── advanced_background.js │ ├── container_query.js │ ├── import │ │ ├── hoisting.js │ │ ├── parse.js │ │ ├── replace.js │ │ └── suppress.js │ ├── meta.js │ ├── nesting.js │ ├── pagination.js │ ├── printable.js │ ├── pseudo_selector │ │ ├── prepend.js │ │ └── replace.js │ ├── root │ │ ├── font_size.js │ │ ├── increasing_specificity.js │ │ ├── rem.js │ │ └── replace.js │ ├── section_size.js │ └── svg_backdrop.js ├── theme.js ├── theme │ ├── scaffold.js │ └── symbol.js └── theme_set.js └── test ├── _supports ├── postcss_finder.js └── selector_normalizer.js ├── element.js ├── helpers └── inline_style.js ├── markdown ├── background_image.js ├── collect.js ├── comment.js ├── container.js ├── directives │ ├── apply.js │ ├── parse.js │ └── yaml.js ├── fragment.js ├── header_and_footer.js ├── heading_divider.js ├── image.js ├── inline_svg.js ├── slide.js ├── slide_container.js ├── style │ ├── assign.js │ └── parse.js └── sweep.js ├── marpit.js ├── plugin.js ├── postcss ├── advanced_background.js ├── container_query.js ├── import │ ├── hoisting.js │ ├── parse.js │ ├── replace.js │ └── suppress.js ├── meta.js ├── pagination.js ├── printable.js ├── pseudo_selector │ ├── prepend.js │ └── replace.js ├── root │ ├── font_size.js │ ├── increasing_specificity.js │ ├── rem.js │ └── replace.js ├── section_size.js └── svg_backdrop.js ├── theme.js └── theme_set.js /.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: ~/marpit 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: v2.3-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: ESLint 67 | command: npm run lint:js 68 | 69 | - run: 70 | name: stylelint 71 | command: npm run lint:css 72 | 73 | test: 74 | steps: 75 | - run: 76 | name: Jest 77 | command: npm run test:coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit 78 | environment: 79 | JEST_JUNIT_OUTPUT_DIR: tmp/test-results 80 | 81 | - codecov/upload 82 | 83 | - store_test_results: 84 | path: tmp/test-results 85 | 86 | - store_artifacts: 87 | path: ./coverage 88 | destination: coverage 89 | 90 | jobs: 91 | audit: 92 | executor: node 93 | steps: 94 | - audit 95 | 96 | test-node18: 97 | executor: 98 | name: node 99 | version: '18.20' 100 | steps: 101 | - prepare: 102 | force: true 103 | - test 104 | 105 | test-node20: 106 | executor: 107 | name: node 108 | version: '20.19' 109 | steps: 110 | - prepare 111 | - lint 112 | - test 113 | 114 | test-node22: 115 | executor: 116 | name: node 117 | version: '22.15.0' # Specify LTS version for development 118 | steps: 119 | - prepare 120 | - lint 121 | - test 122 | 123 | test-node24: 124 | executor: 125 | name: node 126 | version: '24.0' 127 | steps: 128 | - prepare 129 | - lint 130 | - test 131 | 132 | workflows: 133 | test: 134 | jobs: 135 | - audit 136 | - test-node18: 137 | requires: 138 | - audit 139 | - test-node20: 140 | requires: 141 | - audit 142 | - test-node22: 143 | requires: 144 | - audit 145 | - test-node24: 146 | requires: 147 | - audit 148 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Marpit 2 | 3 | Thank you for taking the time to read how to contribute to Marpit! This is the guideline for contributing to Marpit. 4 | 5 | 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 | ### Pull request 10 | 11 | When sending pull request updated the interface of public class, it's better to mind to these resources. 12 | 13 | - JSDoc (for [API documentation](https://marpit-api.marp.app/)) 14 | - [Documents](https://marpit.marp.app/) (at [`/docs/`](../docs/)) 15 | - [`/index.d.ts`](../index.d.ts) (Type definition for TypeScript) 16 | -------------------------------------------------------------------------------- /.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 | dist 3 | jsdoc 4 | /docs/style/ 5 | 6 | ########## gitignore.io ########## 7 | # Created by https://www.gitignore.io/api/node,windows,macos,linux,sublimetext,emacs,vim,visualstudiocode 8 | 9 | ### Emacs ### 10 | # -*- mode: gitignore; -*- 11 | *~ 12 | \#*\# 13 | /.emacs.desktop 14 | /.emacs.desktop.lock 15 | *.elc 16 | auto-save-list 17 | tramp 18 | .\#* 19 | 20 | # Org-mode 21 | .org-id-locations 22 | *_archive 23 | 24 | # flymake-mode 25 | *_flymake.* 26 | 27 | # eshell files 28 | /eshell/history 29 | /eshell/lastdir 30 | 31 | # elpa packages 32 | /elpa/ 33 | 34 | # reftex files 35 | *.rel 36 | 37 | # AUCTeX auto folder 38 | /auto/ 39 | 40 | # cask packages 41 | .cask/ 42 | dist/ 43 | 44 | # Flycheck 45 | flycheck_*.el 46 | 47 | # server auth directory 48 | /server/ 49 | 50 | # projectiles files 51 | .projectile 52 | 53 | # directory configuration 54 | .dir-locals.el 55 | 56 | ### Linux ### 57 | 58 | # temporary files which can be created if a process still has a handle open of a deleted file 59 | .fuse_hidden* 60 | 61 | # KDE directory preferences 62 | .directory 63 | 64 | # Linux trash folder which might appear on any partition or disk 65 | .Trash-* 66 | 67 | # .nfs files are created when an open file is removed but is still being accessed 68 | .nfs* 69 | 70 | ### macOS ### 71 | *.DS_Store 72 | .AppleDouble 73 | .LSOverride 74 | 75 | # Icon must end with two \r 76 | Icon 77 | 78 | # Thumbnails 79 | ._* 80 | 81 | # Files that might appear in the root of a volume 82 | .DocumentRevisions-V100 83 | .fseventsd 84 | .Spotlight-V100 85 | .TemporaryItems 86 | .Trashes 87 | .VolumeIcon.icns 88 | .com.apple.timemachine.donotpresent 89 | 90 | # Directories potentially created on remote AFP share 91 | .AppleDB 92 | .AppleDesktop 93 | Network Trash Folder 94 | Temporary Items 95 | .apdisk 96 | 97 | ### Node ### 98 | # Logs 99 | logs 100 | *.log 101 | npm-debug.log* 102 | yarn-debug.log* 103 | yarn-error.log* 104 | 105 | # Runtime data 106 | pids 107 | *.pid 108 | *.seed 109 | *.pid.lock 110 | 111 | # Directory for instrumented libs generated by jscoverage/JSCover 112 | lib-cov 113 | 114 | # Coverage directory used by tools like istanbul 115 | coverage 116 | 117 | # nyc test coverage 118 | .nyc_output 119 | 120 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 121 | .grunt 122 | 123 | # Bower dependency directory (https://bower.io/) 124 | bower_components 125 | 126 | # node-waf configuration 127 | .lock-wscript 128 | 129 | # Compiled binary addons (http://nodejs.org/api/addons.html) 130 | build/Release 131 | 132 | # Dependency directories 133 | node_modules/ 134 | jspm_packages/ 135 | 136 | # Typescript v1 declaration files 137 | typings/ 138 | 139 | # Optional npm cache directory 140 | .npm 141 | 142 | # Optional eslint cache 143 | .eslintcache 144 | 145 | # Optional REPL history 146 | .node_repl_history 147 | 148 | # Output of 'npm pack' 149 | *.tgz 150 | 151 | # Yarn Integrity file 152 | .yarn-integrity 153 | 154 | # dotenv environment variables file 155 | .env 156 | 157 | 158 | ### SublimeText ### 159 | # cache files for sublime text 160 | *.tmlanguage.cache 161 | *.tmPreferences.cache 162 | *.stTheme.cache 163 | 164 | # workspace files are user-specific 165 | *.sublime-workspace 166 | 167 | # project files should be checked into the repository, unless a significant 168 | # proportion of contributors will probably not be using SublimeText 169 | # *.sublime-project 170 | 171 | # sftp configuration file 172 | sftp-config.json 173 | 174 | # Package control specific files 175 | Package Control.last-run 176 | Package Control.ca-list 177 | Package Control.ca-bundle 178 | Package Control.system-ca-bundle 179 | Package Control.cache/ 180 | Package Control.ca-certs/ 181 | Package Control.merged-ca-bundle 182 | Package Control.user-ca-bundle 183 | oscrypto-ca-bundle.crt 184 | bh_unicode_properties.cache 185 | 186 | # Sublime-github package stores a github token in this file 187 | # https://packagecontrol.io/packages/sublime-github 188 | GitHub.sublime-settings 189 | 190 | ### Vim ### 191 | # swap 192 | [._]*.s[a-v][a-z] 193 | [._]*.sw[a-p] 194 | [._]s[a-v][a-z] 195 | [._]sw[a-p] 196 | # session 197 | Session.vim 198 | # temporary 199 | .netrwhist 200 | # auto-generated tag files 201 | tags 202 | 203 | ### VisualStudioCode ### 204 | .vscode/* 205 | 206 | ### Windows ### 207 | # Windows thumbnail cache files 208 | Thumbs.db 209 | ehthumbs.db 210 | ehthumbs_vista.db 211 | 212 | # Folder config file 213 | Desktop.ini 214 | 215 | # Recycle Bin used on file shares 216 | $RECYCLE.BIN/ 217 | 218 | # Windows Installer files 219 | *.cab 220 | *.msi 221 | *.msm 222 | *.msp 223 | 224 | # Windows shortcuts 225 | *.lnk 226 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "opts": { 3 | "destination": "jsdoc", 4 | "readme": "api.md", 5 | "recurse": true, 6 | "template": "node_modules/clean-jsdoc-theme", 7 | "theme_opts": { 8 | "default_theme": "light", 9 | "homepageTitle": "Marpit API", 10 | "title": "Marpit API", 11 | "menu": [ 12 | { 13 | "title": "« Marpit docs", 14 | "link": "https://marpit.marp.app/", 15 | "target": "_blank" 16 | } 17 | ], 18 | "sections": ["Classes", "Modules"], 19 | "displayModuleHeader": true, 20 | "include_css": ["./docs/jsdoc.css"] 21 | }, 22 | "markdown": { 23 | "idInHeadings": true 24 | } 25 | }, 26 | "plugins": ["plugins/markdown"] 27 | } 28 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.15.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .vscode/ 3 | coverage/ 4 | dist/ 5 | jsdoc/ 6 | lib/ 7 | /docs/style/ 8 | node_modules 9 | package.json 10 | -------------------------------------------------------------------------------- /.stylelintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - stylelint-config-standard-scss 3 | 4 | ignoreFiles: 5 | - coverage/**/* 6 | - docs/style/**/* 7 | - jsdoc/**/* 8 | 9 | rules: 10 | at-rule-no-unknown: 11 | - null 12 | custom-property-pattern: 13 | - null 14 | scss/at-rule-no-unknown: 15 | - true 16 | -------------------------------------------------------------------------------- /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 |

2 | Marpit 3 | Marpit 4 |

5 |

6 | Marpit: Markdown slide deck framework 7 |

8 |

9 | CircleCI 10 | Codecov 11 | npm 12 | LICENSE 13 |

14 | 15 |
16 | 17 | ### [🗒 Documentation](https://marpit.marp.app/) | [⚙ API](https://marpit-api.marp.app/) 18 | 19 |
20 | 21 | --- 22 | 23 | **Marpit** /mɑːrpɪt/ is the skinny framework for creating slide deck from Markdown. It can transform Markdown and CSS theme(s) to slide deck composed of static HTML and CSS and create a web page convertible into slide PDF by printing. 24 | 25 | Marpit is designed to _output minimum assets for the slide deck_. You can use the bare assets as a logicless slide deck, but mainly we expect to integrate output with other tools and applications. 26 | 27 | In fact, this framework is created for using as the base of [a core converter][marp-core] in [Marp ecosystem][marp]. 28 | 29 | [marp]: https://github.com/marp-team/marp/ 30 | [marp-core]: https://github.com/marp-team/marp-core/ 31 | 32 | ## Features 33 | 34 | ### [:pencil: **Marpit Markdown**](https://marpit.marp.app/markdown) 35 | 36 | We have extended several features into [markdown-it](https://github.com/markdown-it/markdown-it) parser to support writing awesome slides, such as [_Directives_](https://marpit.marp.app/directives) and [_Slide backgrounds_](https://marpit.marp.app/image-syntax?id=slide-backgrounds). Additional syntaxes place importance on a compatibility with general Markdown documents. 37 | 38 | ### [:art: **Theme CSS by clean markup**](https://marpit.marp.app/theme-css) 39 | 40 | Marpit has the CSS theming system that can design slides everything. Unlike other slide frameworks, there are not any predefined classes and mixins. You have only to focus styling HTML elements by pure CSS. Marpit would take care of the selected theme's necessary conversion. 41 | 42 | ### [:triangular_ruler: **Inline SVG slide**](https://marpit.marp.app/inline-svg) _(Experimental)_ 43 | 44 | Optionally `` element can use as the container of each slide page. It can be realized the pixel-perfect scaling of the slide only by CSS, so handling slides in integrated apps become simplified. The isolated layer made by `` can provide [_advanced backgrounds_](https://marpit.marp.app/image-syntax?id=advanced-backgrounds) for the slide with keeping the original Markdown DOM structure. 45 | 46 | > We not provide any themes because Marpit is just a framework. You can use [@marp-team/marp-core][marp-core] if you want. It has the official themes, and practical features extended from Marpit. 47 | 48 | ## Getting started 49 | 50 | See [the documentation of Marpit](https://marpit.marp.app/?id=getting-started) to get started. 51 | 52 | - **[Documentation](https://marpit.marp.app/)** 53 | - [API (JSDoc)](https://marpit-api.marp.app/) 54 | 55 | ## Contributing 56 | 57 | Are you interested in contributing? See [CONTRIBUTING.md](.github/CONTRIBUTING.md) and [the common contributing guideline for Marp team](https://github.com/marp-team/.github/blob/master/CONTRIBUTING.md). 58 | 59 | ### Development 60 | 61 | ```bash 62 | git clone https://github.com/marp-team/marpit 63 | cd marpit 64 | 65 | npm install 66 | npm run build 67 | ``` 68 | 69 | ## Sub-projects 70 | 71 | - **[@marp-team/marpit-svg-polyfill](https://github.com/marp-team/marpit-svg-polyfill)** - A polyfill of the inline SVG slide in Safari based browsers. 72 | 73 | ## Author 74 | 75 | Managed by [@marp-team](https://github.com/marp-team). 76 | 77 | - Yuki Hattori ([@yhatt](https://github.com/yhatt)) 78 | 79 | ## License 80 | 81 | This framework releases under the [MIT License](LICENSE). 82 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | # [Marpit API](https://marpit-api.marp.app/) 2 | 3 | The documentation of Marpit API (on `main` branch) has been published at **[https://marpit-api.marp.app/](https://marpit-api.marp.app/)**. 4 | 5 | > Please run `npm run jsdoc` if you want to build documentation at local. It would build docs in `jsdoc` directory. 6 | 7 | ## Documentations 8 | 9 | ### Classes 10 | 11 | **Classes** section is documented about public classes. 12 | 13 | We provide **[`Marpit`](Marpit.html)** class by default export. 14 | 15 | ```javascript 16 | import Marpit from '@marp-team/marpit' 17 | ``` 18 | 19 | And all classes can use with named export. (Recommend if you are using TypeScript) 20 | 21 | ```javascript 22 | import { Element, Marpit, Theme, ThemeSet } from '@marp-team/marpit' 23 | ``` 24 | 25 | ### Modules _(for internal)_ 26 | 27 | **Modules** section is documented about internal modules, that includes plugins for markdown-it and PostCSS, for helping to learn Marpit's architecture by contributors and plugin authors. 28 | 29 | ⚠️ **Do not use internal modules directly.** They might be changed the specification without following semantic versioning. 30 | 31 | --- 32 | 33 | ## Create plugin 34 | 35 | Are you interested to develop third-party plugin for Marpit? 36 | 37 | ### [markdown-it](https://github.com/markdown-it/markdown-it) plugin 38 | 39 | Marpit's plugin interface has compatible with [markdown-it](https://github.com/markdown-it/markdown-it). [Please refer to the documentation of markdown-it](https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md) if you want to manipulate the result of Markdown rendering in plugin. 40 | 41 | ### Marpit plugin 42 | 43 | When plugin was used through [`Marpit.use()`](Marpit.html#use), it can access to current Marpit instance via `marpit` member of the passed markdown-it instance. 44 | 45 | `@marp-team/marpit/plugin` provides [a helper for creating Marpit plugin](module-plugin.html). A generated plugin promises an existance of `marpit` member. 46 | 47 | ```javascript 48 | import { marpitPlugin } from '@marp-team/marpit/plugin' 49 | 50 | export default marpitPlugin(({ marpit }) => { 51 | // Add your plugin code here (Add theme, define custom directives, etc...) 52 | /* 53 | marpit.customDirectives.local.yourDirective = (value) => { 54 | return { yourDirective: value } 55 | } 56 | */ 57 | }) 58 | ``` 59 | 60 | If the user tried to use the generated Marpit plugin from this helper as markdown-it plugin wrongly, the plugin throws an error. Thus, you can mark the plugin as dedicated to Marpit. 61 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: '18' }, shippedProposals: true }], 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /docs/_redirects: -------------------------------------------------------------------------------- 1 | https://marpit.netlify.com/* https://marpit.marp.app/:splat 301! 2 | /* /index.html 200 3 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [Introduction](/) 2 | - [Usage](/usage) 3 | -
[Marpit Markdown](/markdown) 4 | - [Directives](/directives) 5 | - [Image syntax](/image-syntax) 6 | - [Fragmented list](/fragmented-list) 7 | - [Theme CSS](/theme-css) 8 | - [Inline SVG slide](/inline-svg) 9 | 10 | --- 11 | 12 | 13 | 14 | - ![Marpit API](https://icongr.am/material/open-in-new.svg?size=24&color=808080)Marpit API (JSDoc) 15 | - ![GitHub](https://icongr.am/material/github.svg?size=24&color=808080)GitHub 16 | - ![npm](https://icongr.am/material/npm.svg?size=24&color=808080)npm ![](https://img.shields.io/npm/v/@marp-team/marpit.svg?style=flat-square&label=&colorB=888) 17 | -------------------------------------------------------------------------------- /docs/assets/directives.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/directives.png -------------------------------------------------------------------------------- /docs/assets/hello-marpit-theme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/hello-marpit-theme.pdf -------------------------------------------------------------------------------- /docs/assets/hello-marpit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/hello-marpit.pdf -------------------------------------------------------------------------------- /docs/assets/how-to-use/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 77 |
78 |
79 |

Hello, Marpit!

80 |

81 | Marpit is the skinny framework for creating slide deck from Markdown. 82 |

83 |
84 |
85 |

Ready to convert into PDF!

86 |

You can convert into PDF slide deck through Chrome.

87 |
88 |
89 | 90 | 91 | -------------------------------------------------------------------------------- /docs/assets/how-to-use/example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/how-to-use/example.pdf -------------------------------------------------------------------------------- /docs/assets/image-syntax/multiple-bg-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/image-syntax/multiple-bg-vertical.png -------------------------------------------------------------------------------- /docs/assets/image-syntax/multiple-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/image-syntax/multiple-bg.png -------------------------------------------------------------------------------- /docs/assets/image-syntax/split-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/image-syntax/split-background.jpg -------------------------------------------------------------------------------- /docs/assets/image-syntax/split-bg-with-size.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/image-syntax/split-bg-with-size.jpg -------------------------------------------------------------------------------- /docs/assets/image-syntax/split-multiple-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/image-syntax/split-multiple-bg.jpg -------------------------------------------------------------------------------- /docs/assets/plugin-custom-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/assets/plugin-custom-container.png -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/favicon.png -------------------------------------------------------------------------------- /docs/fragmented-list.md: -------------------------------------------------------------------------------- 1 | # Fragmented list 2 | 3 | Since v0.9.0, Marpit will parse lists with specific markers as the **fragmented list** for appearing contents one by one. 4 | 5 | ## For bullet list 6 | 7 | CommonMark allows `-`, `+`, and `*` as the character of [bullet list marker](https://spec.commonmark.org/0.29/#bullet-list-marker). Marpit would parse as fragmented list if you are using `*` as the marker. 8 | 9 | 10 | 11 | ```markdown 12 | # Bullet list 13 | 14 | - One 15 | - Two 16 | - Three 17 | 18 | --- 19 | 20 | # Fragmented list 21 | 22 | * One 23 | * Two 24 | * Three 25 | ``` 26 | 27 | 28 | 29 | ## For ordered list 30 | 31 | CommonMark's [ordered list marker](https://spec.commonmark.org/0.29/#ordered-list-marker) must have `.` or `)` after digits. Marpit would parse as fragmented list if you are using `)` as the following character. 32 | 33 | 34 | 35 | ```markdown 36 | # Ordered list 37 | 38 | 1. One 39 | 2. Two 40 | 3. Three 41 | 42 | --- 43 | 44 | # Fragmented list 45 | 46 | 1) One 47 | 2) Two 48 | 3) Three 49 | ``` 50 | 51 | 52 | 53 | ## Rendering 54 | 55 | A structure of rendered HTML from the fragmented list is same as the regular list. It just adds `data-marpit-fragment` data attribute to list items. They would be numbered from 1 in order of recognized items. 56 | 57 | In addition, `
` element of the slide that has fragmented list would be added `data-marpit-fragments` data attribute. It shows the number of fragmented list items of its slide. 58 | 59 | The below HTML is a rendered result of [bullet list example](#for-bullet-list). 60 | 61 | ```html 62 |
63 |

Bullet list

64 |
    65 |
  • One
  • 66 |
  • Two
  • 67 |
  • Three
  • 68 |
69 |
70 |
71 |

Fragmented list

72 |
    73 |
  • One
  • 74 |
  • Two
  • 75 |
  • Three
  • 76 |
77 |
78 | ``` 79 | 80 | ?> Fragmented list does not change DOM structure and appearances. It relies on a behavior of the integrated app whether actually treats the rendered list as fragments. 81 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Marpit: The skinny framework for creating slide deck from Markdown 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 70 | 71 |
72 | 116 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | [marp]: https://github.com/marp-team/marp/ 2 | [marp-core]: https://github.com/marp-team/marp-core 3 | 4 |
5 | 6 | [![Marpit](./marpit.png ':size=500xauto')](/) 7 | 8 |
9 |
10 | 11 | **Marpit**: Markdown slide deck framework 12 | 13 |
14 | 15 | --- 16 | 17 | **Marpit** /mɑːrpɪt/ is the skinny framework for creating slide deck from Markdown. It can transform Markdown and CSS theme(s) to slide deck composed of static HTML and CSS and create a web page convertible into slide PDF by printing. 18 | 19 | Marpit is designed to _output minimum assets for the slide deck_. You can use the bare assets as a logicless slide deck, but mainly we expect to integrate output with other tools and applications. 20 | 21 | In fact, this framework is created for using as the base of [a core converter][marp-core] in [Marp ecosystem][marp]. 22 | 23 | ## Features 24 | 25 | ### [📝 Marpit Markdown](/markdown) {docsify-ignore} 26 | 27 | We have extended several features into [markdown-it](https://github.com/markdown-it/markdown-it) parser to support writing awesome slides, such as [_Directives_](/directives) and [_Slide backgrounds_](/image-syntax#slide-backgrounds). Additional syntaxes place importance on a compatibility with general Markdown documents. 28 | 29 | ### [🎨 Theme CSS by clean markup](/theme-css) {docsify-ignore} 30 | 31 | Marpit has the CSS theming system that can design slides everything. Unlike other slide frameworks, there are not any predefined classes and mixins. You have only to focus styling HTML elements by pure CSS. Marpit would take care of the selected theme's necessary conversion. 32 | 33 | ### [📐 Inline SVG slide (Experimental)](/inline-svg) {docsify-ignore} 34 | 35 | Optionally `` element can use as the container of each slide page. It can be realized the pixel-perfect scaling of the slide only by CSS, so handling slides in integrated apps become simplified. The isolated layer made by `` can provide [_advanced backgrounds_](/image-syntax#advanced-backgrounds) for the slide with keeping the original Markdown DOM structure. 36 | 37 | ?> We not provide any themes because Marpit is just a framework. You can use [@marp-team/marp-core][marp-core] if you want. It has the official themes, and practical features extended from Marpit. 38 | 39 | ## Getting started 40 | 41 | ### Installation 42 | 43 | #### npm 44 | 45 | ```bash 46 | npm install @marp-team/marpit 47 | ``` 48 | 49 | #### yarn 50 | 51 | ```bash 52 | yarn add @marp-team/marpit 53 | ``` 54 | 55 | #### pnpm 56 | 57 | ```bash 58 | pnpm add @marp-team/marpit 59 | ``` 60 | 61 | ### How to use 62 | 63 | ```javascript 64 | import Marpit from '@marp-team/marpit' 65 | import fs from 'fs' 66 | 67 | // 1. Create instance (with options if you want) 68 | const marpit = new Marpit() 69 | 70 | // 2. Add theme CSS 71 | const theme = ` 72 | /* @theme example */ 73 | 74 | section { 75 | background-color: #369; 76 | color: #fff; 77 | font-size: 30px; 78 | padding: 40px; 79 | } 80 | 81 | h1, 82 | h2 { 83 | text-align: center; 84 | margin: 0; 85 | } 86 | 87 | h1 { 88 | color: #8cf; 89 | } 90 | ` 91 | marpit.themeSet.default = marpit.themeSet.add(theme) 92 | 93 | // 3. Render markdown 94 | const markdown = ` 95 | 96 | # Hello, Marpit! 97 | 98 | Marpit is the skinny framework for creating slide deck from Markdown. 99 | 100 | --- 101 | 102 | ## Ready to convert into PDF! 103 | 104 | You can convert into PDF slide deck through Chrome. 105 | 106 | ` 107 | const { html, css } = marpit.render(markdown) 108 | 109 | // 4. Use output in your HTML 110 | const htmlFile = ` 111 | 112 | 113 | 114 | ${html} 115 | 116 | ` 117 | fs.writeFileSync('example.html', htmlFile.trim()) 118 | ``` 119 | 120 | Outputted HTML is [here](/assets/how-to-use/example.html ':ignore'). It can convert into [PDF slide deck](/assets/how-to-use/example.pdf ':ignore') through printing by Chrome. 121 | 122 | We are introducing the basic usage step-by-step in [Usage](usage.md) section. 123 | 124 | ## Author 125 | 126 | Managed by [@marp-team](https://github.com/marp-team). 127 | 128 | - ![yhatt](https://github.com/yhatt.png ':size=16') Yuki Hattori ([@yhatt](https://github.com/yhatt)) 129 | 130 | ## License 131 | 132 | This framework releases under the [MIT License](https://github.com/marp-team/marpit/blob/main/LICENSE). 133 | -------------------------------------------------------------------------------- /docs/jsdoc.css: -------------------------------------------------------------------------------- 1 | .sidebar-container, 2 | .mobile-sidebar-container { 3 | padding: 0 !important; 4 | border-radius: 0 !important; 5 | } 6 | 7 | .sidebar-items-container { 8 | margin-top: 2rem !important; 9 | } 10 | 11 | .sidebar-container *, 12 | .mobile-sidebar-container * { 13 | border-radius: 0 !important; 14 | } 15 | 16 | section th, 17 | section td { 18 | padding: 0.25rem 0.75rem !important; 19 | font-size: 90%; 20 | } 21 | 22 | .attributes, 23 | .default { 24 | overflow-wrap: anywhere; 25 | } 26 | -------------------------------------------------------------------------------- /docs/markdown.md: -------------------------------------------------------------------------------- 1 | # Marpit Markdown {docsify-ignore-all} 2 | 3 | Marpit Markdown syntax focuses on compatibility with commonly Markdown documents. It means the result of rendering keeps looking nice even if you open the Marpit Markdown in a general Markdown editor. 4 | 5 | ## How to write slides? 6 | 7 | Marpit splits pages of the slide deck by horizontal ruler (e.g. `---`). It's very simple. 8 | 9 | ```markdown 10 | # Slide 1 11 | 12 | foo 13 | 14 | --- 15 | 16 | # Slide 2 17 | 18 | bar 19 | ``` 20 | 21 | ?> An empty line may be required before the dash ruler by the spec of [CommonMark](https://spec.commonmark.org/0.29/#example-28). You can use the underline ruler `___`, asterisk ruler `***`, and space-included ruler `- - -` when you do not want to add empty lines. 22 | 23 | ## Extended features 24 | 25 | ### [Directives](/directives) 26 | 27 | Marpit Markdown has extended syntax called **"Directives"** to support writing awesome slides. It can control your slide-deck theme, page number, header, footer, style, and so on. 28 | 29 | ### [Image syntax](/image-syntax) 30 | 31 | Marpit has extended Markdown image syntax `![](image.jpg)` to be helpful creating beautiful slides. 32 | 33 | ### [Fragmented list](/fragmented-list) 34 | 35 | Since v0.9.0, Marpit will parse lists with specific markers as the **fragmented list** for appearing contents one by one. 36 | -------------------------------------------------------------------------------- /docs/marpit-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/marpit-dark.png -------------------------------------------------------------------------------- /docs/marpit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marp-team/marpit/b4ea3e5ada57da696dab50baa24814344b7dcc0c/docs/marpit.png -------------------------------------------------------------------------------- /docsify/_alert.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | 3 | .alert-for-marp-consumers { 4 | background: var(--notice-important-background); 5 | border: 3px double var(--notice-important-border-color); 6 | margin: 0 0 2rem; 7 | overflow: hidden; 8 | padding: 0 1.5rem; 9 | } 10 | 11 | .alert-for-marp-consumers-content { 12 | grid-area: contents; 13 | } 14 | 15 | .alert-for-marp-consumers-close-container { 16 | float: right; 17 | width: 3rem; 18 | height: 3rem; 19 | padding: 0 0 1rem 1rem; 20 | overflow: hidden; 21 | } 22 | 23 | .alert-for-marp-consumers-close { 24 | background: transparent; 25 | border: 0; 26 | width: 2rem; 27 | height: 2rem; 28 | padding: 0; 29 | cursor: pointer; 30 | opacity: 0.7; 31 | 32 | &:hover { 33 | opacity: 0.8; 34 | } 35 | 36 | &:hover:active { 37 | opacity: 0.5; 38 | } 39 | } 40 | 41 | .alert-for-marp-consumers-close-img { 42 | width: 2rem; 43 | height: 2rem; 44 | } 45 | 46 | .alert-for-marp-consumers-button-container { 47 | text-align: right; 48 | margin-bottom: 1rem; 49 | } 50 | 51 | .alert-for-marp-consumers-button { 52 | background: var(--notice-important-border-color); 53 | border-radius: 0.25rem; 54 | border: 0; 55 | color: #fff; 56 | cursor: pointer; 57 | padding: 0.5rem 2rem; 58 | 59 | &:active { 60 | background: color.mix($important-color, #000, 80%); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docsify/_code.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --code-theme-background: #{rgba(#666, 0.05)}; 3 | --code-theme-comment: #9ab; 4 | --code-theme-function: #e71; 5 | --code-theme-keyword: #d42; 6 | --code-theme-operator: #a67f59; 7 | --code-theme-punctuation: #5ad; 8 | --code-theme-selection: #b3d4fc; 9 | --code-theme-selector: #458; 10 | --code-theme-tag: #70a; 11 | --code-theme-text: #333; 12 | --code-theme-variable: #e90; 13 | 14 | // Block 15 | --code-block-padding: 1.25em; 16 | --code-block-line-height: 1.4; 17 | 18 | // Inline 19 | --code-inline-color: currentcolor; 20 | 21 | // Copy code plugin 22 | --copycode-background: #aaa; 23 | } 24 | -------------------------------------------------------------------------------- /docsify/build.js: -------------------------------------------------------------------------------- 1 | require('./modules/build')() 2 | -------------------------------------------------------------------------------- /docsify/docsify.scss: -------------------------------------------------------------------------------- 1 | @import '../node_modules/docsify-themeable/dist/css/theme-defaults'; 2 | @import './layout'; 3 | @import './code'; 4 | @import './alert'; 5 | -------------------------------------------------------------------------------- /docsify/modules/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { promisify } = require('util') 4 | const autoprefixer = require('autoprefixer') 5 | const cssnano = require('cssnano') 6 | const postcss = require('postcss') 7 | const sass = require('sass') 8 | 9 | const from = path.join(__dirname, '../docsify.scss') 10 | const to = path.join(__dirname, '../../docs/style/docsify.css') 11 | 12 | module.exports = async () => { 13 | const { css: cssBuf } = await promisify(sass.render)({ 14 | file: from, 15 | outFile: to, 16 | sourceMap: true, 17 | sourceMapEmbed: true, 18 | }) 19 | 20 | const { css } = await postcss([ 21 | autoprefixer, 22 | cssnano({ preset: ['default', { mergeLonghand: false }] }), 23 | ]).process(cssBuf, { from, to, map: { annotation: false, inline: false } }) 24 | 25 | await promisify(fs.mkdir)(path.dirname(to), { recursive: true }) 26 | await promisify(fs.writeFile)(to, css) 27 | } 28 | -------------------------------------------------------------------------------- /docsify/serve.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const http = require('http') 3 | const path = require('path') 4 | const stream = require('stream') 5 | const chokidar = require('chokidar') 6 | const handler = require('serve-handler') 7 | const WebSocket = require('ws') 8 | const build = require('./modules/build') 9 | 10 | const debounce = (func, wait) => { 11 | let timeout 12 | return (...args) => { 13 | clearTimeout(timeout) 14 | timeout = setTimeout(() => { 15 | timeout = null 16 | func(...args) 17 | }, wait) 18 | } 19 | } 20 | 21 | // Serve WebSocket server for live reload 22 | const wsPort = process.env.WS_PORT || 3001 23 | const wss = new WebSocket.Server({ port: wsPort }) 24 | const reload = () => wss.clients.forEach((client) => client.send('reload')) 25 | 26 | const wsScript = ` 27 | (function() { 28 | const ws = new WebSocket('ws://' + location.hostname + ':${wsPort}/') 29 | 30 | ws.addEventListener('message', (e) => { 31 | if (e.data === 'reload') location.reload() 32 | }) 33 | })() 34 | ` 35 | 36 | // Build docisfy style and watch changes 37 | chokidar.watch('**/*.scss', { cwd: __dirname }).on('all', debounce(build, 250)) 38 | 39 | // Watch change of docs directory 40 | chokidar 41 | .watch('**/*', { cwd: path.resolve(__dirname, '../docs') }) 42 | .on('all', debounce(reload, 250)) 43 | 44 | // Serve documentation 45 | const port = process.env.PORT || 3000 46 | const server = http.createServer((req, res) => { 47 | const { writeHead } = res 48 | const overriddenHeaders = { 'Cache-Control': 'no-store' } 49 | 50 | res.writeHead = function overriddenWriteHead(status, headers) { 51 | return writeHead.call(this, status, { 52 | ...(headers || {}), 53 | ...overriddenHeaders, 54 | }) 55 | } 56 | 57 | const createReadStream = (absolutePath, opts) => 58 | new Promise((resolve) => { 59 | const readStream = fs.createReadStream(absolutePath, opts) 60 | if (path.extname(absolutePath) !== '.html') { 61 | resolve(readStream) 62 | } else { 63 | let queue = Buffer.from([]) 64 | 65 | readStream.pipe( 66 | new stream.Transform({ 67 | transform: (chunk, _, callback) => { 68 | queue = Buffer.concat([queue, chunk]) 69 | callback() 70 | }, 71 | final(callback) { 72 | let html = queue.toString() 73 | const endBodyIdx = html.lastIndexOf('') 74 | 75 | if (endBodyIdx >= 0) { 76 | html = `${html.slice( 77 | 0, 78 | endBodyIdx, 79 | )}${html.slice( 80 | endBodyIdx + 7, 81 | )}` 82 | } 83 | 84 | this.push(html) 85 | callback() 86 | 87 | overriddenHeaders['Content-Length'] = html.length.toString() 88 | resolve(this) 89 | }, 90 | }), 91 | ) 92 | } 93 | }) 94 | 95 | return handler( 96 | req, 97 | res, 98 | { 99 | public: path.resolve(__dirname, '../docs'), 100 | rewrites: [{ source: '**', destination: '/index.html' }], 101 | }, 102 | { createReadStream }, 103 | ) 104 | }) 105 | 106 | server.listen(port, () => 107 | console.log( 108 | `Listening Marpit documentation on http://127.0.0.1:${port}/ ...`, 109 | ), 110 | ) 111 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import babelParser from '@babel/eslint-parser' 2 | import js from '@eslint/js' 3 | import eslintConfigPrettier from 'eslint-config-prettier' 4 | import eslintPluginImportX, { 5 | flatConfigs as eslintPluginImportXConfigs, 6 | } from 'eslint-plugin-import-x' 7 | import globals from 'globals' 8 | 9 | export default [ 10 | js.configs.recommended, 11 | eslintPluginImportXConfigs.recommended, 12 | eslintConfigPrettier, 13 | { 14 | languageOptions: { 15 | parser: babelParser, 16 | globals: { 17 | ...globals.node, 18 | }, 19 | }, 20 | linterOptions: { 21 | reportUnusedDisableDirectives: 'error', 22 | }, 23 | plugins: { 24 | import: eslintPluginImportX, 25 | }, 26 | rules: { 27 | 'import/order': [ 28 | 'error', 29 | { 30 | alphabetize: { 31 | order: 'asc', 32 | }, 33 | }, 34 | ], 35 | 'max-len': [ 36 | 'error', 37 | { 38 | code: 80, 39 | tabWidth: 2, 40 | ignoreUrls: true, 41 | ignoreComments: false, 42 | ignoreRegExpLiterals: true, 43 | ignoreStrings: true, 44 | ignoreTemplateLiterals: true, 45 | }, 46 | ], 47 | }, 48 | }, 49 | { 50 | files: ['test/**/*', 'jest.*'], 51 | languageOptions: { 52 | globals: { 53 | ...globals.jest, 54 | context: 'readonly', 55 | }, 56 | }, 57 | }, 58 | { 59 | ignores: [ 60 | 'coverage/**/*', 61 | 'dist/**/*', 62 | 'docs/**/*', 63 | 'jsdoc/**/*', 64 | 'lib/**/*', 65 | 'node_modules/**/*', 66 | 'plugin.js', 67 | ], 68 | }, 69 | ] 70 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace MarpitEnv { 2 | interface HTMLAsArray { 3 | htmlAsArray: true 4 | [key: string]: any 5 | } 6 | } 7 | 8 | declare namespace Marpit { 9 | interface Options { 10 | anchor?: boolean | AnchorCallback 11 | container?: false | Element | Element[] 12 | cssContainerQuery?: boolean | string | string[] 13 | cssNesting?: boolean 14 | headingDivider?: false | HeadingDivider | HeadingDivider[] 15 | lang?: string 16 | looseYAML?: boolean 17 | markdown?: any 18 | printable?: boolean 19 | slideContainer?: false | Element | Element[] 20 | inlineSVG?: boolean | InlineSVGOptions 21 | } 22 | 23 | type AnchorCallback = (index: number) => string 24 | 25 | type HeadingDivider = 1 | 2 | 3 | 4 | 5 | 6 26 | 27 | type InlineSVGOptions = { 28 | enabled?: boolean 29 | backdropSelector?: boolean 30 | } 31 | 32 | type RenderResult = { 33 | html: T 34 | css: string 35 | comments: string[][] 36 | } 37 | 38 | type DirectiveDefinitions = { 39 | [directive: string]: ( 40 | value: string | object | (string | object)[], 41 | marpit?: Marpit, 42 | ) => { [meta: string]: any } 43 | } 44 | 45 | type Plugin

= ( 46 | this: Marpit['markdown'] & T, 47 | md: Marpit['markdown'] & T, 48 | ...params: P 49 | ) => void 50 | 51 | type ThemeMetaType = { 52 | [key: string]: StringConstructor | ArrayConstructor 53 | } 54 | 55 | type ThemeReservedMeta = { 56 | theme: string 57 | } 58 | 59 | interface ThemeSetOptions { 60 | cssNesting?: boolean 61 | } 62 | 63 | type ThemeOptions = { 64 | cssNesting?: boolean 65 | metaType?: ThemeMetaType 66 | } 67 | 68 | type ThemeSetPackOptions = { 69 | after?: string 70 | before?: string 71 | containers?: Element[] 72 | printable?: boolean 73 | inlineSVG?: boolean 74 | } 75 | 76 | type PluginFactory =

( 77 | plugin: Plugin, 78 | ) => Plugin 79 | 80 | export class Marpit { 81 | constructor(opts?: Options) 82 | 83 | markdown: any 84 | themeSet: ThemeSet 85 | 86 | readonly customDirectives: { 87 | global: DirectiveDefinitions 88 | local: DirectiveDefinitions 89 | } 90 | readonly options: Options 91 | 92 | protected lastComments: RenderResult['comments'] | undefined 93 | protected lastGlobalDirectives: { [directive: string]: any } | undefined 94 | protected lastSlideTokens: any[] | undefined 95 | protected lastStyles: string[] | undefined 96 | 97 | render(markdown: string, env: MarpitEnv.HTMLAsArray): RenderResult 98 | render(markdown: string, env?: any): RenderResult 99 | 100 | use

(plugin: Plugin

, ...params: P): this 101 | 102 | protected applyMarkdownItPlugins(md: any): void 103 | protected renderMarkdown(markdown: string, env?: any): string 104 | protected renderStyle(theme?: string): string 105 | protected themeSetPackOptions(): ThemeSetPackOptions 106 | } 107 | 108 | export class Element { 109 | constructor(tag: string, attributes?: {}) 110 | 111 | [index: string]: any 112 | tag: string 113 | } 114 | 115 | export class Theme { 116 | protected constructor(name: string, css: string) 117 | 118 | static fromCSS(cssString: string, opts?: ThemeOptions): Readonly 119 | 120 | css: string 121 | height: string 122 | importRules: { 123 | node: any 124 | value: string 125 | }[] 126 | meta: Readonly> 127 | name: string 128 | width: string 129 | 130 | readonly heightPixel: number | undefined 131 | readonly widthPixel: number | undefined 132 | } 133 | 134 | export class ThemeSet { 135 | constructor(opts?: ThemeSetOptions) 136 | 137 | cssNesting: boolean 138 | default: Theme | undefined 139 | metaType: ThemeMetaType 140 | 141 | readonly size: number 142 | private readonly themeMap: Map 143 | 144 | add(css: string): Theme 145 | addTheme(theme: Theme): void 146 | clear(): void 147 | delete(name: string): boolean 148 | get(name: string, fallback?: boolean): Theme | undefined 149 | getThemeMeta( 150 | theme: string | Theme, 151 | meta: string, 152 | ): string | string[] | undefined 153 | getThemeProp(theme: string | Theme, prop: string): any 154 | has(name: string): boolean 155 | pack(name: string, opts: ThemeSetPackOptions): string 156 | themes(): IterableIterator 157 | } 158 | } 159 | 160 | declare module '@marp-team/marpit' { 161 | export = Marpit 162 | } 163 | 164 | declare module '@marp-team/marpit/plugin' { 165 | export const marpitPlugin: Marpit.PluginFactory 166 | export default marpitPlugin 167 | } 168 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['src/**/*.js'], 3 | coverageThreshold: { global: { lines: 95 } }, 4 | restoreMocks: true, 5 | setupFilesAfterEnv: ['/jest.setup.js'], 6 | testEnvironment: 'node', 7 | testRegex: '(/(test|__tests__)/(?!_).*|(\\.|/)(test|spec))\\.js$', 8 | moduleFileExtensions: ['js', 'json', 'node'], 9 | prettierPath: null, 10 | } 11 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | global.context = describe 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marp-team/marpit", 3 | "version": "3.1.3", 4 | "description": "The skinny framework for creating slide deck from Markdown", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Marp team", 8 | "url": "https://github.com/marp-team" 9 | }, 10 | "homepage": "https://marpit.marp.app/", 11 | "contributors": [ 12 | { 13 | "name": "Yuki Hattori", 14 | "url": "https://github.com/yhatt" 15 | } 16 | ], 17 | "keywords": [ 18 | "marp", 19 | "markdown", 20 | "parser", 21 | "slide", 22 | "deck", 23 | "presentation" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/marp-team/marpit" 28 | }, 29 | "engines": { 30 | "node": ">=18" 31 | }, 32 | "main": "lib/index.js", 33 | "types": "index.d.ts", 34 | "files": [ 35 | "lib/", 36 | "index.d.ts", 37 | "plugin.js" 38 | ], 39 | "prettier": { 40 | "semi": false, 41 | "singleQuote": true 42 | }, 43 | "scripts": { 44 | "build": "npm -s run clean && babel src --out-dir lib", 45 | "check:audit": "npm audit", 46 | "check:format": "npm -s run format -- -c", 47 | "clean": "rimraf lib", 48 | "docs": "node ./docsify/serve.js", 49 | "docs:style": "node ./docsify/build.js", 50 | "format": "prettier \"**/*.{css,html,js,json,md,scss,ts,yaml,yml}\"", 51 | "jsdoc": "rimraf jsdoc && jsdoc src -c .jsdoc.json", 52 | "lint:js": "eslint", 53 | "lint:css": "stylelint \"./**/*.{css,scss}\"", 54 | "prepack": "npm-run-all --parallel check:* lint:* test:coverage --sequential build", 55 | "preversion": "run-p check:* lint:* test:coverage", 56 | "test": "jest", 57 | "test:coverage": "jest --coverage", 58 | "version": "curl https://raw.githubusercontent.com/marp-team/actions/v1/lib/scripts/version.js | node && git add -A CHANGELOG.md", 59 | "watch": "babel src --out-dir lib -w --verbose" 60 | }, 61 | "devDependencies": { 62 | "@babel/cli": "^7.27.2", 63 | "@babel/core": "^7.27.1", 64 | "@babel/eslint-parser": "^7.27.1", 65 | "@babel/preset-env": "^7.27.2", 66 | "autoprefixer": "^10.4.21", 67 | "cheerio": "^1.0.0", 68 | "chokidar": "^4.0.3", 69 | "clean-jsdoc-theme": "^4.3.0", 70 | "cssnano": "^7.0.7", 71 | "dedent": "^1.6.0", 72 | "docsify-themeable": "^0.9.0", 73 | "eslint": "^9.26.0", 74 | "eslint-config-prettier": "^10.1.5", 75 | "eslint-plugin-import-x": "^4.11.1", 76 | "globals": "^16.1.0", 77 | "jest": "^29.7.0", 78 | "jest-junit": "^16.0.0", 79 | "jsdoc": "^4.0.4", 80 | "npm-check-updates": "^18.0.1", 81 | "npm-run-all2": "^8.0.1", 82 | "postcss-selector-parser": "^7.1.0", 83 | "prettier": "^3.5.3", 84 | "rimraf": "^6.0.1", 85 | "sass": "1.88.0", 86 | "serve-handler": "^6.1.6", 87 | "stylelint": "^16.19.1", 88 | "stylelint-config-standard-scss": "^15.0.0", 89 | "ws": "^8.18.2" 90 | }, 91 | "dependencies": { 92 | "@csstools/postcss-is-pseudo-class": "^5.0.1", 93 | "cssesc": "^3.0.0", 94 | "js-yaml": "^4.1.0", 95 | "lodash.kebabcase": "^4.1.1", 96 | "markdown-it": "^14.1.0", 97 | "markdown-it-front-matter": "^0.2.4", 98 | "postcss": "^8.5.3", 99 | "postcss-nesting": "^13.0.1" 100 | }, 101 | "publishConfig": { 102 | "access": "public" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/plugin') 2 | -------------------------------------------------------------------------------- /src/element.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | /** 3 | * Marpit element class. 4 | * 5 | * @alias Element 6 | */ 7 | class Element { 8 | /** 9 | * Create a Element instance. 10 | * 11 | * Element instance has compatibility with a plain object that is consists by 12 | * `tag` key and pairs of attribute names and values. A difference is whether 13 | * object has been frozen. 14 | * 15 | * ```js 16 | * import assert from 'assert' 17 | * import { Element } from 'marpit' 18 | * 19 | * const obj = { tag: 'div', class: 'marpit' } 20 | * const elm = new Element('div', { class: 'marpit' }) 21 | * 22 | * // This assertion would pass. 23 | * assert.deepStrictEqual(obj, { ...elm }) 24 | * ``` 25 | * 26 | * @param {string} tag Tag name 27 | * @param {Object} [attributes={}] Tag attributes 28 | */ 29 | constructor(tag, attributes = {}) { 30 | Object.defineProperties(this, { 31 | attributes: { value: attributes }, 32 | tag: { enumerable: true, value: tag }, 33 | }) 34 | 35 | for (const attr of Object.keys(attributes)) { 36 | Object.defineProperty(this, attr, { 37 | enumerable: true, 38 | value: attributes[attr], 39 | }) 40 | } 41 | 42 | Object.freeze(this) 43 | } 44 | } 45 | 46 | /** 47 | * Marpit's default container. 48 | * 49 | * It would output `

`. 50 | * 51 | * @alias module:element.marpitContainer 52 | * @type {Element} 53 | */ 54 | export const marpitContainer = new Element('div', { class: 'marpit' }) 55 | 56 | export default Element 57 | -------------------------------------------------------------------------------- /src/helpers/inline_style.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | 3 | /** 4 | * InlineStyle helper class. 5 | * 6 | * This is the declarative builder of an inline style using PostCSS. The output 7 | * string by `toString()` is sanitized unexpected declarations. 8 | * 9 | * @module 10 | * @alias module:helpers/inline_style 11 | */ 12 | export default class InlineStyle { 13 | /** 14 | * Create an InlineStyle instance. 15 | * 16 | * @function constructor 17 | * @param {Object|String|InlineStyle} [initialDecls] The initial declarations. 18 | */ 19 | constructor(initialDecls) { 20 | this.decls = {} 21 | 22 | if (initialDecls) { 23 | if ( 24 | initialDecls instanceof InlineStyle || 25 | typeof initialDecls === 'string' 26 | ) { 27 | const root = postcss.parse(initialDecls.toString(), { from: undefined }) 28 | 29 | root.each((node) => { 30 | if (node.type === 'decl') this.decls[node.prop] = node.value 31 | }) 32 | } else { 33 | this.decls = { ...initialDecls } 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Delete declaration. 40 | * 41 | * @param {string} prop A property name of declaration. 42 | * @returns {InlineStyle} Returns myself for chaining methods. 43 | */ 44 | delete(prop) { 45 | delete this.decls[prop] 46 | return this 47 | } 48 | 49 | /** 50 | * Set declaration. 51 | * 52 | * @param {string} prop A property name of declaration. 53 | * @param {string} value A value of declaration. 54 | * @returns {InlineStyle} Returns myself for chaining methods. 55 | */ 56 | set(prop, value) { 57 | this.decls[prop] = value 58 | return this 59 | } 60 | 61 | /** 62 | * Build a string of declarations for the inline style. 63 | * 64 | * The unexpected declarations will strip to prevent a style injection. 65 | */ 66 | toString() { 67 | let built = '' 68 | 69 | for (const prop of Object.keys(this.decls)) { 70 | let parsed 71 | 72 | try { 73 | parsed = postcss.parse(`${prop}:${this.decls[prop]}`, { 74 | from: undefined, 75 | }) 76 | } catch { 77 | // A declaration that have value it cannot parse will ignore. 78 | } 79 | 80 | if (parsed) { 81 | parsed.each((node) => { 82 | if (node.type !== 'decl' || node.prop !== prop) node.remove() 83 | }) 84 | 85 | built += `${parsed.toString()};` 86 | } 87 | } 88 | 89 | return built 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/helpers/postcss_plugin.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | 3 | /** 4 | * Generate PostCSS plugin. 5 | * 6 | * This is a glue code generator to migrate existed plugins to support 7 | * PostCSS 8. 8 | * 9 | * @param {string} name Plugin name. 10 | * @param {(Function|Object)} func Function with PostCSS plugin interface. 11 | * @returns {Function} A PostCSS plugin. 12 | */ 13 | export function plugin(name, func) { 14 | return Object.defineProperty( 15 | function intrface(...args) { 16 | const retFunc = func.apply(this, args) 17 | 18 | return Object.defineProperty( 19 | typeof retFunc === 'function' ? { Once: retFunc } : retFunc, 20 | 'postcssPlugin', 21 | { value: name }, 22 | ) 23 | }, 24 | 'postcss', 25 | { value: true }, 26 | ) 27 | } 28 | 29 | export default plugin 30 | -------------------------------------------------------------------------------- /src/helpers/split.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | /** 3 | * Split array into multiple arrays by specified condition. 4 | * 5 | * @param {Array} arr Target array. 6 | * @param {splitCallback} func Callback to split array. 7 | * @param {boolean} [keepSplitedValue=false] Keep splited value. The split 8 | * point is before the matched value. 9 | * @returns {Array[]} Splited array. 10 | */ 11 | export function split(arr, func, keepSplitedValue = false) { 12 | const ret = [[]] 13 | 14 | for (const value of arr) { 15 | /** 16 | * Return true at the split point. 17 | * 18 | * @callback splitCallback 19 | * @param {*} value 20 | */ 21 | if (func(value)) { 22 | ret.push(keepSplitedValue ? [value] : []) 23 | } else { 24 | ret[ret.length - 1].push(value) 25 | } 26 | } 27 | 28 | return ret 29 | } 30 | 31 | export default split 32 | -------------------------------------------------------------------------------- /src/helpers/wrap_array.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | /** 3 | * Wrap value in array if it is not an array. 4 | * 5 | * @function wrapArray 6 | * @param {*} valOrArr 7 | * @return {Array} 8 | */ 9 | export const wrapArray = (valOrArr) => { 10 | if (valOrArr == null || valOrArr === false) return [] 11 | if (valOrArr instanceof Array) return valOrArr 12 | return [valOrArr] 13 | } 14 | 15 | export default wrapArray 16 | -------------------------------------------------------------------------------- /src/helpers/wrap_tokens.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | 3 | /** 4 | * Wrap array of tokens by specified container object. 5 | * 6 | * @param {Token} Token markdown-it's Token class. 7 | * @param {String} type Token type. It will be suffixed by `_open` / `_close`. 8 | * @param {Object} container A container object to wrap tokens, includes tag 9 | * name and attributes. 10 | * @param {String} container.tag The name of container element. 11 | * @param {Object} [container.open] The object assigning to an opening token. 12 | * @param {Object} [container.close] The object assigning to a closing token. 13 | * @param {Token[]} [tokens=[]] Wrapping tokens. 14 | * @returns {Token[]} Wrapped tokens. 15 | */ 16 | export function wrapTokens(Token, type, container, tokens = []) { 17 | const { tag } = container 18 | 19 | // Update nesting level of wrapping tokens 20 | for (const t of tokens) t.level += 1 21 | 22 | // Create markdown-it tokens 23 | const open = new Token(`${type}_open`, tag, 1) 24 | const close = new Token(`${type}_close`, tag, -1) 25 | 26 | Object.assign(open, { ...(container.open || {}) }) 27 | Object.assign(close, { ...(container.close || {}) }) 28 | 29 | // Assign attributes 30 | for (const attr of Object.keys(container)) { 31 | if (!['open', 'close', 'tag'].includes(attr) && container[attr] != null) 32 | open.attrSet(attr, container[attr]) 33 | } 34 | 35 | return [open, ...tokens, close] 36 | } 37 | 38 | export default wrapTokens 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Element from './element' 2 | import Marpit from './marpit' 3 | import Theme from './theme' 4 | import ThemeSet from './theme_set' 5 | 6 | export { Marpit, Element, Theme, ThemeSet } 7 | export default Marpit 8 | -------------------------------------------------------------------------------- /src/markdown/background_image.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | import marpitPlugin from '../plugin' 3 | import advanced from './background_image/advanced' 4 | import apply from './background_image/apply' 5 | import parse from './background_image/parse' 6 | 7 | /** 8 | * Marpit background image plugin. 9 | * 10 | * Convert image token to backgrounds when the alternate text includes `bg`. 11 | * 12 | * When Marpit inline SVG mode is disabled, the image will convert to 13 | * `backgroundImage` and `backgroundSize` spot directive. It supports only 14 | * single background and resizing by using CSS. 15 | * 16 | * When inline SVG mode is enabled, the plugin enables advanced background mode. 17 | * In addition to the basic background implementation, it supports multiple 18 | * background images, filters, and split background. 19 | * 20 | * @function backgroundImage 21 | * @param {MarkdownIt} md markdown-it instance. 22 | */ 23 | function _backgroundImage(md) { 24 | parse(md) 25 | apply(md) 26 | advanced(md) 27 | } 28 | 29 | export const backgroundImage = marpitPlugin(_backgroundImage) 30 | export default backgroundImage 31 | -------------------------------------------------------------------------------- /src/markdown/background_image/apply.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | import marpitPlugin from '../../plugin' 3 | 4 | /** 5 | * Marpit background image apply plugin. 6 | * 7 | * Apply parsed meta for background image / color into directives of each page. 8 | * 9 | * When inline SVG is enabled, it will reshape meta for advanced process instead 10 | * of converting to directives. 11 | * 12 | * @function backgroundImageApply 13 | * @param {MarkdownIt} md markdown-it instance. 14 | */ 15 | function _backgroundImageApply(md) { 16 | md.core.ruler.after( 17 | 'marpit_inline_svg', 18 | 'marpit_apply_background_image', 19 | ({ inlineMode, tokens }) => { 20 | if (inlineMode) return 21 | 22 | let current = {} 23 | 24 | for (const tb of tokens) { 25 | if (tb.type === 'marpit_slide_open') current.open = tb 26 | if (tb.type === 'marpit_inline_svg_content_open') 27 | current.svgContent = tb 28 | 29 | if (tb.type === 'marpit_slide_close') { 30 | if (current.images && current.images.length > 0) { 31 | if (current.svgContent) { 32 | // Reshape meta for advanced background 33 | current.svgContent.meta = { 34 | ...(current.svgContent.meta || {}), 35 | marpitBackground: { 36 | direction: current.direction, 37 | height: current.svgContent.attrGet('height'), 38 | images: current.images, 39 | open: current.open, 40 | split: current.split, 41 | splitSize: current.splitSize, 42 | width: current.svgContent.attrGet('width'), 43 | }, 44 | } 45 | } else { 46 | // Apply simple CSS background 47 | const img = current.images[current.images.length - 1] 48 | 49 | current.open.meta.marpitDirectives = { 50 | ...(current.open.meta.marpitDirectives || {}), 51 | backgroundImage: `url("${img.url}")`, 52 | } 53 | 54 | if (img.size) 55 | current.open.meta.marpitDirectives.backgroundSize = img.size 56 | } 57 | } 58 | current = {} 59 | } 60 | 61 | // Collect parsed inline image meta 62 | if (current.open && tb.type === 'inline') 63 | for (const t of tb.children) { 64 | if (t.type === 'image') { 65 | const { 66 | background, 67 | backgroundDirection, 68 | backgroundSize, 69 | backgroundSplit, 70 | backgroundSplitSize, 71 | color, 72 | filter, 73 | height, 74 | size, 75 | url, 76 | width, 77 | options, 78 | } = t.meta.marpitImage 79 | 80 | if (background && !url.match(/^\s*$/)) { 81 | if (color) { 82 | // Background color 83 | current.open.meta.marpitDirectives = { 84 | ...(current.open.meta.marpitDirectives || {}), 85 | backgroundColor: color, 86 | } 87 | } else { 88 | // Background image 89 | let altText = '' 90 | 91 | for (const opt of options) 92 | if (!opt.consumed) altText += opt.leading + opt.content 93 | 94 | current.images = [ 95 | ...(current.images || []), 96 | { 97 | filter, 98 | height, 99 | size: (() => { 100 | const s = size || backgroundSize || undefined 101 | 102 | return !['contain', 'cover'].includes(s) && 103 | (width || height) 104 | ? `${width || s || 'auto'} ${height || s || 'auto'}` 105 | : s 106 | })(), 107 | url, 108 | width, 109 | alt: altText.trimStart(), 110 | }, 111 | ] 112 | } 113 | } 114 | 115 | if (backgroundDirection) current.direction = backgroundDirection 116 | if (backgroundSplit) current.split = backgroundSplit 117 | if (backgroundSplitSize) current.splitSize = backgroundSplitSize 118 | } 119 | } 120 | } 121 | }, 122 | ) 123 | } 124 | 125 | export const backgroundImageApply = marpitPlugin(_backgroundImageApply) 126 | export default backgroundImageApply 127 | -------------------------------------------------------------------------------- /src/markdown/background_image/parse.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | import marpitPlugin from '../../plugin' 3 | 4 | const bgSizeKeywords = { 5 | auto: 'auto', 6 | contain: 'contain', 7 | cover: 'cover', 8 | fit: 'contain', 9 | } 10 | 11 | const splitSizeMatcher = /^(left|right)(?::((?:\d*\.)?\d+%))?$/ 12 | 13 | /** 14 | * Marpit background image parse plugin. 15 | * 16 | * Parse Marpit's image token and mark as background image when the alternate 17 | * text includes `bg`. The marked images will not show as the regular image. 18 | * 19 | * Furthermore, it parses additional keywords needed for background image. 20 | * 21 | * @function backgroundImageParse 22 | * @param {MarkdownIt} md markdown-it instance. 23 | */ 24 | function _backgroundImageParse(md) { 25 | md.inline.ruler2.after( 26 | 'marpit_parse_image', 27 | 'marpit_background_image', 28 | ({ tokens }) => { 29 | for (const t of tokens) { 30 | if (t.type === 'image') { 31 | const { marpitImage } = t.meta 32 | 33 | if ( 34 | marpitImage.options.some((v) => !v.consumed && v.content === 'bg') 35 | ) { 36 | marpitImage.background = true 37 | t.hidden = true 38 | 39 | for (const opt of marpitImage.options) { 40 | if (opt.consumed) continue 41 | let consumed = false 42 | 43 | // bg keyword 44 | if (opt.content === 'bg') consumed = true 45 | 46 | // Background size keyword 47 | if (bgSizeKeywords[opt.content]) { 48 | marpitImage.backgroundSize = bgSizeKeywords[opt.content] 49 | consumed = true 50 | } 51 | 52 | // Split background keyword 53 | const matched = opt.content.match(splitSizeMatcher) 54 | if (matched) { 55 | const [, splitSide, splitSize] = matched 56 | 57 | marpitImage.backgroundSplit = splitSide 58 | marpitImage.backgroundSplitSize = splitSize 59 | 60 | consumed = true 61 | } 62 | 63 | // Background aligned direction 64 | if (opt.content === 'vertical' || opt.content === 'horizontal') { 65 | marpitImage.backgroundDirection = opt.content 66 | consumed = true 67 | } 68 | 69 | if (consumed) opt.consumed = true 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | ) 76 | } 77 | 78 | export const backgroundImageParse = marpitPlugin(_backgroundImageParse) 79 | export default backgroundImageParse 80 | -------------------------------------------------------------------------------- /src/markdown/collect.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | import marpitPlugin from '../plugin' 3 | 4 | /** 5 | * Marpit collect plugin. 6 | * 7 | * Collect parsed tokens per slide and comments except marked as used for 8 | * internally. These will store to lastSlideTokens and lastComments member of 9 | * Marpit instance. It would use in the returned object from 10 | * {@link Marpit#render}. 11 | * 12 | * @function collect 13 | * @param {MarkdownIt} md markdown-it instance. 14 | */ 15 | function _collect(md) { 16 | const { marpit } = md 17 | 18 | md.core.ruler.push('marpit_collect', (state) => { 19 | if (state.inlineMode) return 20 | 21 | marpit.lastComments = [] 22 | marpit.lastSlideTokens = [] 23 | 24 | let currentPage 25 | let pageIdx = -1 26 | 27 | const collectComment = (token) => { 28 | if ( 29 | currentPage >= 0 && 30 | !(token.meta && token.meta.marpitCommentParsed !== undefined) 31 | ) 32 | marpit.lastComments[currentPage].push(token.content) 33 | } 34 | 35 | const collectable = () => 36 | currentPage >= 0 && marpit.lastSlideTokens[currentPage] !== undefined 37 | 38 | for (const token of state.tokens) { 39 | if (token.meta && token.meta.marpitSlideElement === 1) { 40 | pageIdx += 1 41 | currentPage = pageIdx 42 | 43 | if (marpit.lastSlideTokens[currentPage] === undefined) { 44 | marpit.lastSlideTokens[currentPage] = [token] 45 | marpit.lastComments[currentPage] = [] 46 | } 47 | } else if (token.meta && token.meta.marpitSlideElement === -1) { 48 | if (collectable()) marpit.lastSlideTokens[currentPage].push(token) 49 | currentPage = undefined 50 | } else { 51 | if (collectable()) marpit.lastSlideTokens[currentPage].push(token) 52 | 53 | if (token.type === 'marpit_comment') { 54 | collectComment(token) 55 | } else if (token.type === 'inline') { 56 | for (const t of token.children) 57 | if (t.type === 'marpit_comment') collectComment(t) 58 | } 59 | } 60 | } 61 | }) 62 | } 63 | 64 | export const collect = marpitPlugin(_collect) 65 | export default collect 66 | -------------------------------------------------------------------------------- /src/markdown/comment.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | import marpitPlugin from '../plugin' 3 | import { yaml } from './directives/yaml' 4 | 5 | const commentMatcher = // 8 | 9 | const magicCommentMatchers = [ 10 | // Prettier 11 | /^prettier-ignore(-(start|end))?$/, 12 | 13 | // markdownlint 14 | /^markdownlint-((disable|enable).*|capture|restore)$/, 15 | 16 | // remark-lint (remark-message-control) 17 | /^lint (disable|enable|ignore).*$/, 18 | ] 19 | 20 | export function markAsParsed(token, kind) { 21 | token.meta = token.meta || {} 22 | token.meta.marpitCommentParsed = kind 23 | } 24 | 25 | /** 26 | * Marpit comment plugin. 27 | * 28 | * Parse HTML comment as token. Comments will strip regardless of html setting 29 | * provided by markdown-it. 30 | * 31 | * @function comment 32 | * @param {MarkdownIt} md markdown-it instance. 33 | */ 34 | function _comment(md) { 35 | const parse = (token, content) => { 36 | const parsed = yaml(content, !!md.marpit.options.looseYAML) 37 | 38 | token.meta = token.meta || {} 39 | token.meta.marpitParsedDirectives = parsed === false ? {} : parsed 40 | 41 | // Mark well-known magic comments as parsed comment 42 | for (const magicCommentMatcher of magicCommentMatchers) { 43 | if (magicCommentMatcher.test(content.trim())) { 44 | markAsParsed(token, 'well-known-magic-comment') 45 | break 46 | } 47 | } 48 | } 49 | 50 | md.block.ruler.before( 51 | 'html_block', 52 | 'marpit_comment', 53 | (state, startLine, endLine, silent) => { 54 | // Fast fail 55 | let pos = state.bMarks[startLine] + state.tShift[startLine] 56 | if (state.src.charCodeAt(pos) !== 0x3c) return false 57 | 58 | let max = state.eMarks[startLine] 59 | let line = state.src.slice(pos, max) 60 | 61 | // Match to opening element 62 | if (!commentMatcherOpening.test(line)) return false 63 | if (silent) return true 64 | 65 | // Parse ending element 66 | let nextLine = startLine + 1 67 | if (!commentMatcherClosing.test(line)) { 68 | while (nextLine < endLine) { 69 | if (state.sCount[nextLine] < state.blkIndent) break 70 | 71 | pos = state.bMarks[nextLine] + state.tShift[nextLine] 72 | max = state.eMarks[nextLine] 73 | line = state.src.slice(pos, max) 74 | nextLine += 1 75 | 76 | if (commentMatcherClosing.test(line)) break 77 | } 78 | } 79 | 80 | state.line = nextLine 81 | 82 | // Create token 83 | const token = state.push('marpit_comment', '', 0) 84 | token.map = [startLine, nextLine] 85 | token.markup = state.getLines(startLine, nextLine, state.blkIndent, true) 86 | token.hidden = true 87 | 88 | const matchedContent = commentMatcher.exec(token.markup) 89 | token.content = matchedContent ? matchedContent[1].trim() : '' 90 | parse(token, token.content) 91 | 92 | return true 93 | }, 94 | ) 95 | 96 | md.inline.ruler.before( 97 | 'html_inline', 98 | 'marpit_inline_comment', 99 | (state, silent) => { 100 | const { posMax, src } = state 101 | 102 | // Quick fail by checking `<` and `!` 103 | if ( 104 | state.pos + 2 >= posMax || 105 | src.charCodeAt(state.pos) !== 0x3c || 106 | src.charCodeAt(state.pos + 1) !== 0x21 107 | ) 108 | return false 109 | 110 | const match = src.slice(state.pos).match(commentMatcher) 111 | if (!match) return false 112 | 113 | if (!silent) { 114 | const token = state.push('marpit_comment', '', 0) 115 | 116 | token.hidden = true 117 | token.markup = src.slice(state.pos, state.pos + match[0].length) 118 | token.content = match[1].trim() 119 | 120 | parse(token, token.content) 121 | } 122 | 123 | state.pos += match[0].length 124 | return true 125 | }, 126 | ) 127 | } 128 | 129 | export const comment = marpitPlugin(_comment) 130 | export default comment 131 | -------------------------------------------------------------------------------- /src/markdown/container.js: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | import { wrapArray } from '../helpers/wrap_array' 3 | import { wrapTokens } from '../helpers/wrap_tokens' 4 | import marpitPlugin from '../plugin' 5 | 6 | /** 7 | * Marpit container plugin. 8 | * 9 | * @function container 10 | * @param {MarkdownIt} md markdown-it instance. 11 | */ 12 | function _container(md) { 13 | const containers = wrapArray(md.marpit.options.container) 14 | if (!containers) return 15 | 16 | const target = [...containers].reverse() 17 | 18 | md.core.ruler.push('marpit_containers', (state) => { 19 | if (state.inlineMode) return 20 | 21 | for (const cont of target) 22 | state.tokens = wrapTokens( 23 | state.Token, 24 | 'marpit_containers', 25 | cont, 26 | state.tokens, 27 | ) 28 | }) 29 | } 30 | 31 | export const container = marpitPlugin(_container) 32 | export default container 33 | -------------------------------------------------------------------------------- /src/markdown/directives/directives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The definition of Marpit directives 3 | * @module 4 | */ 5 | 6 | /** 7 | * @typedef {Function} Directive 8 | * @param {string} value Parsed value. 9 | * @param {Marpit} marpit Marpit instance. 10 | * @returns {Object} Assigning object to token meta. 11 | */ 12 | 13 | /** 14 | * Global directives. 15 | * 16 | * Each global directive assigns to the whole slide deck. If you wrote a same 17 | * directive many times, Marpit only recognizes the last value. 18 | * 19 | * @prop {Directive} headingDivider Specify heading divider option. 20 | * @prop {Directive} style Specify the CSS style to apply additionally. 21 | * @prop {Directive} theme Specify theme of the slide deck. 22 | * @prop {Directive} lang Specify the language of the slide deck. It will 23 | * assign as `lang` attribute for each slide. 24 | */ 25 | export const globals = Object.assign(Object.create(null), { 26 | headingDivider: (value) => { 27 | const headings = [1, 2, 3, 4, 5, 6] 28 | const toInt = (v) => 29 | Array.isArray(v) || Number.isNaN(v) ? v : Number.parseInt(v, 10) 30 | const converted = toInt(value) 31 | 32 | if (Array.isArray(converted)) { 33 | const convertedArr = converted.map(toInt) 34 | return { 35 | headingDivider: headings.filter((v) => convertedArr.includes(v)), 36 | } 37 | } 38 | 39 | if (value === 'false') return { headingDivider: false } 40 | if (headings.includes(converted)) return { headingDivider: converted } 41 | 42 | return {} 43 | }, 44 | style: (v) => ({ style: v }), 45 | theme: (v, marpit) => (marpit.themeSet.has(v) ? { theme: v } : {}), 46 | lang: (v) => ({ lang: v }), 47 | }) 48 | 49 | /** 50 | * Local directives. 51 | * 52 | * Mainly these are used to change settings each slide page. By default, a 53 | * local directive applies to the defined page and followed pages. 54 | * 55 | * If you want to set a local directive to single page only, you can add the 56 | * prefix `_` (underbar) to directive name. (Spot directives) 57 | * 58 | * @prop {Directive} backgroundColor Specify background-color style. 59 | * @prop {Directive} backgroundImage Specify background-image style. 60 | * @prop {Directive} backgroundPosition Specify background-position style. The 61 | * default value while setting backgroundImage is `center`. 62 | * @prop {Directive} backgroundRepeat Specify background-repeat style. The 63 | * default value while setting backgroundImage is `no-repeat`. 64 | * @prop {Directive} backgroundSize Specify background-size style. The default 65 | * value while setting backgroundImage is `cover`. 66 | * @prop {Directive} class Specify HTML class of section element(s). 67 | * @prop {Directive} color Specify color style (base text color). 68 | * @prop {Directive} footer Specify the content of slide footer. It will insert 69 | * a `