macana

Static site generator for Obsidian Vault

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>
//
// SPDX-License-Identifier: Apache-2.0

import { visit } from "../../../deps/esm.sh/unist-util-visit/mod.ts";
import { refractor } from "../../../deps/esm.sh/refractor/mod.ts";
import type * as Mdast from "../../../deps/esm.sh/mdast/types.ts";
import type * as Hast from "../../../deps/esm.sh/hast/types.ts";
import {
	defaultHandlers,
	type Handlers,
	type State,
} from "../../../deps/esm.sh/mdast-util-to-hast/mod.ts";

function isValidClassName(value: unknown): value is string | readonly string[] {
	if (typeof value === "string") {
		return true;
	}

	if (Array.isArray(value) && value.every((x) => typeof x === "string")) {
		return true;
	}

	return false;
}

interface SyntaxHighlightingOptions {
	/**
	 * Class name to add to the container element.
	 * `null` to not setting class.
	 *
	 * @default "macana--highlight"
	 */
	className?: string | null;

	/**
	 * Attribute name to set space separated list of node type (e.g. "token", "string", "comment").
	 * `null` to not set node types to attribute.
	 *
	 * @default "data-hl-node"
	 */
	nodeTypeAttribute?: string | null;

	/**
	 * Attribute name to set language name (e.g. "css", "html")
	 * `null` to not set language name to attribute.
	 *
	 * @default "data-hl-lang"
	 */
	langNameAttribute?: string | null;
}

export function syntaxHighlightingHandlers({
	className = "macana--highlight",
	nodeTypeAttribute = "data-hl-node",
	langNameAttribute = "data-hl-lang",
}: SyntaxHighlightingOptions = {}): Handlers {
	return {
		code(state: State, node: Mdast.Code) {
			if (!node.lang || !refractor.registered(node.lang)) {
				return defaultHandlers.code(state, node);
			}

			const code = refractor.highlight(node.value, node.lang);
			visit(code, (node) => node.type === "element", (node) => {
				if (node.type !== "element") {
					return;
				}

				if (!node.properties || !isValidClassName(node.properties.className)) {
					return;
				}

				const className = node.properties.className;
				node.properties.className = undefined;
				if (typeof nodeTypeAttribute === "string") {
					node.properties[nodeTypeAttribute] = className;
				}
			});

			return {
				type: "element",
				tagName: "pre",
				properties: {
					className,
					...(langNameAttribute ? { [langNameAttribute]: node.lang } : {}),
				},
				children: [
					{
						type: "element",
						tagName: "code",
						properties: {},
						children: code.children as Hast.ElementContent[],
					},
				],
			};
		},
	};
}