use-slides-by-frontmatter.ts 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { useEffect, useState } from 'react';
  2. import type { Parent, Root } from 'mdast';
  3. import type { Processor } from 'unified';
  4. type ParseResult = {
  5. marp: boolean | undefined,
  6. slide: boolean | undefined,
  7. }
  8. const parseSlideFrontmatter = (frontmatter: string): ParseResult => {
  9. let marp;
  10. let slide;
  11. const lines = frontmatter.split('\n');
  12. lines.forEach((line) => {
  13. const [key, value] = line.split(':').map(part => part.trim());
  14. if (key === 'marp' && value === 'true') {
  15. marp = true;
  16. }
  17. if (key === 'slide' && value === 'true') {
  18. slide = true;
  19. }
  20. });
  21. return { marp, slide };
  22. };
  23. type ProcessorOpts = {
  24. onParsed?: (result: ParseResult) => void,
  25. onSkipped?: () => void,
  26. };
  27. const generateFrontmatterProcessor = async(opts?: ProcessorOpts) => {
  28. const remarkFrontmatter = (await import('remark-frontmatter')).default;
  29. const remarkParse = (await import('remark-parse')).default;
  30. const remarkStringify = (await import('remark-stringify')).default;
  31. const unified = (await import('unified')).unified;
  32. return (unified()
  33. .use(remarkParse)
  34. .use(remarkStringify)
  35. .use(remarkFrontmatter, ['yaml'])
  36. .use(() => ((obj: Parent) => {
  37. if (obj.children[0]?.type === 'yaml') {
  38. const result = parseSlideFrontmatter(obj.children[0]?.value);
  39. opts?.onParsed?.(result);
  40. }
  41. else {
  42. opts?.onSkipped?.();
  43. }
  44. })));
  45. };
  46. export type UseSlide = {
  47. marp?: boolean,
  48. }
  49. /**
  50. * Frontmatter parser for slide
  51. * @param markdown Markdwon document
  52. * @returns An UseSlide instance. If the markdown does not contain neither "marp" or "slide" attribute in frontmatter, it returns undefined.
  53. */
  54. export const useSlidesByFrontmatter = (markdown?: string, isEnabledMarp?: boolean): UseSlide | undefined => {
  55. const [processor, setProcessor] = useState<Processor<Root, undefined, undefined, Root, string>|undefined>();
  56. const [parseResult, setParseResult] = useState<UseSlide|undefined>();
  57. useEffect(() => {
  58. if (processor != null) {
  59. return;
  60. }
  61. (async() => {
  62. const p = await generateFrontmatterProcessor({
  63. onParsed: result => setParseResult(result.marp || result.slide ? result : undefined),
  64. onSkipped: () => setParseResult(undefined),
  65. });
  66. setProcessor(p);
  67. })();
  68. }, [processor]);
  69. useEffect(() => {
  70. if (markdown == null || processor == null) {
  71. return;
  72. }
  73. processor.process(markdown);
  74. }, [markdown, processor]);
  75. return parseResult != null
  76. ? { marp: isEnabledMarp && parseResult?.marp }
  77. : undefined;
  78. };