├── .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< parameters.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 | [](https://circleci.com/gh/marp-team/marp-core/)
4 | [](https://codecov.io/gh/marp-team/marp-core)
5 | [](https://www.npmjs.com/package/@marp-team/marp-core)
6 | [](./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 `
Markdown | 103 |Rendered slide | 104 |
---|---|
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 | | 123 |124 | 125 |  126 | 127 | | 128 |
to', () => { 77 | document.body.innerHTML = 78 | ' 12' 79 | 80 | browser.applyCustomElements() 81 | 82 | expect(document.body.innerHTML).toMatchInlineSnapshot( 83 | `"1 2 "`, 84 | ) 85 | }) 86 | 87 | it('does not throw known DOMException error while upgradingto(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 upgradingto', () => { 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 replaceto
', () => { 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}">${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 | "-th order
5 | " 6 | `; 7 | 8 | exports[`markdown-it math plugin for KaTeX can appear both maths in lists 1`] = ` 9 | "10 |
15 | " 16 | `; 17 | 18 | exports[`markdown-it math plugin for KaTeX does not allow paragraph break in inline math 1`] = ` 19 | "- 11 |
- 12 |
14 |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 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 | "39 | " 40 | `; 41 | 42 | exports[`markdown-it math plugin for KaTeX does not render math when delimiters are escaped 1`] = ` 43 | "$$ 36 | 1+1 = 2 37 | $$ 38 |
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: .
62 | " 63 | `; 64 | 65 | exports[`markdown-it math plugin for KaTeX recognizes multiline escaped delimiters in math module 1`] = ` 66 | "Weird-o: .
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 | "" 75 | `; 76 | 77 | exports[`markdown-it math plugin for KaTeX renders block math with indented up to 3 spaces 1`] = ` 78 | "
" 80 | `; 81 | 82 | exports[`markdown-it math plugin for KaTeX renders block math with self-closing at the end of document 1`] = `"
"`; 83 | 84 | exports[`markdown-it math plugin for KaTeX renders block math written in one line 1`] = ` 85 | "
" 87 | `; 88 | 89 | exports[`markdown-it math plugin for KaTeX renders math even when it starts with a negative sign 1`] = ` 90 | "
foobar
91 | " 92 | `; 93 | 94 | exports[`markdown-it math plugin for KaTeX renders math without whitespace before and after delimiter 1`] = ` 95 | "foobar
96 | " 97 | `; 98 | 99 | exports[`markdown-it math plugin for KaTeX renders multiline display math 1`] = ` 100 | "" 107 | `; 108 | 109 | exports[`markdown-it math plugin for KaTeX renders multiline inline math 1`] = ` 110 | "
foo bar
112 | " 113 | `; 114 | 115 | exports[`markdown-it math plugin for KaTeX renders simple block math 1`] = ` 116 | "" 118 | `; 119 | 120 | exports[`markdown-it math plugin for KaTeX renders simple inline math 1`] = ` 121 | "
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', 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 | [][example] 34 | [][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 | [][example] 61 | [][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 |  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 |  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 | [][example] 139 | [][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 |  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 | --------------------------------------------------------------------------------