"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _postcss = _interopRequireDefault(require("postcss"));
var _advanced_background = _interopRequireDefault(require("./postcss/advanced_background"));
var _after = _interopRequireDefault(require("./postcss/after"));
var _before = _interopRequireDefault(require("./postcss/before"));
var _container_query = _interopRequireWildcard(require("./postcss/container_query"));
var _hoisting = _interopRequireDefault(require("./postcss/import/hoisting"));
var _replace = _interopRequireDefault(require("./postcss/import/replace"));
var _suppress = _interopRequireDefault(require("./postcss/import/suppress"));
var _nesting = _interopRequireDefault(require("./postcss/nesting"));
var _pagination = _interopRequireDefault(require("./postcss/pagination"));
var _printable = _interopRequireWildcard(require("./postcss/printable"));
var _prepend = _interopRequireDefault(require("./postcss/pseudo_selector/prepend"));
var _replace2 = _interopRequireDefault(require("./postcss/pseudo_selector/replace"));
var _font_size = _interopRequireDefault(require("./postcss/root/font_size"));
var _increasing_specificity = _interopRequireWildcard(require("./postcss/root/increasing_specificity"));
var _rem = _interopRequireDefault(require("./postcss/root/rem"));
var _replace3 = _interopRequireDefault(require("./postcss/root/replace"));
var _scaffold = _interopRequireDefault(require("./postcss/scaffold"));
var _svg_backdrop = _interopRequireDefault(require("./postcss/svg_backdrop"));
var _theme = _interopRequireDefault(require("./theme"));
var _scaffold2 = _interopRequireDefault(require("./theme/scaffold"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const defaultOptions = {
  cssNesting: false
};

/**
 * Marpit theme set class.
 */
class ThemeSet {
  #plugins = [];

  /**
   * Create a ThemeSet instance.
   *
   * @param {Object} [opts]
   * @param {boolean} [opts.cssNesting=true] Enable CSS nesting support.
   */
  constructor(opts = defaultOptions) {
    /**
     * An instance of default theme.
     *
     * While running {@link ThemeSet#pack}, ThemeSet will use this theme when
     * the definition of theme directive or the theme with specified name is not
     * found.
     *
     * By default, Marpit does not provide default theme (`undefined`).
     *
     * @type {Theme|undefined}
     */
    this.default = undefined;

    /**
     * The default type settings for theme metadata added by
     * {@link ThemeSet#add}.
     *
     * A key of object is the name of metadata and a value is the type which of
     * `String` and `Array`. You have to set `Array` if the theme allows
     * multi-time definitions in same meta key.
     *
     * ```css
     * /**
     *  * @theme example
     *  * @foo Single value
     *  * @foo allows only one string
     *  * @bar Multiple value 1
     *  * @bar Multiple value 2
     *  * @bar Multiple value 3
     *  * ...
     * ```
     *
     * ```js
     * const themeSet = new ThemeSet()
     *
     * themeSet.metaType = {
     *   foo: String,
     *   bar: Array,
     * }
     *
     * themeSet.add(css)
     *
     * console.log(themeSet.getThemeMeta('example', 'foo'))
     * // => 'allows only one string'
     *
     * console.log(themeSet.getThemeMeta('example', 'bar'))
     * // => ['Multiple value 1', 'Multiple value 2', 'Multiple value 3']
     * ```
     *
     * @type {Object}
     */
    this.metaType = {};

    /**
     * A boolean value indicating whether the theme set is enabling CSS nesting
     * or not.
     *
     * @type {boolean}
     */
    this.cssNesting = !!opts.cssNesting;
    Object.defineProperty(this, 'themeMap', {
      value: new Map()
    });
  }

  /**
   * Return the number of themes.
   *
   * @type {number}
   * @readonly
   */
  get size() {
    return this.themeMap.size;
  }

  /**
   * Add theme CSS from string.
   *
   * @param {string} css The theme CSS string.
   * @returns {Theme} A created {@link Theme} instance.
   * @throws Will throw an error if the theme name is not specified by `@theme`
   *     metadata.
   */
  add(css) {
    const theme = _theme.default.fromCSS(css, {
      metaType: this.metaType,
      cssNesting: this.cssNesting
    });
    this.addTheme(theme);
    return theme;
  }

  /**
   * Add theme instance.
   *
   * @param {Theme} theme The theme instance.
   * @throws Will throw an error if the theme name is not specified.
   */
  addTheme(theme) {
    if (!(theme instanceof _theme.default)) throw new Error('ThemeSet can add only an instance of Theme.');
    if (typeof theme.name !== 'string') throw new Error('An instance of Theme requires name.');
    this.themeMap.set(theme.name, theme);
  }

  /**
   * Removes all themes from a {@link themeSet} object.
   */
  clear() {
    return this.themeMap.clear();
  }

  /**
   * Remove a specific named theme from a {@link themeSet} object.
   *
   * @param {string} name The theme name to delete.
   * @returns {boolean} Returns `true` if a theme in current {@link ThemeSet}
   *     existed and has been removed, or `false` if the theme does not exist.
   */
  delete(name) {
    return this.themeMap.delete(name);
  }

  /**
   * Returns a specific named theme.
   *
   * @param {string} name The theme name to get.
   * @param {boolean} [fallback=false] If true, return instance's default theme
   *     or scaffold theme when specified theme cannot find.
   * @returns {Theme|undefined} Returns specified or fallback theme, or
   *     `undefined` if `fallback` is false and the specified theme has not
   *     existed.
   */
  get(name, fallback = false) {
    const theme = this.themeMap.get(name);
    return fallback ? theme || this.default || _scaffold2.default : theme;
  }

  /**
   * Returns value(s) of specified metadata from a theme. It considers `@import`
   * and `@import-theme` rules in getting meta value. On the other hand, the
   * default theme specified by the instance is not considered.
   *
   * To support metadata with array type, it will merge into a flatten array
   * when the all of got valid values that includes imported themes are array.
   *
   * @param {string|Theme} theme The theme name or instance.
   * @param {string} meta The meta name to get.
   * @returns {string|string[]|undefined}
   */
  getThemeMeta(theme, meta) {
    const themeInstance = theme instanceof _theme.default ? theme : this.get(theme);
    const metas = themeInstance ? this.resolveImport(themeInstance).map(t => t.meta[meta]).filter(m => m) : [];

    // Flatten in order of definitions when the all of valid values are array
    if (metas.length > 0 && metas.every(m => Array.isArray(m))) {
      const mergedArray = [];
      for (const m of metas) mergedArray.unshift(...m);
      return mergedArray;
    }
    return metas[0];
  }

  /**
   * Returns the value of specified property name from a theme. It considers
   * `@import` and `@import-theme` rules in getting value.
   *
   * It will fallback the reference object into the instance's default theme or
   * scaffold theme when the specified theme is `undefined`.
   *
   * @param {string|Theme} theme The theme name or instance.
   * @param {string} prop The property name to get.
   * @returns {*}
   */
  getThemeProp(theme, prop) {
    const themeInstance = theme instanceof _theme.default ? theme : this.get(theme);
    const props = themeInstance ? this.resolveImport(themeInstance).map(t => t[prop]) : [];
    return [...props, this.default && this.default[prop], _scaffold2.default[prop]].find(t => t);
  }

  /**
   * Returns a boolean indicating whether a specific named theme exists or not.
   *
   * @param {string} name The theme name.
   * @returns {boolean} Returns `true` if a specific named theme exists,
   *     otherwise `false`.
   */
  has(name) {
    return this.themeMap.has(name);
  }

  /**
   * Convert registered theme CSS into usable in the rendered markdown by
   * {@link Marpit#render}.
   *
   * **This method is designed for internal use by {@link Marpit} class.** Use
   * {@link Marpit#render} instead unless there is some particular reason.
   *
   * @param {string} name The theme name. It will use the instance's default
   *     theme or scaffold theme when a specific named theme does not exist.
   * @param {Object} [opts] The option object passed by {@link Marpit#render}.
   * @param {string} [opts.after] A CSS string to append into after theme.
   * @param {string} [opts.before] A CSS string to prepend into before theme.
   * @param {Element[]} [opts.containers] Container elements wrapping whole
   *     slide deck.
   * @param {boolean|string|string[]} [opts.containerQuery] Enable CSS container
   *     query by setting `true`. You can also specify the name of container for
   *     CSS container query used by the `@container` at-rule in child elements.
   * @param {boolean} [opts.printable] Make style printable to PDF.
   * @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 theme = this.get(name, true);
    const inlineSVGOpts = opts.inlineSVG || {};
    const slideElements = [...(inlineSVGOpts.enabled ? [{
      tag: 'svg'
    }, {
      tag: 'foreignObject'
    }] : []), {
      tag: 'section'
    }];
    const containerName = typeof opts.containerQuery === 'string' || Array.isArray(opts.containerQuery) ? opts.containerQuery : undefined;
    const runPostCSS = (css, plugins) => (0, _postcss.default)([this.cssNesting && (0, _nesting.default)(), ...plugins].filter(p => p)).process(css).css;
    const normalizeExtraCSS = css => {
      if (!css) return undefined;
      try {
        return runPostCSS(css, [(0, _suppress.default)(this)]);
      } catch {
        return undefined;
      }
    };
    const after = normalizeExtraCSS(opts.after);
    const before = normalizeExtraCSS(opts.before);
    return runPostCSS(theme.css, [before && (0, _before.default)(before), after && (0, _after.default)(after), opts.containerQuery && (0, _container_query.default)(containerName), _hoisting.default, (0, _replace.default)(this), opts.printable && (0, _printable.default)({
      width: this.getThemeProp(theme, 'width'),
      height: this.getThemeProp(theme, 'height')
    }), theme !== _scaffold2.default && _scaffold.default, inlineSVGOpts.enabled && _advanced_background.default, inlineSVGOpts.enabled && inlineSVGOpts.backdropSelector && _svg_backdrop.default, _pagination.default, (0, _replace3.default)({
      pseudoClass: _increasing_specificity.pseudoClass
    }), _font_size.default, _prepend.default, (0, _replace2.default)(opts.containers, slideElements), _increasing_specificity.default, opts.printable && _printable.postprocess, opts.containerQuery && _container_query.postprocess, _rem.default, _hoisting.default, ...this.#plugins]);
  }

  /**
   * Returns a `Iterator` object that contains registered themes to current
   * instance.
   *
   * @returns {Iterator.<Theme>}
   */
  themes() {
    return this.themeMap.values();
  }

  /**
   * Resolves `@import` and `@import-theme` and returns an array of using theme
   * instances.
   *
   * @private
   * @param {Theme} theme Theme instance
   * @returns {Theme[]}
   */
  resolveImport(theme, importedThemes = []) {
    const {
      name
    } = theme;
    if (importedThemes.includes(name)) throw new Error(`Circular "${name}" theme import is detected.`);
    const resolvedThemes = [theme];
    theme.importRules.forEach(m => {
      const importTheme = this.get(m.value);
      if (importTheme) resolvedThemes.push(...this.resolveImport(importTheme, [...importedThemes, name].filter(n => n)));
    });
    return resolvedThemes.filter(v => v);
  }

  /**
   * Register an additional PostCSS plugin for processing the theme CSS.
   *
   * @private
   * @param {Function} plugin A PostCSS plugin function to register.
   * @return {ThemeSet} The current ThemeSet instance for chainable.
   */
  use(plugin) {
    this.#plugins.push(plugin);
    return this;
  }
}
var _default = exports.default = ThemeSet;