Skip to content

Commit

Permalink
Fragment plugin (#354)
Browse files Browse the repository at this point in the history
* add FragmentPlugin that enables the embedding of pages into pages using the syntax :fragment{src="path-to-fragment"}
  • Loading branch information
lilyvc authored May 19, 2023
1 parent 4983e02 commit 3bc7865
Show file tree
Hide file tree
Showing 69 changed files with 781 additions and 113 deletions.
84 changes: 84 additions & 0 deletions docs/author/fragments.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Fragments
layout: DetailTechnical
sidebar:
priority: 2
---

# {meta.title}

Fragments, also known as content fragments, are powerful tools that allow you to incorporate content from other pages into your documentation. By creating an MDX file and using the [generic directives](https://talk.commonmark.org/t/generic-directives-plugins-syntax/444) syntax `:fragment{src="path-to-fragment"}`, you can easily render the fragment in another file, providing modularity and reusability to your content.

## Use Cases

Fragments offer various use cases, such as:

**Consistent Content**: Use fragments to maintain consistent content across multiple pages. For instance, if you have a table or a tile that appears on multiple pages, you can create a fragment for it and include it in all relevant files. This ensures that any updates made to the fragment automatically reflect across the entire documentation.

**Reusable Components**: Fragments enable the creation of reusable components or sections. You can define a complex or commonly used section once and then include it in multiple pages as needed. This approach saves time and effort, as you only need to update the fragment file to propagate changes throughout your documentation.

**Modular Documentation**: With fragments, you can break down your documentation into smaller, manageable pieces. Each fragment represents a specific topic or section, allowing you to organize and structure your content more efficiently. This modular approach simplifies maintenance and makes it easier to navigate and update your documentation.

## Usage

Firstly, enable the Fragment Plugin by adding the following to your plugins in `mosaic.config.js`.

```
{
modulePath: '@jpmorganchase/mosaic-plugins/FragmentPlugin',
options: {}
}
```

To include a fragment in your content, follow these steps:

Create an MDX file for the fragment you want to reuse. Remember to set the sidebar property of your fragment's frontmatter to exclude: true if you don't want the fragment to appear in the vertical navigation menu.

```
---
title: Fragment Title
sidebar:
exclude: true
---
```

In the target file where you want to include the fragment, use the remark directive syntax `:fragment{src="path-to-fragment"}`.

### Markdown Content Example

This is the contents of a fragment located at `../fragments/content-fragment.mdx`:

```
---
title: Content Fragment
sidebar:
exclude: true
---
#### Fragment Title
This is an example fragment of markdown content, being pulled from `../fragments/content-fragment.mdx`.
```

The below code snippet will render the content from the content-fragment.mdx file in your target file:

```
:fragment{src="../fragments/content-fragment.mdx"}
```

Example output:

:fragment{src="../fragments/content-fragment.mdx"}

### Component Example

Here is another example, where the fragment files each contain a `<Tile>` component.

```
:fragment{src="../fragments/tile-a.mdx"} :fragment{src="../fragments/tile-b.mdx"}
```

The above code will render the content from tile-a.mdx and tile-b.mdx files, demonstrated below:

:fragment{src="../fragments/tile-a.mdx"} :fragment{src="../fragments/tile-b.mdx"}
9 changes: 9 additions & 0 deletions docs/fragments/content-fragment.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Content Fragment
sidebar:
exclude: true
---

#### Fragment Title

This is an example fragment of markdown content, being pulled from `../fragments/content-fragment.mdx`.
8 changes: 8 additions & 0 deletions docs/fragments/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Fragments
layout: DetailTechnical
---

# {meta.title}

This folder contains example fragments that are referenced and rendered in the Fragments docs page.
7 changes: 7 additions & 0 deletions docs/fragments/tile-a.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Tile A
sidebar:
exclude: true
---

<TileContent title="Tile A" description="Tile A description" eyebrow="Eyebrow" />
7 changes: 7 additions & 0 deletions docs/fragments/tile-b.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Tile B
sidebar:
exclude: true
---

<TileContent title="Tile B" description="Tile B description" eyebrow="Eyebrow" />
1 change: 1 addition & 0 deletions packages/plugins/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"react-pro-sidebar": "^1.0.0",
"reading-time": "^1.5.0",
"remark": "^14.0.2",
"remark-directive": "^2.0.1",
"remark-mdx": "^2.1.5",
"remark-parse": "^10.0.1",
"remark-stringify": "^10.0.2",
Expand Down
121 changes: 121 additions & 0 deletions packages/plugins/src/FragmentPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import path from 'path';
import type { Plugin as PluginType } from '@jpmorganchase/mosaic-types';
import { escapeRegExp } from 'lodash-es';
import { remark } from 'remark';
import remarkDirective from 'remark-directive';
import { visitParents } from 'unist-util-visit-parents';

interface FragmentPluginPage {
fullPath: string;
content: string;
}

const createPageTest = (ignorePages, pageExtensions) => {
const extTest = new RegExp(`${pageExtensions.map(ext => escapeRegExp(ext)).join('|')}$`);
const ignoreTest = new RegExp(`${ignorePages.map(ignore => escapeRegExp(ignore)).join('|')}$`);
return file =>
!ignoreTest.test(file) && extTest.test(file) && !path.basename(file).startsWith('.');
};

function getFullPath(fullPath: string, relativePath: string): string {
const pathSegments = fullPath.split('/');
const relativeSegments = relativePath.split('/');

pathSegments.pop();

for (const segment of relativeSegments) {
if (segment === '..') {
pathSegments.pop();
} else if (segment !== '.') {
pathSegments.push(segment);
}
}
return pathSegments.join('/');
}

async function processTree(tree, serialiser, mutableFilesystem, fullPath, isNonHiddenPage) {
const nodesToProcess = [];

visitParents(tree, (node, ancestors) => {
if (node.type === 'code') {
return;
}

const match = node.name === 'fragment' && node.attributes.src;
if (match) {
const parent = ancestors[ancestors.length - 1];
const index = parent.children.indexOf(node);
nodesToProcess.push({ node, parent, index });
}
});

for (const { node, parent, index } of nodesToProcess) {
const fragmentFullPath = getFullPath(fullPath, node.attributes.src);
if (!isNonHiddenPage(fragmentFullPath)) {
console.warn(`Invalid file reference: '${node.attributes.src}'. Skipping.`);
} else {
const fragmentPage = await serialiser.deserialise(
fragmentFullPath,
await mutableFilesystem.promises.readFile(fragmentFullPath)
);

// Create a new node with the content from fragmentPage.content
const newNode = {
type: 'html',
value: fragmentPage.content
};

// Replace the original node with the newNode in the tree
parent.children.splice(index, 1, newNode);
}
}
return tree;
}

const FragmentPlugin: PluginType<FragmentPluginPage, unknown, unknown> = {
async $beforeSend(mutableFilesystem, { serialiser, ignorePages, pageExtensions }) {
const pages = await Promise.all(
(
(await mutableFilesystem.promises.glob('**', {
onlyFiles: true,
ignore: ignorePages.map(ignore => `**/${ignore}`),
cwd: '/'
})) as string[]
).map(async pagePath => {
const deserialisedPage = await serialiser.deserialise(
pagePath,
await mutableFilesystem.promises.readFile(pagePath)
);
return deserialisedPage;
})
);

const isNonHiddenPage = createPageTest(ignorePages, pageExtensions);

for (const page of pages) {
const fullPath = page.fullPath;
if (!isNonHiddenPage(fullPath)) {
continue;
}

const tree = remark().use(remarkDirective).parse(page.content);
const processedTree = await processTree(
tree,
serialiser,
mutableFilesystem,
fullPath,
isNonHiddenPage
);

page.content = remark()
.data('settings', { fences: true })
.use(remarkDirective)
.stringify(processedTree);

const updatedFileData = await serialiser.serialise(fullPath, page);
await mutableFilesystem.promises.writeFile(fullPath, updatedFileData);
}
}
};

export default FragmentPlugin;
2 changes: 1 addition & 1 deletion packages/plugins/src/ReadingTimePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const ReadingTimePlugin: PluginType<ReadingTimePluginPage> = {
async $afterSource(pages) {
const processor = unified().use(markdown);
for (const page of pages) {
const tree = await processor.parse(page.content);
const tree: Node = await processor.parse(page.content);
let textContent = '';

visit(
Expand Down
4 changes: 4 additions & 0 deletions packages/site/mosaic.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const siteConfig = {
modulePath: '@jpmorganchase/mosaic-plugins/SidebarPlugin',
options: { rootDirGlob: '*/*' }
},
{
modulePath: '@jpmorganchase/mosaic-plugins/FragmentPlugin',
options: {}
},
{
modulePath: '@jpmorganchase/mosaic-plugins/BrokenLinksPlugin',
priority: -1,
Expand Down
2 changes: 1 addition & 1 deletion packages/site/public/search-data.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions packages/site/public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mosaic-mosaic-dev-team.vercel.app/mosaic/author/fragments</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mosaic-mosaic-dev-team.vercel.app/mosaic/author/frontmatter</loc>
<changefreq>weekly</changefreq>
Expand Down Expand Up @@ -44,6 +49,26 @@
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mosaic-mosaic-dev-team.vercel.app/mosaic/fragments/content-fragment</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mosaic-mosaic-dev-team.vercel.app/mosaic/fragments/index</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mosaic-mosaic-dev-team.vercel.app/mosaic/fragments/tile-a</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mosaic-mosaic-dev-team.vercel.app/mosaic/fragments/tile-b</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mosaic-mosaic-dev-team.vercel.app/mosaic/publish/index</loc>
<changefreq>weekly</changefreq>
Expand Down
9 changes: 8 additions & 1 deletion packages/site/snapshots/latest/mosaic/author/aliases.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ sidebarData:
level: 2
link: /mosaic/author/refs
childNodes: []
- id: /mosaic/author/fragments
fullPath: /mosaic/author/fragments.mdx
name: Fragments
priority: 2
data:
level: 2
link: /mosaic/author/fragments
childNodes: []
- id: /mosaic/author/ui-components
fullPath: /mosaic/author/ui-components.mdx
name: UI Components
Expand All @@ -112,7 +120,6 @@ sidebarData:
link: /mosaic/author/page-templates
childNodes: []
---

# {meta.title}

Aliases are virtual 'symlinks' of pages, allowing one page to take on one or more other routes.
Expand Down
1 change: 1 addition & 0 deletions packages/site/snapshots/latest/mosaic/author/fragments
Loading

0 comments on commit 3bc7865

Please sign in to comment.