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

Commit 7cd9a9e

Browse files
committed
fix(plugins/plugin-client-common): markdown with non-tab/tip indentation formats poorly
e.g. a tab after tip inside a tab, where the tip itself is indented not due being in a tab or tip, sigh see plugins/plugin-client-common/tests/data/tab-after-tip-in-tab2.md for an example of this
1 parent 5201968 commit 7cd9a9e

File tree

5 files changed

+147
-38
lines changed

5 files changed

+147
-38
lines changed

packages/test/src/api/Markdown.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { ISuite } from './common'
18+
import { OUTPUT_LAST } from './selectors'
19+
1720
export default class Markdown {
1821
public readonly tip = '.kui--markdown-tip'
1922
public readonly tabs = '.kui--markdown-tabs'
@@ -22,6 +25,10 @@ export default class Markdown {
2225
private readonly _codeBlock = '.kui--code-block-in-markdown'
2326
public readonly runButton = '.kui--block-action-run'
2427

28+
public getText(ctx: ISuite) {
29+
return ctx.app.client.$(OUTPUT_LAST).then(_ => _.getText())
30+
}
31+
2532
public tipWithTitle(title: string) {
2633
return `${this.tip}[data-title="${title}"]`
2734
}

plugins/plugin-client-common/src/components/Content/Markdown/rehype-tabbed/index.ts

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { v4 } from 'uuid'
1919
import { Element } from 'hast'
2020
import { START_OF_TIP, END_OF_TIP } from '../rehype-tip'
2121

22+
/** A placeholder marker to indicate markdown that uses indentation not to mean Tab or Tip content */
23+
const FAKE_END_MARKER = ''
24+
2225
// const RE_TAB = /^(.|[\n\r])*===\s+"(.+)"\s*(\n(.|[\n\r])*)?$/
2326
const RE_TAB = /^===\s+"([^"]+)"/
2427

