Skip to content

Commit

Permalink
Merge pull request #234 from zazuko/mc-improvements
Browse files Browse the repository at this point in the history
Configure autoLink, idPrefix, custom classes and other options.
  • Loading branch information
ludovicm67 authored Dec 11, 2023
2 parents 72826bd + 0aa1e52 commit 365368c
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/big-trainers-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zazuko/trifid-markdown-content": minor
---

Configure idPrefix and classes.
5 changes: 5 additions & 0 deletions .changeset/six-pets-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zazuko/trifid-markdown-content": minor
---

Custom templates can be used using `template` configuration option
6 changes: 6 additions & 0 deletions .changeset/tough-deers-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@zazuko/trifid-markdown-content": minor
---

Add support for `autoLink` configuration.
If set to `true`, which is the default value, this will add links to headings.
16 changes: 10 additions & 6 deletions packages/markdown-content/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,25 @@ middlewares:
module: "@zazuko/trifid-markdown-content"
order: 80
config:
namespace: custom-content
directory: file:content/custom
mountPath: /content/
namespace: custom-content
```
This will create a new `custom-content` namespace that will serve the content located in the `content/custom` directory.
The content will be available with the `/content/` prefix.

## Configuration

The following options are supported
The following options are supported:

- `namespace`: The namespace of the content. This is used to separate the content from other namespaces.
- `directory`: The directory where the content is located. This should be a local directory.
- `mountPath`: The path where the content should be mounted. This should be a path that is not used by other middlewares.
- `directory`: The directory where the content is located. This should be a local directory (required).
- `mountPath`: The path where the content should be mounted. This should be a path that is not used by other middlewares (required).
- `namespace`: The namespace of the content. This is used to separate the content from other namespaces (default: `default`).
- `idPrefix`: The prefix to use for the generated IDs for headings (default: `markdown-content-`).
- `classes`: The classes to add to the generated HTML (default: `{}`). Keys should be the CSS selectors, values should be the classes to add.
- `autoLink`: If `true`, will automatically add links to headings (default: `true`).
- `template`: Path to an alternative template (default: `views/content.hbs`).

## Content

Expand Down Expand Up @@ -86,9 +90,9 @@ middlewares:
module: "@zazuko/trifid-markdown-content"
order: 80
config:
namespace: root-content
directory: file:content
mountPath: /
namespace: root-content
```

That way, you will have two new endpoints:
Expand Down
84 changes: 53 additions & 31 deletions packages/markdown-content/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,47 @@ import addClasses from './addClasses.js'

const currentDir = dirname(fileURLToPath(import.meta.url))

const LOCALS_PLUGIN_KEY = 'markdown-content-plugin'

