Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

::backdrop selector support for inline SVG mode #319

Merged
merged 7 commits into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [Unreleased]

### Added

- [`::backdrop` pseudo-element](https://marpit.marp.app/inline-svg?id=backdrop-css-selector) matches to the container SVG when enabled inline SVG mode ([#313](https://github.com/marp-team/marpit/issues/313), [#319](https://github.com/marp-team/marpit/pull/319))
- Allow setting [the option object](https://marpit-api.marp.app/marpit#~InlineSVGOptions) to `inlineSVG` constructor option ([#319](https://github.com/marp-team/marpit/pull/319))

### Fixed

- Remove recognized image keywords from alt text ([#316](https://github.com/marp-team/marpit/issues/316), [#318](https://github.com/marp-team/marpit/pull/318))
Expand Down
49 changes: 46 additions & 3 deletions docs/inline-svg.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ When you set [`inlineSVG: true` in Marpit constructor option](/usage#triangular_
</svg>
```

### Options <!-- {docsify-ignore} -->

`inlineSVG` constructor option also allows setting the option object. Refer to [Marpit API documentation](https://marpit-api.marp.app/marpit#~InlineSVGOptions) for details.

## Motivation

It might feel a bit strange, but this approach has certain advantages.
You may feel it a bit strange. Why we have taken this approach?

### Pixel-perfect scaling

Expand Down Expand Up @@ -57,9 +61,11 @@ Thus, a minimal web-based presentation no longer requires JavaScript. We strongl

### Isolated layer

Marpit's [advanced backgrounds](/image-syntax#advanced-backgrounds) would work within the isolated `<foreignObject>` from the content. It means that the original Markdown DOM structure per page are keeping.
Marpit's [advanced backgrounds](/image-syntax#advanced-backgrounds) will work within the isolated `<foreignObject>` from the content. It means that the original Markdown DOM structure per page are keeping.

If advanced backgrounds were injected into the same layer as the Markdown content, inserted elements may break CSS selectors like [`:first-child` pseudo-class](https://developer.mozilla.org/docs/Web/CSS/:first-child) and [adjacent combinator (`+`)](https://developer.mozilla.org/docs/Web/CSS/Adjacent_sibling_combinator).

## Polyfill
## Webkit polyfill

We provide a polyfill for WebKit based browsers in [@marp-team/marpit-svg-polyfill](https://github.com/marp-team/marpit-svg-polyfill).

Expand All @@ -73,3 +79,40 @@ We provide a polyfill for WebKit based browsers in [@marp-team/marpit-svg-polyfi
<!-- Apply polyfill -->
<script src="https://cdn.jsdelivr.net/npm/@marp-team/marpit-svg-polyfill/lib/polyfill.browser.js"></script>
```

## `::backdrop` CSS selector

If enabled inline SVG mode, Marpit theme CSS and inline styles will redirect [`::backdrop` CSS selector](https://developer.mozilla.org/docs/Web/CSS/::backdrop) to the SVG container.

A following rule matches to `<svg data-marpit-svg>` element.

```css
::backdrop {
background-color: #448;
}
```

Some of Marpit integrated apps treats the background of SVG container as like as the backdrop of slide. By setting `background` style to the SVG container, you can change the color/image of slide [letterbox](<https://en.wikipedia.org/wiki/Letterboxing_(filming)>)/[pillarbox](https://en.wikipedia.org/wiki/Pillarbox).

Try resizing SVG container in below:

<div style="width:300px;height:250px;min-width:100px;min-height:100px;max-width:100%;resize:both;margin:0 auto;overflow: scroll;">
<svg data-marpit-svg viewBox="0 0 1280 720" style="background-color:#448;width:100%;height:100%;display:block;">
<foreignObject width="1280" height="720">
<section style="width:1280px;height:720px;background:url('https://images.unsplash.com/photo-1637224671997-6dd7f74092a7?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=720&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNzMzMDc0Ng&ixlib=rb-1.2.1&q=80&w=1280');">
</section>
</foreignObject>
</svg>
</div>

!> `::backdrop` pseudo-element does not limit applicable styles. To avoid unexpected effects into slides and apps, we strongly recommend to use this selector _only for changing the backdrop color_.

### Disable

If concerned conflict with styles for SVG container provided by app you are creating, you can disable `::backdrop` selector redirection separately by setting `backdropSelector` option as `false`.

```javascript
const marpit = new Marpit({
inlineSVG: { backdropSelector: false },
})
```
7 changes: 6 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ declare namespace Marpit {
markdown?: any
printable?: boolean
slideContainer?: false | Element | Element[]
inlineSVG?: boolean
inlineSVG?: boolean | InlineSVGOptions
}

type HeadingDivider = 1 | 2 | 3 | 4 | 5 | 6

type InlineSVGOptions = {
enabled?: boolean
backdropSelector?: boolean
}

type RenderResult<T = string> = {
html: T
css: string
Expand Down
4 changes: 2 additions & 2 deletions src/markdown/background_image.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import parse from './background_image/parse'
*
* Convert image token to backgrounds when the alternate text includes `bg`.
*
* When Marpit `inlineSVG` option is `false`, the image will convert to
* When Marpit inline SVG mode is disabled, the image will convert to
* `backgroundImage` and `backgroundSize` spot directive. It supports only
* single background and resizing by using CSS.
*
* When `inlineSVG` option is true, the plugin enables advanced background mode.
* When inline SVG mode is enabled, the plugin enables advanced background mode.
* In addition to the basic background implementation, it supports multiple
* background images, filters, and split background.
*
Expand Down
6 changes: 5 additions & 1 deletion src/markdown/inline_svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ function inlineSVG(md) {
'marpit_directives_parse',
'marpit_inline_svg',
(state) => {
if (!marpit.options.inlineSVG || state.inlineMode) return
if (
!(marpit.inlineSVGOptions && marpit.inlineSVGOptions.enabled) ||
state.inlineMode
)
return

const { themeSet, lastGlobalDirectives } = marpit
const w = themeSet.getThemeProp(lastGlobalDirectives.theme, 'widthPixel')
Expand Down
31 changes: 28 additions & 3 deletions src/marpit.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,25 @@ const defaultOptions = {
inlineSVG: false,
}

const defaultInlineSVGOptions = {
enabled: true,
backdropSelector: true,
}

/**
* Parse Marpit Markdown and render to the slide HTML/CSS.
*/
class Marpit {
#markdown = undefined

/**
* @typedef {Object} Marpit~InlineSVGOptions
* @property {boolean} [enabled=true] Whether inline SVG mode is enabled.
* @property {boolean} [backdropSelector=true] Whether `::backdrop` selector
* support is enabled. If enabled, the `::backdrop` CSS selector will
* match to the SVG container element.
*/

/**
* Create a Marpit instance.
*
Expand All @@ -54,8 +67,8 @@ class Marpit {
* @param {boolean} [opts.printable=true] Make style printable to PDF.
* @param {false|Element|Element[]} [opts.slideContainer] Container element(s)
* wrapping each slide sections.
* @param {boolean} [opts.inlineSVG=false] Wrap each sections by inline SVG.
* _(Experimental)_
* @param {boolean|Marpit~InlineSVGOptions} [opts.inlineSVG=false] Wrap each
* slide sections by inline SVG. _(Experimental)_
*/
constructor(opts = {}) {
/**
Expand Down Expand Up @@ -225,11 +238,23 @@ class Marpit {
...wrapArray(this.options.container),
...wrapArray(this.options.slideContainer),
],
inlineSVG: this.options.inlineSVG,
inlineSVG: this.inlineSVGOptions,
printable: this.options.printable,
}
}

/**
* @private
* @returns {Marpit~InlineSVGOptions} Options for inline SVG.
*/
get inlineSVGOptions() {
if (typeof this.options.inlineSVG === 'object') {
return { ...defaultInlineSVGOptions, ...this.options.inlineSVG }
}

return { ...defaultInlineSVGOptions, enabled: !!this.options.inlineSVG }
}

/**
* Load the specified markdown-it plugin with given parameters.
*
Expand Down
81 changes: 81 additions & 0 deletions src/postcss/svg_backdrop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/** @module */
import postcssPlugin from '../helpers/postcss_plugin'

const backdropMatcher = /(?:\b|^)::backdrop$/

/**
* Marpit PostCSS SVG backdrop plugin.
*
* Retarget `::backdrop` and `section::backdrop` selector to
* `@media screen { :marpit-container > svg[data-marpit-svg] { .. } }`. It means
* `::backdrop` targets the SVG container in inline SVG mode.
*
* It's useful for setting style of the letterbox and pillarbox in the SVG
* scaled slide.
*
* ```css
* ::backdrop {
* background-color: #448;
* }
* ```
*
* The original definition will remain to support an original usage of
* `::backdrop`.
*
* The important differences from an original `::backdrop` are following:
*
* - In original spec, `::backdrop` creates a separated layer from the target
* element, but Marpit's `::backdrop` does not. The slide elements still
* become the child of `::backdrop` so setting some properties that are
* inherited may make broken slide rendering.
* - Even if the browser is not fullscreen, `::backdrop` will match to SVG
* container whenever matched to `@media screen` media query.
*
* If concerned to conflict with the style provided by the app, consider to
* disable the selector support by `inlineSVG: { backdropSelector: false }`.
*
* @see https://developer.mozilla.org/docs/Web/CSS/::backdrop
* @alias module:postcss/svg_backdrop
*/
const plugin = postcssPlugin(
'marpit-postcss-svg-backdrop',
() => (css, postcss) => {
css.walkRules((rule) => {
const injectSelectors = new Set()

for (const selector of rule.selectors) {
// Detect pseudo-element (must appear after the simple selectors)
if (!selector.match(backdropMatcher)) continue

// Detect whether the selector is targeted to section
const delimiterMatched = selector.match(/[.:#[]/) // must match
const target = selector.slice(0, delimiterMatched.index)

if (target === 'section' || target === '') {
const delimiter = selector.slice(delimiterMatched.index, -10)
injectSelectors.add(
`:marpit-container > svg[data-marpit-svg]${delimiter}`
)
}
}

if (injectSelectors.size > 0 && rule.nodes.length > 0) {
rule.parent.insertAfter(
rule,
postcss.atRule({
name: 'media',
params: 'screen',
nodes: [
postcss.rule({
selectors: [...injectSelectors.values()],
nodes: rule.nodes,
}),
],
})
)
}
})
}
)

export default plugin
14 changes: 10 additions & 4 deletions src/theme_set.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import postcssRootIncreasingSpecificity, {
} from './postcss/root/increasing_specificity'
import postcssRem from './postcss/root/rem'
import postcssRootReplace from './postcss/root/replace'
import postcssSVGBackdrop from './postcss/svg_backdrop'
import Theme from './theme'
import scaffold from './theme/scaffold'

Expand Down Expand Up @@ -236,16 +237,18 @@ class ThemeSet {
* @param {Element[]} [opts.containers] Container elements wrapping whole
* slide deck.
* @param {boolean} [opts.printable] Make style printable to PDF.
* @param {boolean} [opts.inlineSVG] Apply a hierarchy of inline SVG to CSS
* selector by setting `true`. _(Experimental)_
* @param {Marpit~InlineSVGOptions} [opts.inlineSVG] Apply a hierarchy of
* inline SVG to CSS selector by setting `true`. _(Experimental)_
* @return {string} The converted CSS string.
*/
pack(name, opts = {}) {
const slideElements = [{ tag: 'section' }]
const theme = this.get(name, true)
const inlineSVGOpts = opts.inlineSVG || {}

if (opts.inlineSVG)
if (inlineSVGOpts.enabled) {
slideElements.unshift({ tag: 'svg' }, { tag: 'foreignObject' })
}

const additionalCSS = (css) => {
if (!css) return undefined
Expand Down Expand Up @@ -283,7 +286,10 @@ class ThemeSet {
'marpit-pack-scaffold',
() => (css) => css.first.before(scaffold.css)
),
opts.inlineSVG && postcssAdvancedBackground,
inlineSVGOpts.enabled && postcssAdvancedBackground,
inlineSVGOpts.enabled &&
inlineSVGOpts.backdropSelector &&
postcssSVGBackdrop,
postcssPagination,
postcssRootReplace({ pseudoClass }),
postcssRootFontSize,
Expand Down
3 changes: 2 additions & 1 deletion test/markdown/background_image.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('Marpit background image plugin', () => {
customDirectives: { global: {}, local: {} },
lastGlobalDirectives: {},
themeSet: { getThemeProp: () => 100 },
options: { inlineSVG: svg },
options: {},
inlineSVGOptions: { enabled: svg },
})

const md = (svg = false) => {
Expand Down
3 changes: 2 additions & 1 deletion test/markdown/collect.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('Marpit collect plugin', () => {
themeSet,
customDirectives: { global: {}, local: {} },
lastGlobalDirectives: {},
options: { inlineSVG: svg },
options: {},
inlineSVGOptions: { enabled: svg },
})

const md = (marpitInstance) => {
Expand Down
7 changes: 5 additions & 2 deletions test/markdown/inline_svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ describe('Marpit inline SVG plugin', () => {
customDirectives: { global: {}, local: {} },
themeSet: new ThemeSet(),
lastGlobalDirectives: {},
options: { inlineSVG: true },
options: {},
inlineSVGOptions: { enabled: true },
...props,
})

Expand Down Expand Up @@ -52,7 +53,9 @@ describe('Marpit inline SVG plugin', () => {
})

it('ignores when Marpit inlineSVG option is false', () => {
const marpitStubInstance = marpitStub({ options: { inlineSVG: false } })
const marpitStubInstance = marpitStub({
inlineSVGOptions: { enabled: false },
})
const $ = render(md(marpitStubInstance), '# test\n\n---\n\n# test')
expect($('svg')).toHaveLength(0)
})
Expand Down
Loading