@@ -176,52 +179,52 @@ export function hackIndentation(source: string): string {
176179

177180
const indentDepthOfContent: number[] = []
178181

179-
const rewrite = source.split(/\n/).map(line => {
180-
const tabStartMatch = line.match(/^(\s*)===\s+".*"/)
181-
const tipStartMatch = line.match(/^(\s*)[?!][?!][?!](\+?)\s+(tip|todo|bug|info|note|warning|success|question)/)
182-
const startMatch = tabStartMatch || tipStartMatch
183-
184-
const unindent = (line: string) => {
185-
if (!inBlockquote && !inCodeBlock) {
186-
return line.replace(/^\s*/, '')
182+
const unindent = (line: string) => {
183+
if (!inBlockquote && !inCodeBlock) {
184+
return line.replace(/^\s*/, '')
185+
} else {
186+
if (blockquoteOrCodeBlockIndent > 0) {
187+
return line.replace(new RegExp(`^\\s{${blockquoteOrCodeBlockIndent}}`), '')
187188
} else {
188-
if (blockquoteOrCodeBlockIndent > 0) {
189-
return line.replace(new RegExp(`^\\s{${blockquoteOrCodeBlockIndent}}`), '')
190-
} else {
191-
return line
192-
}
189+
return line
193190
}
194191
}
192+
}
195193

196-
const pop = (line: string, delta = 0) => {
197-
const indentMatch = line.match(/^\s+/)
198-
const curIndentDepth = indentDepthOfContent[indentDepthOfContent.length - 1]
199-
const thisIndentDepth = !indentMatch ? 0 : ~~(indentMatch[0].length / 4)
200-
201-
let pop = ''
202-
for (let idx = curIndentDepth; idx > thisIndentDepth + delta; idx--) {
203-
indentDepthOfContent.pop()
204-
const endMarker = endMarkers.pop()
205-
if (endMarker) {
206-
pop += `\n${endMarker}\n\n`
207-
}
194+
const pop = (line: string, delta = 0) => {
195+
const indentMatch = line.match(/^\s+/)
196+
const curIndentDepth = indentDepthOfContent[indentDepthOfContent.length - 1]
197+
const thisIndentDepth = !indentMatch ? 0 : ~~(indentMatch[0].length / 4)
198+
199+
let pop = ''
200+
for (let idx = curIndentDepth; idx > thisIndentDepth + delta; idx--) {
201+
indentDepthOfContent.pop()
202+
const endMarker = endMarkers.pop()
203+
if (endMarker) {
204+
pop += `\n${endMarker}\n\n`
208205
}
206+
}
209207

210-
if (pop) {
211-
if (endMarkers.length === 0) {
212-
inTab = undefined
213-
} else {
214-
const indentDepth = indentDepthOfContent[indentDepthOfContent.length - 1]
215-
inTab = new RegExp(
216-
'^' +
217-
Array(indentDepth * 4)
218-
.fill(' ')
219-
.join('')
220-
)
221-
}
208+
if (pop) {
209+
if (endMarkers.length === 0) {
210+
inTab = undefined
211+
} else {
212+
const indentDepth = indentDepthOfContent[indentDepthOfContent.length - 1]
213+
inTab = new RegExp(
214+
'^' +
215+
Array(indentDepth * 4)
216+
.fill(' ')
217+
.join('')
218+
)
222219
}
223-
return pop
224220
}
221+
return pop
222+
}
223+
224+
const rewrite = source.split(/\n/).map(line => {
225+
const tabStartMatch = line.match(/^(\s*)===\s+".*"/)
226+
const tipStartMatch = line.match(/^(\s*)[?!][?!][?!](\+?)\s+(tip|todo|bug|info|note|warning|success|question)/)
227+
const startMatch = tabStartMatch || tipStartMatch
225228

226229
if (!inBlockquote && startMatch) {
227230
const thisIndentation = startMatch[1] || ''
@@ -249,6 +252,14 @@ export function hackIndentation(source: string): string {
249252

250253
const startMarker = tabStartMatch ? START_OF_TAB : START_OF_TIP
251254

255+
if (thisIndentDepth > indentDepth + 1) {
256+
// then the markdown is using indentation in ways beyond that
257+
// which would be "normal" for pymdown, i.e. to indicate Tab
258+
// or Tip content
259+
endMarkers.push(FAKE_END_MARKER)
260+
indentDepthOfContent.push(-1)
261+
}
262+
252263
if (endMarker === END_OF_TIP || endMarkers.length === 0 || thisIndentDepth > indentDepth) {
253264
endMarkers.push(endMarker)
254265
indentDepthOfContent.push(thisIndentDepth)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2022 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ok } from 'assert'
18+
import { basename, dirname, join } from 'path'
19+
import { encodeComponent } from '@kui-shell/core'
20+
import { Common, CLI, ReplExpect, Selectors } from '@kui-shell/test'
21+
22+
const ROOT = join(dirname(require.resolve('@kui-shell/plugin-client-common/tests/data/tab-after-tip-in-tab1.md')), '..')
23+
24+
const IN1 = {
25+
input: join(ROOT, 'data', 'tab-after-tip-in-tab1.md'),
26+
tips: [{ title: 'XXXX', content: 'YYYY' }],
27+
textToBeFound: 'YYYY',
28+
textNotToBeFound: 'AAAA' // we don't want the second tab's text to be shown on initial render!
29+
}
30+
31+
const IN2 = {
32+
input: join(ROOT, 'data', 'tab-after-tip-in-tab2.md'),
33+
tips: IN1.tips,
34+
textToBeFound: 'Bullet1',
35+
textNotToBeFound: IN1.textNotToBeFound
36+
}
37+
;[IN1, IN2].forEach(markdown => {
38+
describe(`markdown tab after tip in tab ${basename(markdown.input)} ${process.env.MOCHA_RUN_TARGET ||
39+
''}`, function(this: Common.ISuite) {
40+
before(Common.before(this))
41+
after(Common.after(this))
42+
43+
markdown.tips.forEach(tip => {
44+
it(`should load the markdown and show tab after tip in tab with title=${tip.title}`, async () => {
45+
try {
46+
await CLI.command(`commentary -f ${encodeComponent(markdown.input)}`, this.app).then(
47+
ReplExpect.okWithString(markdown.textToBeFound)
48+
)
49+
50+
const allText = await Selectors.Markdown.getText(this)
51+
ok(allText.indexOf(markdown.textNotToBeFound) < 0)
52+
53+
const tipSelector = Selectors.Markdown.tipWithTitle(tip.title)
54+
const tipElement = await this.app.client.$(tipSelector)
55+
await tipElement.waitForExist({ timeout: CLI.waitTimeout })
56+
57+
const content = await this.app.client.$(Selectors.Markdown.tipContent(tipSelector))
58+
await content.waitForExist({ timeout: CLI.waitTimeout })
59+
60+
await this.app.client.waitUntil(
61+
async () => {
62+
const actualText = await content.getText()
63+
return actualText === tip.content
64+
},
65+
{ timeout: CLI.waitTimeout }
66+
)
67+
} catch (err) {
68+
await Common.oops(this, true)(err)
69+
}
70+
})
71+
})
72+
})
73+
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== "Tab1"
2+
!!! warning "XXXX"
3+
YYYY
4+
5+
=== "Tab2"
6+
AAAA
7+
8+
BBBB
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
=== "Tab1"
2+
- Bullet1
3+
!!! warning "XXXX"
4+
YYYY
5+
6+
=== "Tab2"
7+
AAAA
8+
9+
BBBB
10+

0 commit comments

Comments
 (0)