|
1 | 1 | import { describe, expect, test } from 'vitest' |
| 2 | +import type { AnnotationComment, AnnotationTag } from '../src/core/types' |
| 3 | +import { parseAnnotationComments } from '../src/core/parse' |
| 4 | +import { splitCodeLines } from './utils' |
| 5 | + |
| 6 | +type PartialAnnotationComment = Partial<Omit<AnnotationComment, 'tag'>> & { tag: Partial<AnnotationTag> } |
2 | 7 |
|
3 | 8 | describe('parseAnnotationComments()', () => { |
4 | | - // TODO: We need to test if the parser properly skips processing the second annotation tag |
5 | | - // in the following code, which requires checking the comment range of the previously processed |
6 | | - // annotation comment and skipping all tags located before the end of the comment range: |
7 | | - // "someCode() // [!note] Mismatching comment # [!syntax] also prevents chaining" |
8 | | - test('Todo', () => { |
9 | | - expect(true).toBe(true) |
| 9 | + describe('Successfully parses code examples in various languages', () => { |
| 10 | + test('JavaScript (including JSDoc)', () => { |
| 11 | + const jsTestCode = ` |
| 12 | +import { defineConfig } from 'astro/config'; |
| 13 | +
|
| 14 | +/** |
| 15 | + * Some JSDoc test. |
| 16 | + * |
| 17 | + * [!note:test] The \`test\` function here is just an example |
| 18 | + * and doesn't do anything meaningful. |
| 19 | + * |
| 20 | + * Also note that we just added a [!note] tag to an existing |
| 21 | + * JSDoc comment to create this note. |
| 22 | + */ |
| 23 | +export async function test(a, b, c) { |
| 24 | + let x = true |
| 25 | + let y = a.toString() |
| 26 | + const z = \`hello\${a === true ? 'x' : 'y'}\` |
| 27 | + const fn = () => "test\\nanother line" |
| 28 | +} |
| 29 | +
|
| 30 | +// Single-line comment |
| 31 | +var v = 300 // [!ins] |
| 32 | +test(1, Math.min(6, 2), defineConfig.someProp || v) |
| 33 | +
|
| 34 | +export default defineConfig({ |
| 35 | + markdown: { |
| 36 | + // [!note:"'some.example'"] This setting does not actually exist. |
| 37 | + 'some.example': 2048, |
| 38 | + smartypants: false, // [!ins] |
| 39 | + /* [!ins] */ gfm: false, |
| 40 | + } |
| 41 | +}); |
| 42 | + `.trim() |
| 43 | + const comments = getComments(jsTestCode) |
| 44 | + expect(comments).toHaveLength(5) |
| 45 | + expect(comments).toMatchObject([ |
| 46 | + { |
| 47 | + tag: { name: 'note', targetSearchQuery: 'test' }, |
| 48 | + contents: [ |
| 49 | + `The \`test\` function here is just an example`, |
| 50 | + `and doesn't do anything meaningful.`, |
| 51 | + ``, |
| 52 | + `Also note that we just added a [!note] tag to an existing`, |
| 53 | + `JSDoc comment to create this note.`, |
| 54 | + ], |
| 55 | + }, |
| 56 | + { |
| 57 | + tag: { name: 'ins', targetSearchQuery: undefined }, |
| 58 | + contents: [], |
| 59 | + }, |
| 60 | + { |
| 61 | + tag: { name: 'note', targetSearchQuery: "'some.example'" }, |
| 62 | + contents: [`This setting does not actually exist.`], |
| 63 | + }, |
| 64 | + { |
| 65 | + tag: { name: 'ins', targetSearchQuery: undefined }, |
| 66 | + contents: [], |
| 67 | + }, |
| 68 | + { |
| 69 | + tag: { name: 'ins', targetSearchQuery: undefined }, |
| 70 | + contents: [], |
| 71 | + }, |
| 72 | + ] as PartialAnnotationComment[]) |
| 73 | + }) |
| 74 | + |
| 75 | + test('CSS', () => { |
| 76 | + const cssTestCode = ` |
| 77 | +@media (min-width: 50em) { |
| 78 | + :root { |
| 79 | + --min-spacing-inline: calc(0.5vw - 1.5rem); /* [!ins] */ |
| 80 | + /* [!del] */ color: blue; |
| 81 | + } |
| 82 | + body, html, .test[data-size="large"], #id { |
| 83 | + /* [!note:linear-gradient] |
| 84 | + As this [!note] points out, we let the browser |
| 85 | + create a gradient for us here. */ |
| 86 | + background: linear-gradient(to top, #80f 1px, rgb(30, 90, 130) 50%); |
| 87 | + } |
| 88 | + .frame:focus-within :focus-visible ~ .copy button:not(:hover) { |
| 89 | + content: 'Hello \\000026 welcome!'; |
| 90 | + opacity: 0.75; |
| 91 | + } |
| 92 | +} |
| 93 | + `.trim() |
| 94 | + const comments = getComments(cssTestCode) |
| 95 | + expect(comments).toHaveLength(3) |
| 96 | + expect(comments).toMatchObject([ |
| 97 | + { |
| 98 | + tag: { name: 'ins', targetSearchQuery: undefined }, |
| 99 | + contents: [], |
| 100 | + }, |
| 101 | + { |
| 102 | + tag: { name: 'del', targetSearchQuery: undefined }, |
| 103 | + contents: [], |
| 104 | + }, |
| 105 | + { |
| 106 | + tag: { name: 'note', targetSearchQuery: 'linear-gradient' }, |
| 107 | + contents: [ |
| 108 | + // Content lines |
| 109 | + `As this [!note] points out, we let the browser`, |
| 110 | + `create a gradient for us here.`, |
| 111 | + ], |
| 112 | + }, |
| 113 | + ] as PartialAnnotationComment[]) |
| 114 | + }) |
| 115 | + |
| 116 | + test('Astro', () => { |
| 117 | + const astroTestCode = ` |
| 118 | +--- |
| 119 | +import Header from './Header.astro'; |
| 120 | +import Logo from './Logo.astro'; |
| 121 | +import Footer from './Footer.astro'; |
| 122 | +
|
| 123 | +// [!note:title] By destructuring the \`Astro.props\` object, |
| 124 | +// we can access the \`title\` prop passed to this component. |
| 125 | +const { title } = Astro.props |
| 126 | +--- |
| 127 | +<div id="content-wrapper" class="test"> |
| 128 | + <Header /> |
| 129 | + <Logo size="large"/> |
| 130 | + <!-- [!note:{title}] By wrapping any variable name in curly braces, |
| 131 | + we can output its value in the HTML template, |
| 132 | + as explained by this [!note] annotation. --> |
| 133 | + <h1>{title} & some text</h1> |
| 134 | + <slot /> <!-- [!note] Children passed to the component will be inserted here --> |
| 135 | + <Footer /> |
| 136 | +</div> |
| 137 | + `.trim() |
| 138 | + const comments = getComments(astroTestCode) |
| 139 | + expect(comments).toHaveLength(3) |
| 140 | + expect(comments).toMatchObject([ |
| 141 | + { |
| 142 | + tag: { name: 'note', targetSearchQuery: 'title' }, |
| 143 | + contents: [ |
| 144 | + // Content lines |
| 145 | + `By destructuring the \`Astro.props\` object,`, |
| 146 | + `we can access the \`title\` prop passed to this component.`, |
| 147 | + ], |
| 148 | + }, |
| 149 | + { |
| 150 | + tag: { name: 'note', targetSearchQuery: '{title}' }, |
| 151 | + contents: [ |
| 152 | + // Content lines |
| 153 | + `By wrapping any variable name in curly braces,`, |
| 154 | + `we can output its value in the HTML template,`, |
| 155 | + `as explained by this [!note] annotation.`, |
| 156 | + ], |
| 157 | + }, |
| 158 | + { |
| 159 | + tag: { name: 'note', targetSearchQuery: undefined }, |
| 160 | + contents: [`Children passed to the component will be inserted here`], |
| 161 | + }, |
| 162 | + ] as PartialAnnotationComment[]) |
| 163 | + }) |
| 164 | + |
| 165 | + test('Python', () => { |
| 166 | + const pythonTestCode = ` |
| 167 | +import time |
| 168 | +
|
| 169 | +# [!note] This function will print a countdown from the given time, |
| 170 | +# as explained by this # [!note] annotation. |
| 171 | +def countdown(time_sec): |
| 172 | + while time_sec: |
| 173 | + mins, secs = divmod(time_sec, 60) |
| 174 | + timeformat = '{:02d}:{:02d}'.format(mins, secs) |
| 175 | + print(timeformat, end='\\r') |
| 176 | + time.sleep(1) |
| 177 | + time_sec -= 1 # [!note] This is important to actually count down |
| 178 | + print("stop") |
| 179 | +
|
| 180 | +countdown(5) |
| 181 | + `.trim() |
| 182 | + const comments = getComments(pythonTestCode) |
| 183 | + expect(comments).toMatchObject([ |
| 184 | + { |
| 185 | + tag: { name: 'note', targetSearchQuery: undefined }, |
| 186 | + contents: [ |
| 187 | + // Content lines |
| 188 | + `This function will print a countdown from the given time,`, |
| 189 | + `as explained by this # [!note] annotation.`, |
| 190 | + ], |
| 191 | + }, |
| 192 | + { |
| 193 | + tag: { name: 'note', targetSearchQuery: undefined }, |
| 194 | + contents: [`This is important to actually count down`], |
| 195 | + }, |
| 196 | + ] as PartialAnnotationComment[]) |
| 197 | + }) |
10 | 198 | }) |
| 199 | + |
| 200 | + test('Supports chaining multiple matching single-line annotations on the same line', () => { |
| 201 | + const lines = [ |
| 202 | + `// [!note] This is the note content. // [!ins]`, |
| 203 | + `console.log('Inserted line with an attached note')`, |
| 204 | + `testCode() // [!mark] // [!note] It also works at the end of a line.`, |
| 205 | + ] |
| 206 | + const comments = getComments(lines.join('\n')) |
| 207 | + expect(comments).toHaveLength(4) |
| 208 | + expect(comments).toMatchObject([ |
| 209 | + { |
| 210 | + tag: { name: 'note', targetSearchQuery: undefined }, |
| 211 | + commentRange: { start: { line: 0 }, end: { line: 0, column: lines[0].indexOf(' // [!ins]') } }, |
| 212 | + contents: [`This is the note content.`], |
| 213 | + }, |
| 214 | + { |
| 215 | + tag: { name: 'ins', targetSearchQuery: undefined }, |
| 216 | + commentRange: { start: { line: 0, column: lines[0].indexOf(' // [!ins]') }, end: { line: 0 } }, |
| 217 | + contents: [], |
| 218 | + }, |
| 219 | + { |
| 220 | + tag: { name: 'mark', targetSearchQuery: undefined }, |
| 221 | + commentRange: { |
| 222 | + start: { line: 2, column: lines[2].indexOf(' // [!mark]') }, |
| 223 | + end: { line: 2, column: lines[2].indexOf(' // [!note]') }, |
| 224 | + }, |
| 225 | + contents: [], |
| 226 | + }, |
| 227 | + { |
| 228 | + tag: { name: 'note', targetSearchQuery: undefined }, |
| 229 | + commentRange: { start: { line: 2, column: lines[2].indexOf(' // [!note]') }, end: { line: 2 } }, |
| 230 | + contents: [`It also works at the end of a line.`], |
| 231 | + }, |
| 232 | + ] as PartialAnnotationComment[]) |
| 233 | + }) |
| 234 | + |
11 | 235 | // TODO: We also need to implement and test the `[!ignore-tags]` logic |
| 236 | + |
| 237 | + function getComments(code: string) { |
| 238 | + const codeLines = splitCodeLines(code) |
| 239 | + return parseAnnotationComments({ codeLines }) |
| 240 | + } |
12 | 241 | }) |
0 commit comments