Skip to content

Commit 61c8d15

Browse files
committed
feat: add built-in support for MDX
1 parent c336149 commit 61c8d15

File tree

12 files changed

+126
-62
lines changed

12 files changed

+126
-62
lines changed

internal/core/format.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var FormatByExtension = map[string][]string{
7373
`\.(?:js|jsx)$`: {".js", "code"},
7474
`\.(?:lua)$`: {".lua", "code"},
7575
`\.(?:md|mdown|markdown|markdn)$`: {".md", "markup"},
76+
`\.(?:mdx)$`: {".mdx", "markup"},
7677
`\.(?:org)$`: {".org", "markup"},
7778
`\.(?:php)$`: {".php", "code"},
7879
`\.(?:pl|pm|pod)$`: {".r", "code"},

internal/core/location.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func initialPosition(ctx, txt string, a Alert) (int, string) {
4646
// by ignoring these inline code spans.
4747
//
4848
// TODO: What about `scope: raw`?
49-
size := len(ctx)
49+
size := nlp.StrLen(ctx)
5050
for _, fs := range fsi {
5151
start := fs[0] - 1
5252
end := fs[1] + 1

internal/lint/html.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type extensionConfig struct {
3131
var blockDelimiters = map[string]string{
3232
".adoc": "\n----\n$1\n----\n",
3333
".md": "\n```\n$1\n```\n",
34+
".mdx": "\n```\n$1\n```\n",
3435
".rst": "\n::\n\n%s\n",
3536
".org": orgExample,
3637
}
@@ -82,6 +83,7 @@ func applyBlockPatterns(c *core.Config, exts extensionConfig, content string) (s
8283
var inlineDelimiters = map[string]string{
8384
".adoc": "`$1`",
8485
".md": "`$1`",
86+
".mdx": "`$1`",
8587
".rst": "``$1``",
8688
".org": "=$1=",
8789
}

internal/lint/lint.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ func (l *Linter) lintFile(src string) lintResult {
193193
err = l.lintADoc(file)
194194
case ".md":
195195
err = l.lintMarkdown(file)
196+
case ".mdx":
197+
err = l.lintMDX(file)
196198
case ".rst":
197199
err = l.lintRST(file)
198200
case ".xml":

internal/lint/mdx.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package lint
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"os/exec"
7+
"strings"
8+
9+
"github.com/errata-ai/vale/v3/internal/core"
10+
"github.com/errata-ai/vale/v3/internal/nlp"
11+
"github.com/errata-ai/vale/v3/internal/system"
12+
)
13+
14+
func (l Linter) lintMDX(f *core.File) error {
15+
var html string
16+
var err error
17+
18+
exe := system.Which([]string{"mdx2vast"})
19+
if exe == "" {
20+
return core.NewE100("lintMDX", errors.New("mdx2vast not found"))
21+
}
22+
23+
s, err := l.Transform(f)
24+
if err != nil {
25+
return err
26+
}
27+
28+
html, err = callVast(f, s, exe)
29+
if err != nil {
30+
return core.NewE100(f.Path, err)
31+
}
32+
33+
// NOTE: This is required to avoid finding matches inside info strings. For
34+
// example, if we're looking for 'json' we many incorrectly report the
35+
// location as being in an infostring like '```json'.
36+
//
37+
// See https://github.com/errata-ai/vale/v2/issues/248.
38+
body := reExInfo.ReplaceAllStringFunc(f.Content, func(m string) string {
39+
parts := strings.Split(m, "`")
40+
41+
// This ensures that we respect the number of opening backticks, which
42+
// could be more than 3.
43+
//
44+
// See https://github.com/errata-ai/vale/v2/issues/271.
45+
tags := strings.Repeat("`", len(parts)-1)
46+
span := strings.Repeat("*", nlp.StrLen(parts[len(parts)-1]))
47+
48+
return tags + span
49+
})
50+
51+
// NOTE: This is required to avoid finding matches inside link references.
52+
body = reLinkRef.ReplaceAllStringFunc(body, func(m string) string {
53+
return "][" + strings.Repeat("*", nlp.StrLen(m)-3) + "]"
54+
})
55+
body = reLinkDef.ReplaceAllStringFunc(body, func(m string) string {
56+
return "[" + strings.Repeat("*", nlp.StrLen(m)-3) + "]:"
57+
})
58+
59+
// NOTE: This is required to avoid finding matches inside ordered lists.
60+
body = reNumericList.ReplaceAllStringFunc(body, func(m string) string {
61+
return strings.Repeat("*", nlp.StrLen(m))
62+
})
63+
64+
f.Content = body
65+
return l.lintHTMLTokens(f, []byte(html), 0)
66+
}
67+
68+
func callVast(_ *core.File, text, exe string) (string, error) {
69+
var out bytes.Buffer
70+
var eut bytes.Buffer
71+
72+
cmd := exec.Command(exe)
73+
cmd.Stdin = strings.NewReader(text)
74+
cmd.Stdout = &out
75+
cmd.Stderr = &eut
76+
77+
if err := cmd.Run(); err != nil {
78+
return "", errors.New(eut.String())
79+
}
80+
81+
return out.String(), nil
82+
}

testdata/features/lint.feature

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,9 @@ Feature: Lint
249249
When I lint "test.mdx"
250250
Then the output should contain exactly:
251251
"""
252-
test.mdx:3:1:vale.Annotations:'NOTE' left in text
253-
test.mdx:32:1:vale.Annotations:'XXX' left in text
254-
test.mdx:34:29:vale.Annotations:'TODO' left in text
255-
test.mdx:36:3:vale.Annotations:'TODO' left in text
256-
test.mdx:36:10:vale.Annotations:'XXX' left in text
257-
test.mdx:36:16:vale.Annotations:'FIXME' left in text
258-
test.mdx:40:21:vale.Annotations:'FIXME' left in text
259-
test.mdx:44:5:vale.Annotations:'TODO' left in text
260-
test.mdx:46:3:vale.Annotations:'TODO' left in text
252+
test.mdx:13:70:vale.Annotations:'TODO' left in text
253+
test.mdx:24:1:vale.Annotations:'TODO' left in text
254+
test.mdx:34:26:vale.Annotations:'TODO' left in text
261255
"""
262256
And the exit status should be 0
263257

@@ -370,6 +364,7 @@ Feature: Lint
370364
Then the output should contain exactly:
371365
"""
372366
test.mdx:3:1:vale.Annotations:'NOTE' left in text
367+
test.mdx:29:5:vale.Annotations:'XXX' left in text
373368
test.mdx:32:1:vale.Annotations:'XXX' left in text
374369
test.mdx:34:29:vale.Annotations:'TODO' left in text
375370
test.mdx:36:3:vale.Annotations:'TODO' left in text
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
StylesPath = ../../styles
22
MinAlertLevel = suggestion
33

4-
[formats]
5-
mdx = md
6-
74
[*.{md,rst,org,adoc,mdx}]
85
BasedOnStyles = vale, demo
96

@@ -12,6 +9,3 @@ demo.CheckLinks = NO
129
demo.Spellcheck = NO
1310
demo.ZeroOccurrence = NO
1411
demo.SentenceCaseAny = NO
15-
16-
[*.mdx]
17-
CommentDelimiters = {/*, */}

testdata/fixtures/formats/.vale.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ StylesPath = ../../styles
22
MinAlertLevel = suggestion
33

44
[formats]
5-
mdx = md
65
ts = js
76

87
[*]
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
StylesPath = ../../../styles
22
MinAlertLevel = suggestion
33

4-
[formats]
5-
mdx = md
6-
74
[*.mdx]
85
vale.Annotations = YES

testdata/fixtures/formats/test.mdx

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,48 @@
1-
## Introduction
1+
# Hello, world!
22

3-
NOTE: This is a very interesting file.
3+
import {External} from './some/place.js'
4+
import {Chart} from './chart.js'
5+
import population from './population.js'
6+
import XXX from './XXX.js'
47

5-
```python
6-
def foo():
7-
"""
8-
NOTE: this is a very important function.
9-
"""
10-
obviously = False
11-
very = True
12-
return very and obviously
13-
```
8+
export const TODO = 3.14
9+
export const Local = properties => <span style={{color: 'red'}} {...properties} />
1410

15-
As you can see, `very = True`. Here is another interesting codeblock:
11+
<Chart data={population} label={'Something with ' + pi} />
1612

17-
```go
18-
// XXX: fix this relatively soon.
19-
func ExtFromSyntax(name string) string {
20-
for XXX, relatively := range LookupSyntaxName {
21-
if matched, _ := regexp.MatchString(r, name); matched {
22-
return s
23-
}
24-
}
25-
return name
26-
}
27-
```
13+
An <External>XXX</External> component and a <Local>local one</Local> TODO.
2814

29-
XXX = False
30-
# This is a codeblock.
15+
<div className="note">
16+
> Some notable things in a block quote!
17+
</div>
3118
32-
XXX: This is a very interesting sentence that is not in a block.
19+
<Welcome name="Venus" />
20+
<XXX name="Mars" />
3321

34-
this sentence has -- dashes TODO: but it should still work.
22+
<MyComponent id="123" />
3523

36-
| TODO | XXX | FIXME |
37-
|:----:|:---:|:-----:|
38-
| one | two | three |
24+
TODO: You can also use objects with components, such as the `TODO` component on
25+
the `myComponents` object: <myComponents.thisOne />
3926

40-
> **Note**: one two FIXME three.
27+
<Component
28+
open
29+
x={1}
30+
label={'this is a string, *not* markdown!'}
31+
icon={<Icon />}
32+
/>
4133

42-
[![Boxy Monokai ★ Predawn][img-monokai]][img-monokai]
34+
Two 🍰 is: {Math.PI * 2}, TODO
4335

44-
is *TODO* by
36+
{(function () {
37+
const TODO = Math.random()
4538

46-
![TODO](/images/logo.png)
39+
if (guess > 0.66) {
40+
return <span style={{color: 'tomato'}}>Look at us.</span>
41+
}
4742

48-
[img-monokai]: https://
43+
if (guess > 0.33) {
44+
return <span style={{color: 'violet'}}>Who would have guessed?!</span>
45+
}
4946

50-
RedHat
47+
return <span style={{color: 'goldenrod'}}>Not me.</span>
48+
})()}

0 commit comments

Comments
 (0)