Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit f429dca

Browse files
committed
feat(plugins/plugin-client-common): allow inlining of snippets inside of tabs
1 parent 294ed45 commit f429dca

File tree

8 files changed

+153
-78
lines changed

8 files changed

+153
-78
lines changed

plugins/plugin-client-common/src/components/Content/Markdown/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,8 @@ export default class Markdown extends React.PureComponent<Props, State> {
181181

182182
if (this.props.tab && this.props.tab.REPL) {
183183
setTimeout(async () => {
184-
const source = await inlineSnippets(this.snippetBasePath)(sourcePriorToInlining, this.props.filepath, {
185-
REPL: this.repl
186-
})
184+
const args = { REPL: this.repl }
185+
const source = await inlineSnippets(args, this.snippetBasePath)(sourcePriorToInlining, this.props.filepath)
187186
this.setState({ source: Markdown.hackSource(source), codeBlockResponses: [] })
188187
})
189188
} else {

plugins/plugin-client-common/src/controller/snippets-inliner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function loader(
125125
return (esModule ? 'export default ' : 'module.exports = ') + json
126126
}
127127

128-
inlineSnippets()(data, srcFilePath, { REPL })
128+
inlineSnippets({ REPL })(data, srcFilePath)
129129
.then(exportIt)
130130
.then(data => callback(null, data))
131131
.catch(callback)

plugins/plugin-client-common/src/controller/snippets.ts

Lines changed: 107 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import { isAbsolute as pathIsAbsolute, dirname as pathDirname, join as pathJoin
1919
import { Arguments, isError } from '@kui-shell/core'
2020
import { loadNotebook } from '@kui-shell/plugin-client-common/notebook'
2121

22-
import { stripFrontmatter } from '../components/Content/Markdown/frontmatter-parser'
22+
import { tryFrontmatter } from '../components/Content/Markdown/frontmatter-parser'
2323

2424
const debug = Debug('plugin-client-common/markdown/snippets')
2525

26-
const RE_SNIPPET = /^--(-*)8<--(-*)\s+"([^"]+)"(\s+"([^"]+)")?\s*$/
26+
const RE_SNIPPET = /^(\s*)--(-*)8<--(-*)\s+"([^"]+)"(\s+"([^"]+)")?\s*$/
2727

2828
function isUrl(a: string) {
2929
return /^https?:/.test(a)
@@ -78,77 +78,103 @@ function rerouteLinks(basePath: string, data: string) {
7878
* Simplistic approximation of
7979
* https://facelessuser.github.io/pymdown-extensions/extensions/snippets/.
8080
*/
81-
export default function inlineSnippets(snippetBasePath?: string) {
82-
return async (data: string, srcFilePath: string, args: Pick<Arguments, 'REPL'>): Promise<string> =>
83-
Promise.all(
84-
data.split(/\n/).map(async line => {
85-
const match = line.match(RE_SNIPPET)
86-
if (!match) {
87-
return line
88-
} else {
89-
const snippetFileName = match[3]
81+
export default function inlineSnippets(
82+
args: Pick<Arguments, 'REPL'>,
83+
snippetBasePath?: string,
84+
includeFrontmatter = true
85+
) {
86+
const fetchRecursively = async (
87+
snippetFileName: string,
88+
srcFilePath: string,
89+
provenance: string[],
90+
optionalSnippetBasePathInSnippetLine?: string
91+
) => {
92+
const getBasePath = (snippetBasePath: string) => {
93+
try {
94+
const basePath = optionalSnippetBasePathInSnippetLine || snippetBasePath
95+
96+
return isAbsolute(basePath) ? basePath : srcFilePath ? join(srcFilePath, basePath) : undefined
97+
} catch (err) {
98+
debug(err)
99+
return undefined
100+
}
101+
}
90102

91-
const getBasePath = (snippetBasePath: string) => {
92-
try {
93-
const basePath = match[5] || snippetBasePath
103+
// Call ourselves recursively, in case a fetched snippet
104+
// fetches other files. We also may need to reroute relative
105+
// <img> and <a> links according to the given `basePath`.
106+
const recurse = (basePath: string, recursedSnippetFileName: string, data: string) => {
107+
// Note: intentionally using `snippetBasePath` for the
108+
// first argument, as this represents the "root" base
109+
// path, either from the URL of the original filepath (we
110+
// may be recursing here) or from the command line or from
111+
// the topmatter of the original document. The second
112+
// represents the current base path in the recursion.
113+
const base = isAbsolute(basePath) || !snippetBasePath ? basePath : snippetBasePath
114+
return inlineSnippets(args, base, false)(rerouteLinks(base, data), recursedSnippetFileName, provenance)
115+
}
94116

95-
return isAbsolute(basePath) ? basePath : srcFilePath ? join(srcFilePath, basePath) : undefined
96-
} catch (err) {
97-
debug(err)
98-
return undefined
99-
}
100-
}
117+
const candidates = optionalSnippetBasePathInSnippetLine
118+
? [optionalSnippetBasePathInSnippetLine]
119+
: ['./', snippetBasePath, '../', '../snippets', '../../snippets', '../../', '../../../'].filter(Boolean)
120+
121+
const snippetDatas = isUrl(snippetFileName)
122+
? [
123+
await loadNotebook(snippetFileName, args)
124+
.then(async data => ({
125+
filepath: snippetFileName,
126+
snippetData: await recurse(snippetBasePath || dirname(snippetFileName), snippetFileName, toString(data))
127+
}))
128+
.catch(err => {
129+
debug('Warning: could not fetch inlined content 1', snippetBasePath, snippetFileName, err)
130+
return err
131+
})
132+
]
133+
: await Promise.all(
134+
candidates
135+
.map(getBasePath)
136+
.filter(Boolean)
137+
.map(myBasePath => ({
138+
myBasePath,
139+
filepath: join(myBasePath, snippetFileName)
140+
}))
141+
.filter(_ => _ && _.filepath !== srcFilePath) // avoid cycles
142+
.map(({ myBasePath, filepath }) =>
143+
loadNotebook(filepath, args)
144+
.then(async data => ({ filepath, snippetData: await recurse(myBasePath, filepath, toString(data)) }))
145+
.catch(err => {
146+
debug('Warning: could not fetch inlined content 2', myBasePath, snippetFileName, err)
147+
return err
148+
})
149+
)
150+
).then(_ => _.filter(Boolean))
151+
152+
const snippetData =
153+
snippetDatas.find(_ => _.snippetData && !isError(_.snippetData)) ||
154+
snippetDatas.find(_ => isError(_.snippetData) && !/ENOTDIR/.test(_.snippetData.message)) ||
155+
snippetDatas[0]
156+
157+
return snippetData
158+
}
101159

102-
// Call ourselves recursively, in case a fetched snippet
103-
// fetches other files. We also may need to reroute relative
104-
// <img> and <a> links according to the given `basePath`.
105-
const recurse = (basePath: string, snippetFileName: string, data: string) => {
106-
// Note: intentionally using `snippetBasePath` for the
107-
// first argument, as this represents the "root" base
108-
// path, either from the URL of the original filepath (we
109-
// may be recursing here) or from the command line or from
110-
// the topmatter of the original document. The second
111-
// represents the current base path in the recursion.
112-
const base = isAbsolute(basePath) || !snippetBasePath ? basePath : snippetBasePath
113-
return inlineSnippets(base)(rerouteLinks(base, data), snippetFileName, args)
114-
}
160+
return async (data: string, srcFilePath: string, provenance: string[] = []): Promise<string> => {
161+
const { body } = tryFrontmatter(data)
115162

116-
const candidates = match[5]
117-
? [match[5]]
118-
: ['./', snippetBasePath, '../', '../snippets', '../../snippets', '../../', '../../../'].filter(Boolean)
119-
120-
const snippetDatas = isUrl(snippetFileName)
121-
? [
122-
await loadNotebook(snippetFileName, args)
123-
.then(data => recurse(snippetBasePath || dirname(snippetFileName), snippetFileName, toString(data)))
124-
.catch(err => {
125-
debug('Warning: could not fetch inlined content 1', snippetBasePath, snippetFileName, err)
126-
return err
127-
})
128-
]
129-
: await Promise.all(
130-
candidates
131-
.map(getBasePath)
132-
.filter(Boolean)
133-
.map(myBasePath => ({
134-
myBasePath,
135-
filepath: join(myBasePath, snippetFileName)
136-
}))
137-
.filter(_ => _ && _.filepath !== srcFilePath) // avoid cycles
138-
.map(({ myBasePath, filepath }) =>
139-
loadNotebook(filepath, args)
140-
.then(data => recurse(myBasePath, filepath, toString(data)))
141-
.catch(err => {
142-
debug('Warning: could not fetch inlined content 2', myBasePath, snippetFileName, err)
143-
return err
144-
})
145-
)
146-
).then(_ => _.filter(Boolean))
147-
148-
const snippetData =
149-
snippetDatas.find(_ => _ && !isError(_)) ||
150-
snippetDatas.find(_ => isError(_) && !/ENOTDIR/.test(_.message)) ||
151-
snippetDatas[0]
163+
const mainContent = await Promise.all(
164+
(includeFrontmatter ? data : body).split(/\n/).map(async line => {
165+
const match = line.match(RE_SNIPPET)
166+
if (!match) {
167+
return line
168+
} else {
169+
const indentation = match[1]
170+
const snippetFileName = match[4]
171+
const optionalSnippetBasePathInSnippetLine = match[6]
172+
const { snippetData } = await fetchRecursively(
173+
snippetFileName,
174+
srcFilePath,
175+
provenance,
176+
optionalSnippetBasePathInSnippetLine
177+
)
152178

153179
if (!snippetData) {
154180
return line
@@ -157,12 +183,20 @@ export default function inlineSnippets(snippetBasePath?: string) {
157183
${indent(snippetData.message)}`
158184
} else {
159185
debug('successfully fetched inlined content', snippetFileName)
160-
161-
// for now, we completely strip off the topmatter from
162-
// snippets. TODO?
163-
return stripFrontmatter(toString(snippetData))
186+
const data = toString(snippetData)
187+
if (indentation && indentation.length > 0) {
188+
return data
189+
.split(/\n/)
190+
.map(line => `${indentation}${line}`)
191+
.join('\n')
192+
} else {
193+
return data
194+
}
164195
}
165196
}
166197
})
167198
).then(_ => _.join('\n'))
199+
200+
return mainContent
201+
}
168202
}

plugins/plugin-client-common/src/test/core/snippets-in-markdown.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,28 @@ const IN4: Input = {
6969
content: IN3.content,
7070
tips: IN3.tips
7171
}
72-
;[IN1, IN1viaUrl, IN2, IN3, IN4].forEach(markdown => {
72+
73+
const IN5: Input = {
74+
input: join(ROOT, 'data', 'snippets-in-tab1.md'),
75+
content: `AAA
76+
Tab1
77+
Tab2
78+
BBB
79+
DDD`,
80+
tips: []
81+
}
82+
83+
const IN6: Input = {
84+
input: join(ROOT, 'data', 'snippets-in-tab2.md'),
85+
content: `AAA
86+
Tab1
87+
Tab2
88+
TipTitle
89+
TipContent
90+
DDD`,
91+
tips: [{ title: 'TipTitle', content: 'TipContent' }]
92+
}
93+
;[IN1, IN1viaUrl, IN2, IN3, IN4, IN5, IN6].forEach(markdown => {
7394
describe(`markdown snippets hash include ${markdown.input} ${process.env.MOCHA_RUN_TARGET ||
7495
''}`, function(this: Common.ISuite) {
7596
before(Common.before(this))
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
AAA
2+
3+
=== "Tab1"
4+
--8<-- "snippets-in-tab1a.md"
5+
6+
=== "Tab2"
7+
--8<-- "snippets-in-tab1b.md"
8+
9+
DDD
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
BBB
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
???+ tip "TipTitle"
2+
TipContent
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
AAA
2+
3+
=== "Tab1"
4+
--8<-- "snippets-in-tab1b.md"
5+
6+
=== "Tab2"
7+
--8<-- "snippets-in-tab1a.md"
8+
9+
DDD

0 commit comments

Comments
 (0)