/**
* Return a HTML string from a Markdown string.
*
* @param {string} markdownString
* @param {string} markdownString Markdown string
* @param {Record<string, any>} config configuration
* @returns HTML string
*/
const convertToHtml = async (markdownString) => {
const html = await unified()
.use(remarkParse)
.use(remarkFrontmatter)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeSlug, {
prefix: 'markdown-content-',
})
.use(rehypeAutolinkHeadings, {
const convertToHtml = async (markdownString, config) => {
const processors = [
[remarkParse],
[remarkFrontmatter],
[remarkGfm],
[remarkRehype],
[rehypeSlug, {
prefix: config.idPrefix,
}],
]

if (config.autoLink) {
// @ts-ignore
processors.push([rehypeAutolinkHeadings, {
// @ts-ignore
behavior: 'wrap',
properties: {
class: 'headers-autolink',
},
})
.use(addClasses, {
h1: 'h1',
h2: 'h2',
h3: 'h3',
h4: 'h4',
h5: 'h5',
table: 'table',
})
.use(rehypeStringify)
.process(markdownString)
}])
}

processors.push([addClasses, config.classes])
processors.push([rehypeStringify])

const processor = unified()
for (const [plugin, options] of processors) {
// @ts-ignore
processor.use(plugin, options)
}

const html = await processor.process(markdownString)

return html.toString()
}
Expand Down Expand Up @@ -81,9 +91,10 @@ const getItems = async (path) => {
* Read all markdown files from a directory and convert them in HTML format.
*
* @param {string} path path of the directory to read
* @param {Record<string, any>} config configuration
* @returns list of files that are in that directory
*/
const getContent = async (path) => {
const getContent = async (path, config) => {
const files = []

const pathContent = await fs.readdir(path, { withFileTypes: true })
Expand All @@ -98,7 +109,7 @@ const getContent = async (path) => {
}

const content = await fs.readFile(fullPath, 'utf-8')
const html = await convertToHtml(content)
const html = await convertToHtml(content, config)
files.push({
language: item.name.replace(/\.md*/, ''),
path: fullPath,
Expand Down Expand Up @@ -151,21 +162,21 @@ const contentMiddleware = ({ logger, namespace, store }) => async (_req, res, ne
logger.debug(`loaded store into '${namespace}' namespace`)

// just make sure that the `content-plugin` entry exists
if (!res.locals['content-plugin']) {
res.locals['content-plugin'] = {}
if (!res.locals[LOCALS_PLUGIN_KEY]) {
res.locals[LOCALS_PLUGIN_KEY] = {}
}

// add all configured entries for the specified namespace
const lang = res?.locals?.currentLanguage || 'en'
res.locals['content-plugin'][namespace] = entriesForLanguage(store, lang)
res.locals[LOCALS_PLUGIN_KEY][namespace] = entriesForLanguage(store, lang)

// let's forward all of this to other middlewares
return next()
}

const factory = async (trifid) => {
const { config, logger, server, render } = trifid
const { namespace, directory, mountPath } = config
const { namespace, directory, mountPath, idPrefix, classes, autoLink, template } = config

// check config
const configuredNamespace = namespace ?? 'default'
Expand All @@ -174,11 +185,22 @@ const factory = async (trifid) => {
}
const mountAtPath = mountPath || false

const configuredIdPrefix = idPrefix || 'markdown-content-'
const configuredClasses = classes || {}
const configuredAutolink = !!autoLink || autoLink === 'true'
const configuredTemplate = template || `${currentDir}/../views/content.hbs`

const contentConfiguration = {
idPrefix: configuredIdPrefix,
classes: configuredClasses,
autoLink: configuredAutolink,
}

const store = {}
const items = await getItems(directory)

for (const item of items) {
store[item.name] = await getContent(item.path)
store[item.name] = await getContent(item.path, contentConfiguration)
}

// apply the middleware in all cases
Expand All @@ -190,8 +212,8 @@ const factory = async (trifid) => {

for (const item of items) {
server.get(`${mountAtPathSlash}${item.name}`, async (_req, res, _next) => {
return res.send(await render(`${currentDir}/../views/content.hbs`, {
content: res.locals['content-plugin'][configuredNamespace][item.name] || '',
return res.send(await render(configuredTemplate, {
content: res.locals[LOCALS_PLUGIN_KEY][configuredNamespace][item.name] || '',
locals: res.locals,
}))
})
Expand Down
161 changes: 161 additions & 0 deletions packages/markdown-content/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,165 @@ describe('@zazuko/trifid-markdown-content', () => {
}
})
})

describe('features', () => {
it('should use the configured classes', async () => {
const trifidInstance = await createTrifidInstance({
directory: './test/support/content/',
mountPath: '/content',
classes: {
h1: 'h1-class',
},
})
const trifidListener = await trifidInstance.start()

try {
const pluginUrl = `${getListenerURL(trifidListener)}/content/test-entry`

const res = await fetch(pluginUrl)
const body = await res.text()
const match = body.match(/h1-class/) || false

strictEqual(res.status, 200)
strictEqual(match && match.length > 0, true)

// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e
} finally {
trifidListener.close()
}
})

it('should use the configured idPrefix', async () => {
const trifidInstance = await createTrifidInstance({
directory: './test/support/content/',
mountPath: '/content',
idPrefix: 'custom-prefix-',
})
const trifidListener = await trifidInstance.start()

try {
const pluginUrl = `${getListenerURL(trifidListener)}/content/test-entry?lang=en`

const res = await fetch(pluginUrl)
const body = await res.text()
const match = body.match(/custom-prefix-title/) || false

strictEqual(res.status, 200)
strictEqual(match && match.length > 0, true)

// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e
} finally {
trifidListener.close()
}
})

it('should use the configured template', async () => {
const trifidInstance = await createTrifidInstance({
directory: './test/support/content/',
mountPath: '/content',
template: './test/support/custom.hbs',
})
const trifidListener = await trifidInstance.start()

try {
const pluginUrl = `${getListenerURL(trifidListener)}/content/test-entry`

const res = await fetch(pluginUrl)
const body = await res.text()
const match = body.match(/This is using custom template/) || false

strictEqual(res.status, 200)
strictEqual(match && match.length > 0, true)

// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e
} finally {
trifidListener.close()
}
})

it('should use autoLink', async () => {
const trifidInstance = await createTrifidInstance({
directory: './test/support/content/',
mountPath: '/content',
autoLink: true,
})
const trifidListener = await trifidInstance.start()

try {
const pluginUrl = `${getListenerURL(trifidListener)}/content/test-entry?lang=en`

const res = await fetch(pluginUrl)
const body = await res.text()
const match = body.match(/ href="#markdown-content-title"/) || false

strictEqual(res.status, 200)
strictEqual(match && match.length > 0, true)

// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e
} finally {
trifidListener.close()
}
})

it('should use autoLink and custom idPrefix', async () => {
const trifidInstance = await createTrifidInstance({
directory: './test/support/content/',
mountPath: '/content',
autoLink: true,
idPrefix: 'custom-prefix-',
})
const trifidListener = await trifidInstance.start()

try {
const pluginUrl = `${getListenerURL(trifidListener)}/content/test-entry?lang=en`

const res = await fetch(pluginUrl)
const body = await res.text()
const match = body.match(/ href="#custom-prefix-title"/) || false

strictEqual(res.status, 200)
strictEqual(match && match.length > 0, true)

// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e
} finally {
trifidListener.close()
}
})

it('should not insert links if autoLink=false', async () => {
const trifidInstance = await createTrifidInstance({
directory: './test/support/content/',
mountPath: '/content',
autoLink: false,
})
const trifidListener = await trifidInstance.start()

try {
const pluginUrl = `${getListenerURL(trifidListener)}/content/test-entry?lang=en`

const res = await fetch(pluginUrl)
const body = await res.text()
const match = body.match(/ href="#markdown-content-title"/) || false

strictEqual(res.status, 200)
strictEqual(match && match.length > 0, false)

// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e
} finally {
trifidListener.close()
}
})
})
})
6 changes: 6 additions & 0 deletions packages/markdown-content/test/support/custom.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div class="container">
<div class="trifid-content">
<p>This is using custom template</p>
{{{ content }}}
</div>
</div>

0 comments on commit 365368c

Please sign in to comment.