diff --git a/package.json b/package.json
index 3c13345..6cf3347 100644
--- a/package.json
+++ b/package.json
@@ -11,12 +11,15 @@
},
"dependencies": {
"@fontsource/jetbrains-mono": "^5.0.2",
- "@fontsource/roboto-slab": "^5.0.2"
+ "@fontsource/roboto-slab": "^5.0.2",
+ "@nzbr/parcel-transformer-postprocess-html": "github:nzbr/parcel-transformer-postprocess-html"
},
"peerDependencies": {
"sharp": "^0.31.1"
},
"devDependencies": {
+ "@types/node": "^20.10.6",
+ "typescript": "^5.3.3",
"base16-builder": "^1.3.0"
}
}
diff --git a/web/html-postprocessors/.gitignore b/web/html-postprocessors/.gitignore
new file mode 100644
index 0000000..a6c7c28
--- /dev/null
+++ b/web/html-postprocessors/.gitignore
@@ -0,0 +1 @@
+*.js
diff --git a/web/html-postprocessors/speechbubbles.ts b/web/html-postprocessors/speechbubbles.ts
new file mode 100644
index 0000000..5b806c9
--- /dev/null
+++ b/web/html-postprocessors/speechbubbles.ts
@@ -0,0 +1,79 @@
+import * as path from 'path';
+import * as fs from 'fs';
+import type { StepInputs } from '@nzbr/parcel-transformer-postprocess-html/src';
+import { toPosixPath } from '@nzbr/parcel-transformer-postprocess-html/src';
+
+export function generateSpeechbubbles({ $, asset, options }: StepInputs): void {
+ const root = toPosixPath(options.projectRoot);
+ const stickers = path.posix.join(root, 'src', 'stickers');
+
+ asset.invalidateOnFileCreate({
+ glob: path.posix.join(stickers, '*'),
+ });
+
+ fs.readdirSync(stickers).forEach((character) => {
+ const characterDir = path.posix.join(stickers, character);
+ const cssClass = character.toLowerCase().trim().replaceAll(' ', '-');
+ $(`.${cssClass}`).each((_, el) => {
+ asset.invalidateOnFileCreate({
+ glob: path.posix.join(characterDir, '*'),
+ });
+
+ const stickers = fs
+ .readdirSync(characterDir)
+ .filter((file) => !file.endsWith('.txt'))
+ .map((file) => {
+ const stickerName = file.split('.').slice(0, -1).join('.');
+
+ return {
+ stickerName,
+ stickerFile: path.posix.join(characterDir, file),
+ stickerAltFile: path.posix.join(characterDir, stickerName + '.txt'),
+ stickerCssClass: stickerName.toLowerCase().trim().replaceAll(' ', '-'),
+ };
+ });
+
+ const defaultSticker = stickers.find(
+ (sticker) => sticker.stickerName === character,
+ );
+ const variants = stickers.filter((sticker) => sticker.stickerName !== character);
+
+ const sticker = variants
+ .concat(defaultSticker ? [defaultSticker] : [])
+ .find((sticker) => $(el).hasClass(sticker.stickerCssClass));
+
+ if (!sticker) {
+ asset.invalidateOnFileCreate({
+ glob: path.posix.join(characterDir, `${character}.*`),
+ });
+ throw new Error(`No sticker found for ${character} in ${asset.filePath}`);
+ }
+
+ if (!fs.existsSync(sticker.stickerAltFile)) {
+ asset.invalidateOnFileCreate({
+ glob: path.posix.join(characterDir, `${sticker.stickerName}.txt`),
+ });
+ throw new Error(
+ `No alt text found for ${sticker.stickerName} in ${asset.filePath}`,
+ );
+ }
+
+ asset.invalidateOnFileChange(sticker.stickerAltFile);
+ const alt = fs.readFileSync(sticker.stickerAltFile, 'utf-8').trim();
+
+ const url = asset.addURLDependency(
+ `/${path.posix.relative(root, sticker.stickerFile)}`,
+ {},
+ );
+
+ $(el).addClass('speechbubble');
+ $(el).html(`
+
+