-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add `DocumentAssetPlugin` The `DocumentAssetsPlugin` is responsible for copying assets from a document sub-directory to the public folder of your site. This is particularly useful for co-locating images within your document structure and referencing them from documents using relative paths. * - switch `assetSubDirs` to an array of globs - remove `isRelativePath` and replace with `path.isAbsolute` * add checks that we do not traverse outside the working directory
- Loading branch information
Showing
11 changed files
with
449 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
'@jpmorganchase/mosaic-plugins': patch | ||
'@jpmorganchase/mosaic-site': patch | ||
--- | ||
|
||
Add DocumentAssetPlugin | ||
|
||
The `DocumentAssetsPlugin` is responsible for copying assets from a document sub-directory to the public folder of your site. This is particularly useful for co-locating images within your document structure and referencing them from documents using relative paths. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ tsconfig.tsbuildinfo | |
|
||
# Deployment | ||
packages/rig | ||
packages/site/public/images | ||
|
||
# Test Results | ||
coverage | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ patches | |
LICENSE | ||
*.png | ||
*.hbs | ||
*.jpg | ||
|
||
**/build | ||
**/dist | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
--- | ||
title: DocumentAssetsPlugin | ||
layout: DetailOverview | ||
--- | ||
|
||
# {meta.title} | ||
|
||
The `DocumentAssetsPlugin` is responsible for copying assets from a document sub-directory to the public folder of your site. This is particularly useful for co-locating images within your document structure and referencing them from documents using relative paths. | ||
|
||
## Co-locating Images | ||
|
||
A common use case is to store images within the same directory structure as your documents. This allows you to reference images using relative paths. | ||
|
||
For example, to load an image (`mosaic.jpg`) from a sub-directory called `images`, relative to the document's path, you can use the following Markdown: | ||
|
||
```markdown | ||
![alt text](./images/mosaic.jpg) | ||
``` | ||
|
||
This will render the image as follows: | ||
![alt text](./images/mosaic.jpg) | ||
|
||
## Centralized Image Directory | ||
|
||
Alternatively, if you prefer to store all your images in a common parent directory, you can reference them using a relative path that navigates up the directory structure. | ||
|
||
For example, to load an image from a common parent directory, you can use: | ||
|
||
``` | ||
![alt text](../../images/mosaic.jpg) | ||
``` | ||
|
||
## Handling Absolute Paths and URLs | ||
|
||
The plugin ignores image paths that start with a leading slash (/) or are fully qualified URLs. This ensures that only relative paths are processed and copied to the public folder. | ||
|
||
``` | ||
![alt text](/images/mosaic.jpg) | ||
![alt text](https://www.saltdesignsystem.com/img/hero_image.svg) | ||
``` | ||
|
||
## Priority | ||
|
||
This plugin runs with a priority of -1 so it runs _after_ most other plugins. | ||
|
||
## Options | ||
|
||
| Property | Description | Default | | ||
| ------------ | ---------------------------------------------------------------------------------- | ------------- | | ||
| srcDir | The path where pages reside **after** cloning or when running locally | './docs' | | ||
| outputDir | There path to your site's public images directory where you want to put the images | './public' | | ||
| assetSubDirs | An array of subdirectory globs that could contain assets | ['**/images'] | | ||
| imagesPrefix | The prefix that is added to all new paths | '/images' | | ||
|
||
## Adding to Mosaic | ||
|
||
This plugin is **not** included in the mosaic config shipped by the Mosaic standard generator so it must be added manually to the `plugins` collection: | ||
|
||
```js | ||
plugins: [ | ||
{ | ||
modulePath: '@jpmorganchase/mosaic-plugins/DocumentAssetsPlugin', | ||
priority: -1, | ||
options: { | ||
srcDir: `../../docs`, | ||
outputDir: './public/images/mosaic', | ||
assetSubDirs: ['**/images'], | ||
imagesPrefix: '/images' | ||
} | ||
} | ||
// other plugins | ||
]; | ||
``` |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
import type { Page, Plugin as PluginType } from '@jpmorganchase/mosaic-types'; | ||
import fsExtra from 'fs-extra'; | ||
import glob from 'fast-glob'; | ||
import path from 'path'; | ||
import { escapeRegExp } from 'lodash-es'; | ||
import { unified } from 'unified'; | ||
import { visit } from 'unist-util-visit'; | ||
import remarkParse from 'remark-parse'; | ||
import remarkStringify from 'remark-stringify'; | ||
import { VFile } from 'vfile'; | ||
|
||
interface DocumentAssetsPluginOptions { | ||
/** | ||
* An array of subdirectory globs that could contain assets | ||
* @default: ['**\/images'] | ||
*/ | ||
assetSubDirs?: string[]; | ||
/** | ||
* The source path, where your docs reside, when the site runs | ||
*/ | ||
srcDir?: string; | ||
/** | ||
* The directory to copy matched assets to, typically the site's public directory | ||
* @default './public' | ||
*/ | ||
outputDir?: string; | ||
/** | ||
* The prefix we add to all images in documents, so that it routes to the public directory | ||
* @default '/images' | ||
*/ | ||
imagesPrefix?: string; | ||
} | ||
|
||
function isUrl(assetPath: string): boolean { | ||
try { | ||
new URL(assetPath); | ||
return true; | ||
} catch (_err) {} | ||
return false; | ||
} | ||
|
||
const createPageTest = (ignorePages: string[], pageExtensions: string[]) => { | ||
const extTest = new RegExp(`${pageExtensions.map(ext => escapeRegExp(ext)).join('|')}$`); | ||
const ignoreTest = new RegExp(`${ignorePages.map(ignore => escapeRegExp(ignore)).join('|')}$`); | ||
|
||
return (file: string) => | ||
!ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.'); | ||
}; | ||
|
||
function remarkRewriteImagePaths(newPrefix: string) { | ||
return (tree: any) => { | ||
visit(tree, 'image', (node: any) => { | ||
if (node.url) { | ||
if (isUrl(node.url) || /^\//.test(node.url)) { | ||
// Absolute URL or path, do nothing | ||
return; | ||
} else { | ||
const isRelativePath = !isUrl(node.url) && !path.isAbsolute(node.url); | ||
const assetPath = isRelativePath ? node.url : `./${node.url}`; | ||
const resolvedPath = path.resolve(path.dirname(newPrefix), assetPath); | ||
node.url = resolvedPath; | ||
} | ||
} | ||
}); | ||
}; | ||
} | ||
|
||
/** | ||
* Plugin that finds assets within the Mosaic filesystem and copies them to the configured `outputDir`. | ||
* Documents that create relative references to those images, will be re-written to pull the images from the `outputDir`. | ||
*/ | ||
const DocumentAssetsPlugin: PluginType<Page, DocumentAssetsPluginOptions> = { | ||
async afterUpdate( | ||
_mutableFileSystem, | ||
_helpers, | ||
{ | ||
assetSubDirs = [path.join('**', 'images')], | ||
srcDir = path.join(process.cwd(), 'docs'), | ||
outputDir = `${path.sep}public` | ||
} | ||
) { | ||
const resolvedCwd = path.resolve(process.cwd()); | ||
const resolvedSrcDir = path.resolve(srcDir); | ||
const resolvedOutputDir = path.resolve(outputDir); | ||
if (!resolvedOutputDir.startsWith(resolvedCwd)) { | ||
throw new Error(`outputDir must be within the current working directory: ${outputDir}`); | ||
} | ||
await fsExtra.ensureDir(srcDir); | ||
await fsExtra.ensureDir(outputDir); | ||
|
||
for (const assetSubDir of assetSubDirs) { | ||
const resolvedAssetSubDir = path.resolve(resolvedSrcDir, assetSubDir); | ||
if (!resolvedAssetSubDir.startsWith(resolvedSrcDir)) { | ||
console.log('ERROR 3'); | ||
|
||
throw new Error(`Asset subdirectory must be within srcDir: ${srcDir}`); | ||
} | ||
|
||
let globbedImageDirs; | ||
try { | ||
globbedImageDirs = await glob(assetSubDir, { | ||
cwd: resolvedSrcDir, | ||
onlyDirectories: true | ||
}); | ||
} catch (err) { | ||
console.error(`Error globbing ${assetSubDir} in ${srcDir}:`, err); | ||
continue; | ||
} | ||
|
||
if (globbedImageDirs?.length === 0) { | ||
continue; | ||
} | ||
|
||
for (const globbedImageDir of globbedImageDirs) { | ||
let imageFiles; | ||
let globbedPath; | ||
let rootSrcDir = srcDir; | ||
let rootOutputDir = outputDir; | ||
try { | ||
if (!path.isAbsolute(rootSrcDir)) { | ||
rootSrcDir = path.resolve(path.join(process.cwd(), srcDir)); | ||
} | ||
globbedPath = path.join(rootSrcDir, globbedImageDir); | ||
imageFiles = await fsExtra.promises.readdir(globbedPath); | ||
} catch (err) { | ||
console.error(`Error reading directory ${globbedPath}:`, err); | ||
continue; | ||
} | ||
if (!path.isAbsolute(rootOutputDir)) { | ||
rootOutputDir = path.resolve(path.join(process.cwd(), outputDir)); | ||
} | ||
|
||
for (const imageFile of imageFiles) { | ||
try { | ||
const imageSrcPath = path.join(globbedImageDir, imageFile); | ||
const fullImageSrcPath = path.join(rootSrcDir, imageSrcPath); | ||
const fullImageDestPath = path.join(rootOutputDir, imageSrcPath); | ||
|
||
await fsExtra.mkdir(path.dirname(fullImageDestPath), { recursive: true }); | ||
const symlinkAlreadyExists = await fsExtra.pathExists(fullImageDestPath); | ||
if (!symlinkAlreadyExists) { | ||
await fsExtra.symlink(fullImageSrcPath, fullImageDestPath); | ||
console.log(`Symlink created: ${fullImageSrcPath} -> ${fullImageDestPath}`); | ||
} | ||
} catch (error) { | ||
console.error(`Error processing ${imageFile}:`, error); | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
async $afterSource( | ||
pages, | ||
{ ignorePages, pageExtensions }, | ||
{ imagesPrefix = `${path.sep}images` } | ||
) { | ||
if (!pageExtensions.includes('.mdx')) { | ||
return pages; | ||
} | ||
for (const page of pages) { | ||
const isNonHiddenPage = createPageTest(ignorePages, ['.mdx']); | ||
if (!isNonHiddenPage(page.fullPath)) { | ||
continue; | ||
} | ||
|
||
const processor = unified() | ||
.use(remarkParse) | ||
.use(remarkRewriteImagePaths, path.join(imagesPrefix, page.route)) | ||
.use(remarkStringify); | ||
await processor | ||
.process(page.content) | ||
.then((file: VFile) => { | ||
page.content = String(file); | ||
}) | ||
.catch((err: Error) => { | ||
console.error('Error processing Markdown:', err); | ||
}); | ||
} | ||
return pages; | ||
} | ||
}; | ||
|
||
export default DocumentAssetsPlugin; |
Oops, something went wrong.