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
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
  124. 124
  125. 125
  126. 126
  127. 127
  128. 128
  129. 129
  130. 130
  131. 131
  132. 132
  133. 133
  134. 134
  135. 135
  136. 136
  137. 137
  138. 138
  139. 139
// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>
//
// SPDX-License-Identifier: Apache-2.0

import * as brotli from "../deps/deno.land/x/brotli/mod.ts";
import * as zstd from "../deps/deno.land/x/zstd_wasm/deno/zstd.ts";

import type { FileSystemWriter } from "./interface.ts";

type WriterFactory = (childWriter: FileSystemWriter) => FileSystemWriter;

function DEFAULT_IGNORE_FN(path: readonly string[]): boolean {
	const filename = path[path.length - 1];
	if (!filename) {
		// Something went wrong, but this middleware is not responsible for this case.
		return false;
	}

	return !(/\.(js|css|html)$/.test(filename));
}

/**
 * @param ext - File extension, *including* leading dot.
 */
function appendExt(path: readonly string[], ext: string): readonly string[] {
	const filename = path[path.length - 1];
	const dirname = path.slice(0, -1);

	if (!filename) {
		// Something went wrong, but this middleware is not responsible for this case.
		return path;
	}

	return [...dirname, filename + ext];
}

// TODO: Support compression options.
// TODO: Enable users to discard compressed file depends on compression ratio.
//       `discard?(compressed: Uint8Array, original: Uint8Array): boolean
interface PrecompressOptions {
	/**
	 * Whether to enable gzip compression.
	 * @default true
	 */
	gzip?: boolean;

	/**
	 * Whether to enable brotli compression.
	 * @default true
	 */
	brotli?: boolean;

	/**
	 * Whether to enable Zstandard compression.
	 * @default true
	 */
	zstd?: boolean;

	/**
	 * Minimum byte length to create compressed files.
	 * If the source file size is below this threshold, this middleware does not compress
	 * the file.
	 * By default, this middleware compresses regardless of the file size.
	 * @default 0
	 */
	minBytes?: number;

	/**
	 * If this callback function returned `true`, this middleware skips compression
	 * for the file.
	 * By default, this middleware skips files whose filename does not end with `.js`,
	 * `.html`, or `.css`.
	 */
	ignore?(path: readonly string[], content: Uint8Array): boolean;
}

/**
 * Wraps the given FileSystem Writer and returns a new FileSystem Writer
 * that produces additional gzip/brotli/Zstandard compressed files along with
 * the original file.
 *
 * Each compressed files have ".gz", ".br", ".zst" suffix, respectively.
 * This works well with Caddy's `precompressed` [0] directive.
 *
 * [0]: https://caddyserver.com/docs/caddyfile/directives/file_server#precompressed
 */
export function precompress(
	{
		gzip: enableGzip = true,
		brotli: enableBrotli = true,
		zstd: enableZstd = true,
		minBytes = 0,
		ignore = DEFAULT_IGNORE_FN,
	}: PrecompressOptions = {},
): WriterFactory {
	let isZstdInitCompleted = false;

	return (childWriter) => {
		return {
			async write(path, content) {
				if (ignore(path, content) || content.byteLength < minBytes) {
					return childWriter.write(path, content);
				}

				if (enableGzip) {
					const blob = new Blob([content]);
					const stream = blob.stream().pipeThrough(
						new CompressionStream("gzip"),
					);
					await childWriter.write(
						appendExt(path, ".gz"),
						new Uint8Array(await new Response(stream).arrayBuffer()),
					);
				}

				if (enableBrotli) {
					await childWriter.write(
						appendExt(path, ".br"),
						brotli.compress(content),
					);
				}

				if (enableZstd) {
					if (!isZstdInitCompleted) {
						await zstd.init();
						isZstdInitCompleted = true;
					}

					await childWriter.write(
						appendExt(path, ".zst"),
						zstd.compress(content, 10),
					);
				}

				return childWriter.write(path, content);
			},
		};
	};
}