Skip to content

Commit b47ccb0

Browse files
committed
fix: don't mask an autolink's URL twice
Fixes #847 Signed-off-by: Joseph Kato <joseph@jdkato.io>
1 parent 65f5687 commit b47ccb0

3 files changed

Lines changed: 141 additions & 1 deletion

File tree

‎internal/lint/walk.go‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ type walker struct {
8888
// since their position is derived from its own. Reset with the block.
8989
inline []inlineCapture
9090

91+
// pending holds the attribute values of the element just opened, waiting
92+
// to be masked out of the context. They can't be masked on the start tag
93+
// itself: an autolink's `href` and its text are the same source bytes, so
94+
// masking the attribute leaves the text token to find its string somewhere
95+
// else in the document -- and `idx`, which only moves forward, then places
96+
// every later block on that line. See #847.
97+
pending []string
98+
9199
begin int
92100
end int
93101

@@ -126,7 +134,22 @@ func (w *walker) update(txt string, tokt html.TokenType) {
126134
}
127135
}
128136

137+
// flush masks the attribute values held since the last start tag.
138+
//
139+
// `text` is the element's own text, if it has arrived; an attribute equal to it
140+
// is dropped rather than masked, because the two are one run of source and
141+
// masking it twice consumes an occurrence that belongs to something else.
142+
func (w *walker) flush(text string) {
143+
for _, val := range w.pending {
144+
if val != text {
145+
w.update(val, html.TextToken)
146+
}
147+
}
148+
w.pending = nil
149+
}
150+
129151
func (w *walker) reset() {
152+
w.flush("")
130153
for _, s := range w.queue {
131154
w.update(s, html.TextToken)
132155
}
@@ -143,6 +166,10 @@ func (w *walker) getCtx() string {
143166

144167
func (w *walker) append(text string) {
145168
if text != "" {
169+
// Before the search below, so the text finds its own occurrence, and
170+
// after the drop, so an autolink's `href` doesn't take it first.
171+
w.flush(text)
172+
146173
pos := w.advance(text)
147174
if pos > -1 {
148175
w.idx = pos
@@ -340,6 +367,9 @@ func (w *walker) walk() (html.TokenType, html.Token, string) {
340367
func (w *walker) replaceToks(tok html.Token) {
341368
tags := core.StringInSlice(tok.Data, []string{
342369
"img", "a", "p", "script", "h1", "h2", "h3", "h4", "h5", "h6", "span"})
370+
// Anything still waiting belongs to an element that had no text of its own.
371+
w.flush("")
372+
343373
if tags {
344374
names := []string{"href", "id", "src", "alt"}
345375
if w.ext == ".html" {
@@ -356,7 +386,7 @@ func (w *walker) replaceToks(tok html.Token) {
356386
if a.Key == "href" {
357387
a.Val, _ = url.QueryUnescape(a.Val)
358388
}
359-
w.update(a.Val, html.TextToken)
389+
w.pending = append(w.pending, a.Val)
360390
}
361391
}
362392
}

‎internal/lint/walk_test.go‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,52 @@ func TestWalkerClasses(t *testing.T) {
148148
})
149149
}
150150
}
151+
152+
// An element's attributes are masked out of the context, but not the source
153+
// bytes its own text still has to find. An autolink writes the URL once and
154+
// the parser reports it twice -- as `href` and as text -- so masking the
155+
// attribute first sent the text's search to the next copy of that URL, wherever
156+
// in the document it happened to be. See #847.
157+
func TestWalkerFlush(t *testing.T) {
158+
tests := []struct {
159+
name string
160+
pending []string
161+
text string
162+
want string
163+
}{
164+
{
165+
name: "autolink keeps its own bytes",
166+
pending: []string{"http://github.com"},
167+
text: "http://github.com",
168+
want: "http://github.com here\nand http://github.com there\n",
169+
},
170+
{
171+
name: "a link target is masked",
172+
pending: []string{"http://github.com"},
173+
text: "read this",
174+
want: "@@@@@@@@@@@@@@@@@ here\nand http://github.com there\n",
175+
},
176+
{
177+
name: "no text of its own",
178+
pending: []string{"http://github.com"},
179+
text: "",
180+
want: "@@@@@@@@@@@@@@@@@ here\nand http://github.com there\n",
181+
},
182+
}
183+
184+
const src = "http://github.com here\nand http://github.com there\n"
185+
for _, tt := range tests {
186+
t.Run(tt.name, func(t *testing.T) {
187+
w := &walker{context: []byte(src), pending: tt.pending}
188+
189+
w.flush(tt.text)
190+
191+
if got := w.getCtx(); got != tt.want {
192+
t.Errorf("flush(%q) left %q, want %q", tt.text, got, tt.want)
193+
}
194+
if w.pending != nil {
195+
t.Errorf("pending = %v, want nil", w.pending)
196+
}
197+
})
198+
}
199+
}

‎testdata/e2e/patterns.yaml‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,64 @@ cases:
130130
want: |
131131
guides/b.md:1:15:T.Tok:'ZQX' found
132132
tutorials/a.md:1:27:T.Tok:'QQV' found
133+
134+
- name: repeated-autolink
135+
about: "#847 -- an autolink's URL is one run of source, reported twice by
136+
the parser; masking the `href` copy used to send the text's search to the
137+
next identical URL in the document, moving every later block's alerts
138+
onto its line. The three files differ only in lines that hold no alert,
139+
so all three report the same two."
140+
files:
141+
.vale.ini: |
142+
StylesPath = styles
143+
MinAlertLevel = suggestion
144+
145+
[*.md]
146+
BasedOnStyles = T
147+
styles/T/GitHub.yml: |
148+
extends: substitution
149+
message: Use '%s' instead of '%s'
150+
level: warning
151+
ignorecase: true
152+
swap:
153+
GitHub: GitHub
154+
trailing-prose.md: |
155+
http://github.com bar
156+
157+
[github](#whatever)
158+
[github1](#whatever)
159+
[github.](#whatever)
160+
161+
https://jwodder.github.io/
162+
http://github.com
163+
164+
whatever
165+
trailing-url.md: |
166+
http://github.com bar
167+
168+
[github](#whatever)
169+
[github1](#whatever)
170+
[github.](#whatever)
171+
172+
https://jwodder.github.io/
173+
174+
http://github.com
175+
no-leading-url.md: |
176+
bar
177+
178+
[github](#whatever)
179+
[github1](#whatever)
180+
[github.](#whatever)
181+
182+
https://jwodder.github.io/
183+
184+
http://github.com
185+
args: .
186+
exit: 0
187+
want: |
188+
no-leading-url.md:3:2:T.GitHub:Use 'GitHub' instead of 'github'
189+
no-leading-url.md:5:2:T.GitHub:Use 'GitHub' instead of 'github'
190+
trailing-prose.md:3:2:T.GitHub:Use 'GitHub' instead of 'github'
191+
trailing-prose.md:5:2:T.GitHub:Use 'GitHub' instead of 'github'
192+
trailing-url.md:3:2:T.GitHub:Use 'GitHub' instead of 'github'
193+
trailing-url.md:5:2:T.GitHub:Use 'GitHub' instead of 'github'

0 commit comments

Comments
 (0)