Skip to content

Commit 73f8dbc

Browse files
authored
feat(docs): allow audio (mp3/ogg), video (mp4/webm) and font (woff2) attachments (#7605)
These file types can now be stored next to a Markdown file to be used in live samples. In the future, only two steps are necessary to add a file extension/type: 1. The file extension (and its mime type) must be added to `libs/constants` and `client/src/setupProxy.js`. 2. A module must be declared in `{server,ssr}/react-app.d.ts`. Notes: - The `Image` module is now called `FileAttachment`, e.g. `FileAttachment.findByUrl()`. - A new npm script `test:libs` was added to run all unit tests in `libs/`.
1 parent cd5e710 commit 73f8dbc

File tree

19 files changed

+320
-45
lines changed

19 files changed

+320
-45
lines changed

Diff for: .github/workflows/testing.yml

+3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ jobs:
6969
- name: Unit testing client
7070
run: yarn test:client
7171

72+
- name: Unit testing libs
73+
run: yarn test:libs
74+
7275
- name: Build and start server
7376
id: server
7477
env:

Diff for: build/check-images.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import path from "node:path";
66

77
import imagesize from "image-size";
88

9-
import { Document, Image } from "../content/index.js";
9+
import { Document, FileAttachment } from "../content/index.js";
1010
import { FLAW_LEVELS, DEFAULT_LOCALE } from "../libs/constants/index.js";
1111
import { findMatchesInText } from "./matches-in-text.js";
1212
import * as cheerio from "cheerio";
@@ -140,12 +140,12 @@ export function checkImageReferences(
140140
// but all our images are going to be static.
141141
finalSrc = absoluteURL.pathname;
142142
// We can use the `finalSrc` to look up and find the image independent
143-
// of the correct case because `Image.findByURL` operates case
143+
// of the correct case because `FileAttachment.findByURL` operates case
144144
// insensitively.
145145

146-
// What follows uses the same algorithm as Image.findByURLWithFallback
146+
// What follows uses the same algorithm as FileAttachment.findByURLWithFallback
147147
// but only adds a filePath if it exists for the DEFAULT_LOCALE
148-
const filePath = Image.findByURL(finalSrc);
148+
const filePath = FileAttachment.findByURL(finalSrc);
149149
let enUSFallback = false;
150150
if (
151151
!filePath &&
@@ -156,7 +156,7 @@ export function checkImageReferences(
156156
new RegExp(`^/${doc.locale}/`, "i"),
157157
`/${DEFAULT_LOCALE}/`
158158
);
159-
if (Image.findByURL(enUSFinalSrc)) {
159+
if (FileAttachment.findByURL(enUSFinalSrc)) {
160160
// Use the en-US src instead
161161
finalSrc = enUSFinalSrc;
162162
// Note that this `<img src="...">` value can work if you use the
@@ -366,7 +366,7 @@ export function checkImageWidths(
366366
);
367367
}
368368
} else if (!imgSrc.includes("://") && imgSrc.startsWith("/")) {
369-
const filePath = Image.findByURLWithFallback(imgSrc);
369+
const filePath = FileAttachment.findByURLWithFallback(imgSrc);
370370
if (filePath) {
371371
const dimensions = sizeOf(filePath);
372372
img.attr("width", `${dimensions.width}`);

Diff for: build/flaws/broken-links.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from "node:fs";
33
import { fromMarkdown } from "mdast-util-from-markdown";
44
import { visit } from "unist-util-visit";
55

6-
import { Document, Redirect, Image } from "../../content/index.js";
6+
import { Document, Redirect, FileAttachment } from "../../content/index.js";
77
import { findMatchesInText } from "../matches-in-text.js";
88
import {
99
DEFAULT_LOCALE,
@@ -277,8 +277,8 @@ export function getBrokenLinksFlaws(
277277
const absoluteURL = new URL(href, "http://www.example.com");
278278
const found = Document.findByURL(hrefNormalized);
279279
if (!found) {
280-
// Before we give up, check if it's an image.
281-
if (!Image.findByURLWithFallback(hrefNormalized)) {
280+
// Before we give up, check if it's an attachment.
281+
if (!FileAttachment.findByURLWithFallback(hrefNormalized)) {
282282
// Even if it's a redirect, it's still a flaw, but it'll be nice to
283283
// know what it *should* be.
284284
const resolved = Redirect.resolve(hrefNormalized);

Diff for: build/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import LANGUAGES_RAW from "../libs/languages/index.js";
3333
import { safeDecodeURIComponent } from "../kumascript/src/api/util.js";
3434
import { wrapTables } from "./wrap-tables.js";
3535
import {
36-
getAdjacentImages,
36+
getAdjacentFileAttachments,
3737
injectLoadingLazyAttributes,
3838
injectNoTranslate,
3939
makeTOC,
@@ -382,8 +382,8 @@ export async function buildDocument(
382382
// The checkImageReferences() does 2 things. Checks image *references* and
383383
// it returns which images it checked. But we'll need to complement any
384384
// other images in the folder.
385-
getAdjacentImages(path.dirname(document.fileInfo.path)).forEach((fp) =>
386-
fileAttachments.add(fp)
385+
getAdjacentFileAttachments(path.dirname(document.fileInfo.path)).forEach(
386+
(fp) => fileAttachments.add(fp)
387387
);
388388

389389
// Check the img tags for possible flaws and possible build-time rewrites

Diff for: build/utils.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import imageminSvgo from "imagemin-svgo";
1212
import { rgPath } from "@vscode/ripgrep";
1313
import sanitizeFilename from "sanitize-filename";
1414

15-
import { VALID_MIME_TYPES } from "../libs/constants/index.js";
16-
import { Image } from "../content/index.js";
15+
import {
16+
ANY_ATTACHMENT_REGEXP,
17+
VALID_MIME_TYPES,
18+
} from "../libs/constants/index.js";
19+
import { FileAttachment } from "../content/index.js";
1720
import { spawnSync } from "node:child_process";
1821
import { BLOG_ROOT } from "../libs/env/index.js";
1922

@@ -184,15 +187,12 @@ export function splitSections(rawHTML) {
184187
*
185188
* @param {Document} document
186189
*/
187-
export function getAdjacentImages(documentDirectory) {
190+
export function getAdjacentFileAttachments(documentDirectory: string) {
188191
const dirents = fs.readdirSync(documentDirectory, { withFileTypes: true });
189192
return dirents
190193
.filter((dirent) => {
191194
// This needs to match what we do in filecheck/checker.py
192-
return (
193-
!dirent.isDirectory() &&
194-
/\.(png|jpeg|jpg|gif|svg|webp)$/i.test(dirent.name)
195-
);
195+
return !dirent.isDirectory() && ANY_ATTACHMENT_REGEXP.test(dirent.name);
196196
})
197197
.map((dirent) => path.join(documentDirectory, dirent.name));
198198
}
@@ -249,21 +249,21 @@ export function postLocalFileLinks($, doc) {
249249
const href = element.attribs.href;
250250

251251
// This test is merely here to quickly bail if there's no hope to find the
252-
// image as a local file link. There are a LOT of hyperlinks throughout
253-
// the content and this simple if statement means we can skip 99% of the
254-
// links, so it's presumed to be worth it.
252+
// file attachment as a local file link. There are a LOT of hyperlinks
253+
// throughout the content and this simple if statement means we can skip 99%
254+
// of the links, so it's presumed to be worth it.
255255
if (
256256
!href ||
257257
/^(\/|\.\.|http|#|mailto:|about:|ftp:|news:|irc:|ftp:)/i.test(href)
258258
) {
259259
return;
260260
}
261261
// There are a lot of links that don't match. E.g. `<a href="SubPage">`
262-
// So we'll look-up a lot "false positives" that are not images.
262+
// So we'll look-up a lot "false positives" that are not file attachments.
263263
// Thankfully, this lookup is fast.
264264
const url = `${doc.mdn_url}/${href}`;
265-
const image = Image.findByURLWithFallback(url);
266-
if (image) {
265+
const fileAttachment = FileAttachment.findByURLWithFallback(url);
266+
if (fileAttachment) {
267267
$(element).attr("href", url);
268268
}
269269
});

Diff for: client/src/react-app.d.ts

+25
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,41 @@ declare module "*.jpeg" {
3434
export default src;
3535
}
3636

37+
declare module "*.mp3" {
38+
const src: string;
39+
export default src;
40+
}
41+
42+
declare module "*.mp4" {
43+
const src: string;
44+
export default src;
45+
}
46+
47+
declare module "*.ogg" {
48+
const src: string;
49+
export default src;
50+
}
51+
3752
declare module "*.png" {
3853
const src: string;
3954
export default src;
4055
}
4156

57+
declare module "*.webm" {
58+
const src: string;
59+
export default src;
60+
}
61+
4262
declare module "*.webp" {
4363
const src: string;
4464
export default src;
4565
}
4666

67+
declare module "*.woff2" {
68+
const src: string;
69+
export default src;
70+
}
71+
4772
declare module "*.svg" {
4873
import * as React from "react";
4974

Diff for: client/src/setupProxy.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ function config(app) {
1616
app.use("/_+(flaws|translations|open|document)", proxy);
1717
// E.g. search-index.json or index.json
1818
app.use("**/*.json", proxy);
19-
// This has to match what we do in server/index.js in the catchall handler
20-
app.use("**/*.(png|webp|gif|jpe?g|svg)", proxy);
19+
// Always update libs/constant/index.js when adding/removing extensions!
20+
app.use(`**/*.(gif|jpeg|jpg|mp3|mp4|ogg|png|svg|webm|webp|woff2)`, proxy);
2121
// All those root-level images like /favicon-48x48.png
2222
app.use("/*.(png|webp|gif|jpe?g|svg)", proxy);
2323
}

Diff for: cloud-function/src/app.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import express, { Request, Response } from "express";
22
import { Router } from "express";
33

4+
import { ANY_ATTACHMENT_EXT } from "./internal/constants/index.js";
5+
46
import { Origin } from "./env.js";
57
import { proxyContent } from "./handlers/proxy-content.js";
68
import { proxyKevel } from "./handlers/proxy-kevel.js";
@@ -48,7 +50,7 @@ router.get(
4850
proxyContent
4951
);
5052
router.get(
51-
"/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)",
53+
`/[^/]+/docs/*/*.(${ANY_ATTACHMENT_EXT.join("|")})`,
5254
requireOrigin(Origin.main, Origin.liveSamples),
5355
resolveIndexHTML,
5456
proxyContent

Diff for: cloud-function/src/utils.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Request, Response } from "express";
22

3+
import {
4+
ANY_ATTACHMENT_EXT,
5+
createRegExpFromExtensions,
6+
} from "./internal/constants/index.js";
7+
38
import { DEFAULT_COUNTRY } from "./constants.js";
49

510
export function getRequestCountry(req: Request): string {
@@ -45,8 +50,12 @@ export function isLiveSampleURL(url: string) {
4550

4651
// These are the only extensions in client/build/*/docs/*.
4752
// `find client/build -type f | grep docs | xargs basename | sed 's/.*\.\([^.]*\)$/\1/' | sort | uniq`
48-
const ASSET_REGEXP = /\.(gif|html|jpeg|jpg|json|png|svg|txt|xml)$/i;
53+
const TEXT_EXT = ["html", "json", "svg", "txt", "xml"];
54+
const ANY_ATTACHMENT_REGEXP = createRegExpFromExtensions(
55+
...ANY_ATTACHMENT_EXT,
56+
...TEXT_EXT
57+
);
4958

5059
export function isAsset(url: string) {
51-
return ASSET_REGEXP.test(url);
60+
return ANY_ATTACHMENT_REGEXP.test(url);
5261
}

Diff for: content/image.ts renamed to content/file-attachment.ts

+41-3
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,52 @@ import { readChunkSync } from "read-chunk";
55
import imageType from "image-type";
66
import isSvg from "is-svg";
77

8-
import { DEFAULT_LOCALE } from "../libs/constants/index.js";
8+
import {
9+
ANY_IMAGE_EXT,
10+
AUDIO_EXT,
11+
DEFAULT_LOCALE,
12+
FONT_EXT,
13+
VIDEO_EXT,
14+
createRegExpFromExtensions,
15+
} from "../libs/constants/index.js";
916
import { ROOTS } from "../libs/env/index.js";
1017
import { memoize, slugToFolder } from "./utils.js";
1118

12-
function isImage(filePath: string) {
19+
function isFileAttachment(filePath: string) {
1320
if (fs.statSync(filePath).isDirectory()) {
1421
return false;
1522
}
23+
24+
return (
25+
isAudio(filePath) ||
26+
isFont(filePath) ||
27+
isVideo(filePath) ||
28+
isImage(filePath)
29+
);
30+
}
31+
32+
const AUDIO_FILE_REGEXP = createRegExpFromExtensions(...AUDIO_EXT);
33+
const FONT_FILE_REGEXP = createRegExpFromExtensions(...FONT_EXT);
34+
const VIDEO_FILE_REGEXP = createRegExpFromExtensions(...VIDEO_EXT);
35+
const IMAGE_FILE_REGEXP = createRegExpFromExtensions(...ANY_IMAGE_EXT);
36+
37+
function isAudio(filePath: string) {
38+
return AUDIO_FILE_REGEXP.test(filePath);
39+
}
40+
41+
function isFont(filePath: string) {
42+
return FONT_FILE_REGEXP.test(filePath);
43+
}
44+
45+
function isVideo(filePath: string) {
46+
return VIDEO_FILE_REGEXP.test(filePath);
47+
}
48+
49+
function isImage(filePath: string) {
50+
if (!IMAGE_FILE_REGEXP.test(filePath)) {
51+
return false;
52+
}
53+
1654
if (filePath.toLowerCase().endsWith(".svg")) {
1755
return isSvg(fs.readFileSync(filePath, "utf-8"));
1856
}
@@ -37,7 +75,7 @@ function urlToFilePath(url: string) {
3775

3876
const find = memoize((relativePath: string) => {
3977
return ROOTS.map((root) => path.join(root, relativePath)).find(
40-
(filePath) => fs.existsSync(filePath) && isImage(filePath)
78+
(filePath) => fs.existsSync(filePath) && isFileAttachment(filePath)
4179
);
4280
});
4381

Diff for: content/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export * as Document from "./document.js";
22
export * as Translation from "./translation.js";
33
export { getPopularities } from "./popularities.js";
44
export * as Redirect from "./redirect.js";
5-
export * as Image from "./image.js";
5+
export * as FileAttachment from "./file-attachment.js";
66
export {
77
buildURL,
88
memoize,

0 commit comments

Comments
 (0)