view/comment/waline.jsx

/**
 * Waline comment JSX component.
 * @module view/comment/waline
 */
const { Component } = require('inferno');
const { cacheComponent } = require('../../util/cache');
const { toJs } = require('../../util/script');
const { getAppearance } = require('../../util/appearance');

/**
 * Waline comment JSX component.
 *
 * @see https://waline.js.org/guide/get-started.html
 * @example
 * <Waline
 *     serverURL="https://path/to/waline/server"
 *     path="window.location.pathname"
 *     lang="zh-CN"
 *     locale={{placeholder: "", ...}}
 *     emoji={["https://cdn.jsdelivr.net/gh/walinejs/emojis/weibo"]}
 *     dark="auto"
 *     meta={["nick", "mail", "link"]}
 *     requiredMeta={[]}
 *     login="enable"
 *     wordLimit={0},
 *     pageSize={10}
 *     imageUploader={true}
 *     highlighter={true}
 *     texRenderer={false}
 *     search={true}
 *     pageview={false}
 *     comment={false}
 *     copyright={true}
 *     jsUrl="/path/to/Waline.js" />
 */
class Waline extends Component {
  render() {
    const {
      serverURL,
      path,
      lang = 'zh-CN',
      locale,
      emoji = ['https://cdn.jsdelivr.net/gh/walinejs/emojis/weibo'],
      dark,
      meta = ['nick', 'mail', 'link'],
      requiredMeta = [],
      login = 'enable',
      wordLimit = 0,
      pageSize = 10,
      imageUploader,
      highlighter,
      texRenderer,
      search,
      pageview = false,
      comment = false,
      copyright = true,
      jsUrl,
      cssUrl,
    } = this.props;
    if (!serverURL) {
      return (
        <div class="notification is-danger">
          You forgot to set the <code>server_url</code> for Waline. Please set it in{' '}
          <code>_config.yml</code>.
        </div>
      );
    }
    // Without the dark option, follow the appearance of the page: the system color scheme for the auto
    // appearance, or the data-appearance="dark" attribute of <html> otherwise
    const followAppearance = dark === null || typeof dark === 'undefined' || dark === '';
    const darkOption = followAppearance
      ? `${getAppearance()} === 'auto' ? 'auto' : 'html[data-appearance="dark"]'`
      : toJs(dark);
    // A path, such as /posts/hello/, or a JavaScript expression, such as window.location.pathname
    let pathCode = path || 'window.location.pathname';
    if (pathCode.startsWith('/')) {
      pathCode = toJs(pathCode);
    }
    // Use a classic script with a dynamic import instead of a module script, so that PJAX, which only
    // runs classic scripts, initializes Waline again after switching pages. The previous instance,
    // whose element PJAX has replaced, is destroyed first.
    const appearanceObserver = followAppearance
      ? `var instance = window.__walineInstance;
        if (instance) {
          var observer = new MutationObserver(function () {
            instance.update({ dark: ${darkOption}, path: path });
          });
          observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-appearance'] });
          var destroy = instance.destroy;
          instance.destroy = function () {
            observer.disconnect();
            destroy.call(instance);
          };
        }`
      : '';
    const js = `(function () {
      var container = document.getElementById('waline-thread');
      var path = ${pathCode};
      return import(${toJs(jsUrl)}).then(function (Waline) {
        if (!container || !container.isConnected || document.getElementById('waline-thread') !== container) return;
        if (window.__walineInstance) {
            window.__walineInstance.destroy();
        }
        window.__walineInstance = Waline.init({
            el: '#waline-thread',
            serverURL: ${toJs(serverURL)},
            path: path,
            ${lang ? `lang: ${toJs(lang)},` : ''}
            ${locale ? `locale: ${toJs(locale)},` : ''}
            ${emoji !== null && typeof emoji !== 'undefined' ? `emoji: ${toJs(emoji)},` : ''}
            dark: ${darkOption},
            ${meta ? `meta: ${toJs(meta)},` : ''}
            ${Array.isArray(requiredMeta) ? `requiredMeta: ${toJs(requiredMeta)},` : ''}
            ${login ? `login: ${toJs(login)},` : ''}
            ${wordLimit ? `wordLimit: ${toJs(wordLimit)},` : ''}
            ${pageSize ? `pageSize: ${toJs(pageSize)},` : ''}
            ${imageUploader === false ? `imageUploader: false,` : ''}
            ${highlighter === false ? `highlighter: false,` : ''}
            ${texRenderer === false ? `texRenderer: false,` : ''}
            ${search === false ? `search: false,` : ''}
            ${typeof pageview !== 'undefined' ? `pageview: ${toJs(pageview)},` : ''}
            ${typeof comment !== 'undefined' ? `comment: ${toJs(comment)},` : ''}
            noCopyright: ${toJs(copyright === false)},
        });
        ${appearanceObserver}
      });
    })();`;
    return (
      <>
        <div id="waline-thread" class="content"></div>
        <link rel="stylesheet" href={cssUrl} media="print" onload="this.media='all'" />
        <script dangerouslySetInnerHTML={{ __html: js }} />
      </>
    );
  }
}

/**
 * Cacheable Waline comment JSX component.
 * <p>
 * This class is supposed to be used in combination with the <code>locals</code> hexo filter
 * ({@link module:hexo/filter/locals}).
 *
 * @see module:util/cache.cacheComponent
 * @example
 * <Waline.Cacheable
 *     comment={{
 *         server_url: "https://path/to/waline/server",
 *         path: "window.location.pathname",
 *         lang: "zh-CN",
 *         locale: {placeholder: "", ...},
 *         emoji: "https://cdn.jsdelivr.net/gh/walinejs/emojis/weibo",
 *         dark: "",
 *         meta: ["nick", "mail", "link"],
 *         required_meta: [],
 *         login: false,
 *         word_limit: 0,
 *         page_size: 10,
 *         image_uploader: false,
 *         highlighter: false,
 *         tex_renderer: false,
 *         search: false,
 *         pageview: false,
 *         comment: false,
 *         copyright: true,
 *     }}
 *     helper={{ cdn: function() {...} }} />
 */
Waline.Cacheable = cacheComponent(Waline, 'comment.waline', (props) => {
  const { comment, helper, page, config } = props;
  const language = comment.lang || page.lang || page.language || config.language;

  return {
    serverURL: comment.server_url,
    path: comment.path,
    lang: (Array.isArray(language) ? language[0] : language) || 'zh-CN',
    locale: comment.locale,
    emoji: comment.emoji,
    dark: comment.dark,
    meta: comment.meta,
    requiredMeta: comment.required_meta,
    login: comment.login,
    wordLimit: comment.word_limit,
    pageSize: comment.page_size,
    // These options of the theme config disable the features when they are true, which Waline does
    // when its options are false
    imageUploader: comment.image_uploader === true ? false : undefined,
    highlighter: comment.highlighter === true ? false : undefined,
    texRenderer: comment.tex_renderer === true ? false : undefined,
    search: comment.search === true ? false : undefined,
    pageview: comment.pageview,
    comment: comment.comment,
    copyright: comment.copyright,
    jsUrl: helper.cdn('@waline/client', '3.16.1', 'dist/waline.js'),
    cssUrl: helper.cdn('@waline/client', '3.16.1', 'dist/waline.css'),
  };
});

module.exports = Waline;