lsx.ts 3.2 KB

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