markdown_background_image_advanced.js

/** @module */
import InlineStyle from '../../helpers/inline_style'
import { wrapTokens } from '../../helpers/wrap_tokens'
import marpitPlugin from '../../plugin'

/**
 * Marpit advanced background image plugin.
 *
 * Support the advanced features for background image, by using `<figure>`
 * element(s) instead of CSS backgrounds. It works by creating the isolated
 * layer into inline SVG.
 *
 * @function advancedBackground
 * @param {MarkdownIt} md markdown-it instance.
 */
function _advancedBackground(md) {
  md.core.ruler.after(
    'marpit_directives_apply',
    'marpit_advanced_background',
    (state) => {
      let current
      const newTokens = []

      for (const t of state.tokens) {
        if (
          t.type === 'marpit_inline_svg_content_open' &&
          t.meta &&
          t.meta.marpitBackground
        ) {
          current = t

          const { height, images, open, width } = t.meta.marpitBackground
          open.attrSet('data-marpit-advanced-background', 'content')

          // Aligned direction
          const direction = t.meta.marpitBackground.direction || 'horizontal'

          // Split backgrounds
          const splitSide = t.meta.marpitBackground.split
          if (splitSide) {
            open.attrSet('data-marpit-advanced-background-split', splitSide)

            const splitBgSize = t.meta.marpitBackground.splitSize || '50%'

            t.attrSet(
              'width',
              `${100 - Number.parseFloat(splitBgSize.slice(0, -1))}%`,
            )

            if (splitSide === 'left') t.attrSet('x', splitBgSize)

            const style = new InlineStyle(open.attrGet('style'))
            style.set('--marpit-advanced-background-split', splitBgSize)
            open.attrSet('style', style.toString())
          }

          // Add the isolated layer for background image
          newTokens.push(
            ...wrapTokens(
              state.Token,
              'marpit_advanced_background_foreign_object',
              { tag: 'foreignObject', width, height },
              wrapTokens(
                state.Token,
                'marpit_advanced_background_section',
                {
                  ...open.attrs.reduce((o, [k, v]) => ({ ...o, [k]: v }), {}),
                  tag: 'section',
                  id: undefined,
                  'data-marpit-advanced-background': 'background',
                },
                wrapTokens(
                  state.Token,
                  'marpit_advanced_background_image_container',
                  {
                    tag: 'div',
                    'data-marpit-advanced-background-container': true,
                    'data-marpit-advanced-background-direction': direction,
                  },
                  (() => {
                    const imageTokens = []

                    // Add multiple image elements
                    for (const img of images) {
                      const style = new InlineStyle({
                        'background-image': `url("${img.url}")`,
                      })

                      // Image sizing
                      if (img.size) style.set('background-size', img.size)

                      // Image filter for backgrounds (Only in advanced BG)
                      if (img.filter) style.set('filter', img.filter)

                      imageTokens.push(
                        ...wrapTokens(
                          state.Token,
                          'marpit_advanced_background_image',
                          {
                            tag: 'figure',
                            style: style.toString(),
                            open: {
                              meta: {
                                marpitBackgroundAlt: img.alt,
                              },
                            },
                          },
                        ),
                      )
                    }

                    return imageTokens
                  })(),
                ),
              ),
            ),
            t,
          )
        } else if (current && t.type === 'marpit_inline_svg_content_close') {
          const { open, height, width } = current.meta.marpitBackground

          // Apply styles
          const style = new InlineStyle()

          if (
            open.meta &&
            open.meta.marpitDirectives &&
            open.meta.marpitDirectives.color
          )
            style.set('color', open.meta.marpitDirectives.color)

          // Add the isolated layer for pseudo contents (e.g. Page number)
          newTokens.push(
            t,
            ...wrapTokens(
              state.Token,
              'marpit_advanced_background_foreign_object',
              {
                tag: 'foreignObject',
                width,
                height,
                'data-marpit-advanced-background': 'pseudo',
              },
              wrapTokens(state.Token, 'marpit_advanced_pseudo_section', {
                ...open.attrs.reduce((o, [k, v]) => ({ ...o, [k]: v }), {}),
                tag: 'section',
                id: undefined,
                style: style.toString(),
                'data-marpit-advanced-background': 'pseudo',
              }),
            ),
          )

          current = undefined
        } else {
          newTokens.push(t)
        }
      }

      state.tokens = newTokens
    },
  )

  // Renderer for advanced background image
  md.renderer.rules.marpit_advanced_background_image_open = (
    tokens,
    idx,
    options,
    _env,
    self,
  ) => {
    const token = tokens[idx]
    const open = self.renderToken(tokens, idx, options)

    if (token.meta?.marpitBackgroundAlt) {
      return `${open}<figcaption>${md.utils.escapeHtml(
        token.meta.marpitBackgroundAlt,
      )}</figcaption>`
    }

    return open
  }
}

export const advancedBackground = marpitPlugin(_advancedBackground)
export default advancedBackground