lsx.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import assert from 'assert';
  2. import { pathUtils } from '@growi/core';
  3. import { remarkGrowiDirectivePluginType } from '@growi/remark-growi-directive';
  4. import { Schema as SanitizeOption } from 'hast-util-sanitize';
  5. import { selectAll, HastNode } from 'hast-util-select';
  6. import isAbsolute from 'is-absolute-url';
  7. import { Plugin } from 'unified';
  8. import { visit } from 'unist-util-visit';
  9. const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
  10. const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter', 'except', 'isSharedPage'];
  11. const { hasHeadingSlash } = pathUtils;
  12. type DirectiveAttributes = Record<string, string>
  13. export const remarkPlugin: Plugin = function() {
  14. return (tree) => {
  15. visit(tree, (node, index) => {
  16. if (node.type === remarkGrowiDirectivePluginType.Text || node.type === remarkGrowiDirectivePluginType.Leaf) {
  17. if (typeof node.name !== 'string') {
  18. return;
  19. }
  20. if (!NODE_NAME_PATTERN.test(node.name)) {
  21. return;
  22. }
  23. const data = node.data ?? (node.data = {});
  24. const attributes = node.attributes as DirectiveAttributes || {};
  25. // set 'prefix' attribute if the first attribute is only value
  26. // e.g.
  27. // case 1: lsx(prefix=/path..., ...) => prefix="/path"
  28. // case 2: lsx(/path, ...) => prefix="/path"
  29. // case 3: lsx(/foo, prefix=/bar ...) => prefix="/bar"
  30. if (attributes.prefix == null) {
  31. const attrEntries = Object.entries(attributes);
  32. if (attrEntries.length > 0) {
  33. const [firstAttrKey, firstAttrValue] = attrEntries[0];
  34. if (firstAttrValue === '' && !SUPPORTED_ATTRIBUTES.includes(firstAttrValue)) {
  35. attributes.prefix = firstAttrKey;
  36. }
  37. }
  38. }
  39. data.hName = 'lsx';
  40. data.hProperties = attributes;
  41. }
  42. });
  43. };
  44. };
  45. export type LsxRehypePluginParams = {
  46. pagePath?: string,
  47. isSharedPage?: boolean,
  48. }
  49. const pathResolver = (href: string, basePath: string): string => {
  50. // exclude absolute URL
  51. if (isAbsolute(href)) {
  52. // remove scheme
  53. return href.replace(/^(.+?):\/\//, '/');
  54. }
  55. // generate relative pathname
  56. const baseUrl = new URL(pathUtils.addTrailingSlash(basePath), 'https://example.com');
  57. const relativeUrl = new URL(href, baseUrl);
  58. return relativeUrl.pathname;
  59. };
  60. export const rehypePlugin: Plugin<[LsxRehypePluginParams]> = (options = {}) => {
  61. assert.notStrictEqual(options.pagePath, null, 'lsx rehype plugin requires \'pagePath\' option');
  62. return (tree) => {
  63. if (options.pagePath == null) {
  64. return;
  65. }
  66. const basePagePath = options.pagePath;
  67. const elements = selectAll('lsx', tree as HastNode);
  68. elements.forEach((lsxElem) => {
  69. if (lsxElem.properties == null) {
  70. return;
  71. }
  72. const isSharedPage = lsxElem.properties.isSharedPage;
  73. if (isSharedPage == null || typeof isSharedPage !== 'boolean') {
  74. lsxElem.properties.isSharedPage = options.isSharedPage;
  75. }
  76. const prefix = lsxElem.properties.prefix;
  77. // set basePagePath when prefix is undefined or invalid
  78. if (prefix == null || typeof prefix !== 'string') {
  79. lsxElem.properties.prefix = basePagePath;
  80. return;
  81. }
  82. // return when prefix is already determined and aboslute path
  83. if (hasHeadingSlash(prefix)) {
  84. return;
  85. }
  86. // resolve relative path
  87. lsxElem.properties.prefix = decodeURI(pathResolver(prefix, basePagePath));
  88. });
  89. };
  90. };
  91. export const sanitizeOption: SanitizeOption = {
  92. tagNames: ['lsx'],
  93. attributes: {
  94. lsx: SUPPORTED_ATTRIBUTES,
  95. },
  96. };