diff --git a/README.md b/README.md index b7cc5916..0ab45947 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Contented -[Contented](https://contented.dev) is a Markdown-based bundler for your documentation with pipeline driven -authoring-oriented workflow to encourage developer authoring within its contextual Git repository. +[Contented](https://contented.dev) is a prose bundler for your documentation with pipeline driven +authoring-oriented workflow to encourage developers authoring within its contextual Git repository. With a headless design of 1 config file `contented.config.mjs`, developers can start writing their [markdown content](./04-markdown.md) and preview it on their localhost `contented generate --watch`. @@ -15,7 +15,7 @@ checks it is compilable and presentable. By encouraging authoring next to the source (in the same git repo), developers can contextually document changes as they develop. All domain-specific changes will go into the `main` branch with one Git Pull Request. -With `contented build`, you can compile your markdown into sources `index.js` and `*.json`. That output +With `contented build`, you can compile your prose into sources `index.js` and `*.json`. That output into `./dist` to `npm publish` them into any registry of your choice, for you can easily `npm i @your-scope/your-npm-package` and use the processed content on any of your downstream sites. Easily pulling up-to-date content and prose from individual domain-specific repositories and re-presented. Think microservices, @@ -30,7 +30,7 @@ naturally satisfies. ### Just Another SSG? -**This is not a static site generator.** This is a markdown processor workflow with a built-in static site generator. +**This is not a static site generator.** This is a prose processor workflow with a built-in static site generator. The outcome we're trying to achieve is this [@contentedjs/contented-example/dist](https://www.jsdelivr.com/package/npm/@contentedjs/contented-example) @@ -51,12 +51,12 @@ Your docs can be anywhere as long as contented is configured to pick them up. repo/ ├─ packages/** ├─ docs/ -│ ├─ 01:Title 1/*.md -│ ├─ 02:Title 2/*.md -│ ├─ 03:Title 3/ -│ │ ├─ 01:Subtitle 1/*.md -│ │ ├─ 02:overview.md -│ │ └─ 03:faq.md +│ ├─ 01-Title 1/*.md +│ ├─ 02-Title 2/*.md +│ ├─ 03-Title 3/ +│ │ ├─ 01-Subtitle 1/*.md +│ │ ├─ 02-overview.md +│ │ └─ 03-faq.md │ └─ package.json ├─ contented.config.mjs ├─ package.json diff --git a/package-lock.json b/package-lock.json index 28f3738f..b055ab98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11563,6 +11563,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-relative-url": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-relative-url/-/is-relative-url-4.0.0.tgz", + "integrity": "sha512-PkzoL1qKAYXNFct5IKdKRH/iBQou/oCC85QhXj6WKtUQBliZ4Yfd3Zk27RHu9KQG8r6zgvAA2AQKC9p+rqTszg==", + "dependencies": { + "is-absolute-url": "^4.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -15139,6 +15153,17 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -18189,6 +18214,113 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-embed-images": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-embed-images/-/remark-embed-images-4.0.0.tgz", + "integrity": "sha512-c8qQMdhNTSO9TMMfbvf5CWI+HhIX5UZPTBVNaNSpxaEuRt0KCLFk68s5CQ7szbSERLnpos+c9K6xZoILWRh+eQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "is-relative-url": "^4.0.0", + "mime": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-embed-images/node_modules/@types/mdast": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.1.tgz", + "integrity": "sha512-IlKct1rUTJ1T81d8OHzyop15kGv9A/ff7Gz7IJgrk6jDb4Udw77pCJ+vq8oxZf4Ghpm+616+i1s/LNg/Vh7d+g==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-embed-images/node_modules/@types/unist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", + "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" + }, + "node_modules/remark-embed-images/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-embed-images/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-embed-images/node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-embed-images/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-embed-images/node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-embed-images/node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-frontmatter": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz", @@ -21578,6 +21710,7 @@ "rehype-slug": "^5.1.0", "rehype-stringify": "^9.0.4", "remark-directive": "^2.0.1", + "remark-embed-images": "^4.0.0", "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1", "remark-parse": "^10.0.2", diff --git a/packages/contented-example/docs/04-markdown.md b/packages/contented-example/docs/04-markdown.md index c5235208..54120786 100644 --- a/packages/contented-example/docs/04-markdown.md +++ b/packages/contented-example/docs/04-markdown.md @@ -330,3 +330,34 @@ const b = 2; :::: ```` + +## Images + +Image commonly used in Markdown is supported but with some caveats. +Contented being a prose-bundler is designed to bundle prose such that it is portable and can be deployed to different +sites or domains. +This makes it difficult to support images that are not bundled together with the prose. +To get around this, all local images are embedded into the Markdown file as base64 encoded strings. +Remote images are left as is. + +> You can inspect this HTML page to see how the images are embedded. + +:::div{.admonitions.yellow} + +Always be careful with user input. For example, it’s possible to hide JavaScript inside images (such as GIFs, WebPs, and +SVGs). User provided images open you up to a cross-site scripting (XSS) attack. + +If you’re using Contented to render user-provided Markdown, you should disable images by default and only enable them +when you trust the source. Contented designed to be used for developer authoring where the source is trusted and XSS +being the least of your worries since the developer (having control of source code) can already inject arbitrary +JavaScript into the page without needing to go through this lengthy process. + +::: + +![local-embedded-image.png](local-embedded-image.png) +![placehold.co](https://placehold.co/1500x300.png?text=Remote%20Loaded%20Image) + +```markdown +![local-embedded-image.png](local-embedded-image.png) +![placehold.co](https://placehold.co/1500x300.png?text=Remote%20Loaded%20Image) +``` diff --git a/packages/contented-example/docs/local-embedded-image.png b/packages/contented-example/docs/local-embedded-image.png new file mode 100644 index 00000000..d02af847 Binary files /dev/null and b/packages/contented-example/docs/local-embedded-image.png differ diff --git a/packages/contented-pipeline-md/package.json b/packages/contented-pipeline-md/package.json index 1ece2adc..7e29833c 100644 --- a/packages/contented-pipeline-md/package.json +++ b/packages/contented-pipeline-md/package.json @@ -56,6 +56,7 @@ "rehype-slug": "^5.1.0", "rehype-stringify": "^9.0.4", "remark-directive": "^2.0.1", + "remark-embed-images": "^4.0.0", "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1", "remark-parse": "^10.0.2", diff --git a/packages/contented-pipeline-md/src/UnifiedProcessor.ts b/packages/contented-pipeline-md/src/UnifiedProcessor.ts index 68dd976b..f9d09f9d 100644 --- a/packages/contented-pipeline-md/src/UnifiedProcessor.ts +++ b/packages/contented-pipeline-md/src/UnifiedProcessor.ts @@ -4,6 +4,7 @@ import rehypeExternalLinks from 'rehype-external-links'; import rehypeSlug from 'rehype-slug'; import rehypeStringify from 'rehype-stringify'; import remarkDirective from 'remark-directive'; +import remarkEmbedImages from 'remark-embed-images'; import remarkFrontmatter from 'remark-frontmatter'; import remarkGfm from 'remark-gfm'; import remarkParse from 'remark-parse'; @@ -41,6 +42,7 @@ export function initProcessor(processor: Processor, options?: UnifiedOptions): P .use(remarkGfm) .use(remarkFrontmatter) .use(remarkParse) + .use(remarkEmbedImages) .use(remarkLink) .use(remarkDirective) .use(remarkDirectiveRehypeCodeblockHeader) @@ -90,6 +92,7 @@ export { remarkDirectiveRehype, remarkDirectiveRehypeCodeblockGroup, remarkDirectiveRehypeCodeblockHeader, + remarkEmbedImages, remarkFrontmatter, remarkFrontmatterCollect, remarkFrontmatterResolve,