Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cmd/check/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ var Cmd = &cobra.Command{
"not fail.\n\n" +
"\"link not resolved: TARGET\" means TARGET is a sibling .md file that exists\n" +
"under the documentation root but has no page_id yet -- the normal state of\n" +
"a tree that hasn't been published, not a defect. A same-page anchor\n" +
"(#heading) hits the same warning when the current file itself has no\n" +
"page_id yet, which can read as though the file names itself as an\n" +
"unresolved target -- it doesn't; that's just this file, before its first\n" +
"publish.",
"a tree that hasn't been published, not a defect. \"same-page anchor not\n" +
"resolved: #heading\" is the same situation for a same-page anchor: it\n" +
"resolves to a real heading in the current file, but can't be turned into\n" +
"an absolute URL until this file itself has a page_id -- resolved by this\n" +
"file's own first publish, nothing to fix.",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: completion.MarkdownFiles,
RunE: run,
Expand Down
32 changes: 32 additions & 0 deletions cmd/check/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,38 @@ func TestRunWarnings(t *testing.T) {
}
}

func TestRunSamePageAnchorUnpublishedWarnsDistinctly(t *testing.T) {
dir := t.TempDir()
write(t, filepath.Join(dir, "draft.md"), "# Draft\n\n[back to top](#draft)\n")

out, err := captureOutput(t, func() error { return run(testCmd(t, ""), []string{filepath.Join(dir, "draft.md")}) })
if err != nil {
t.Fatalf("run: %v (warnings alone must not fail)", err)
}
if !strings.Contains(out, "same-page anchor not resolved: #draft") {
t.Errorf("output = %q, want the same-page-anchor warning", out)
}
if strings.Contains(out, "link not resolved") {
t.Errorf("output = %q, must not read as an unresolved cross-file link to itself", out)
}
}

func TestRunSelfReferenceWithBadFragmentDoesNotClaimAnchorResolved(t *testing.T) {
dir := t.TempDir()
write(t, filepath.Join(dir, "draft.md"), "# Draft\n\n[bad self ref](draft.md#does-not-exist)\n")

out, err := captureOutput(t, func() error { return run(testCmd(t, ""), []string{filepath.Join(dir, "draft.md")}) })
if err != nil {
t.Fatalf("run: %v (warnings alone must not fail)", err)
}
if !strings.Contains(out, "anchor not found: draft.md#does-not-exist") {
t.Errorf("output = %q, want the anchor-not-found warning", out)
}
if strings.Contains(out, "same-page anchor not resolved") {
t.Errorf("output = %q, must not claim the anchor resolved when it didn't", out)
}
}

func TestRunBroken(t *testing.T) {
dir := t.TempDir()
write(t, filepath.Join(dir, "main.md"), "# Main\n\n![missing](nope.png)\n")
Expand Down
29 changes: 27 additions & 2 deletions internal/convert/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (r *storageRenderer) rewriteHref(
href string, node ast.Node, source []byte,
) (newHref string, rewritten bool, brokenText string) {
prefix := r.linePrefix(node, source)
original := href // as authored, before any same-page/anchor rewrite -- see rewriteDocLink's self-reference case.
if strings.HasPrefix(href, "#") {
if nf, ok := r.index.Anchor(r.currentDocKey, decodeDestination(href[1:])); ok {
// Same-page anchors become fake cross-file links to the current
Expand Down Expand Up @@ -96,7 +97,7 @@ func (r *storageRenderer) rewriteHref(
}
}

newHref, ok, brokenText := r.rewriteDocLink(href, prefix)
newHref, ok, brokenText := r.rewriteDocLink(href, original, rewritten, prefix)
if brokenText != "" {
return "", false, brokenText
}
Expand All @@ -118,7 +119,20 @@ func (r *storageRenderer) rewriteHref(
// entirely" apart from "not published yet": both look identical to
// index.Page (a miss), but only the first is a defect. prefix is rewriteHref's
// already-computed line prefix, threaded through rather than recomputed.
func (r *storageRenderer) rewriteDocLink(href, prefix string) (newHref string, ok bool, brokenText string) {
// original is href as the author wrote it, before rewriteHref's same-page
// anchor branch turned it into a fake cross-file link to the current file --
// needed only to word the self-reference case of the "not published yet"
// warning below without naming this file as if it were some other target.
// anchorResolved is rewriteHref's rewritten flag: whether the fragment was
// actually matched to a heading before this call, which is what tells a
// genuine same-page anchor (fully resolved, just pending this page's own
// publish) apart from a self-referencing href whose fragment never matched
// anything -- that one already got its own "anchor not found" warning
// upstream, and must fall through to the generic message below rather than
// claim a resolution that didn't happen.
func (r *storageRenderer) rewriteDocLink(
href, original string, anchorResolved bool, prefix string,
) (newHref string, ok bool, brokenText string) {
path, fragment := href, ""
if i := strings.Index(href, "#"); i >= 0 {
path, fragment = href[:i], href[i:]
Expand All @@ -137,6 +151,17 @@ func (r *storageRenderer) rewriteDocLink(href, prefix string) (newHref string, o
return "", false, r.reportLinkBroken(prefix, href, "outside the documentation root")
case !r.index.FileExists(key):
return "", false, r.reportLinkBroken(prefix, href, "not found")
case key == r.currentDocKey && anchorResolved:
// The target is this file itself -- a same-page anchor (or an
// explicit self-referencing "thisfile.md#frag") that resolved to a
// real heading but can't be fully qualified until this page has
// its own page_id. "link not resolved: thisfile.md#Frag" would
// read as if some other file were the broken reference; it isn't.
// A self-reference whose fragment never matched anything falls
// through to the default case instead: anchorResolved is false, and
// "anchor not found" already covered it upstream.
r.warnings = append(r.warnings, prefix+fmt.Sprintf("same-page anchor not resolved: %s", original))
return "", false, ""
default:
// The file exists but has no page_id yet -- the normal state of
// every page in a tree that hasn't been published, not an error.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Same Page Anchor Unpublished

This file has no `page_id` yet, so a same-page anchor resolves to a real
heading but can't be turned into an absolute URL -- a Warning distinct from
the "link not resolved" one a genuinely unpublished sibling produces (#118),
since the target here is this file itself.

Jump to [Getting Started](#getting-started).

A self-reference whose fragment matches no heading at all is a different
case -- it must not claim the anchor resolved, since it didn't:
[bad self anchor](main.md#does-not-exist).

## Getting Started

Some getting-started content.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"attachments": [],
"broken": [],
"html": "<h1>Same Page Anchor Unpublished</h1>\n<p>This file has no <code>page_id</code> yet, so a same-page anchor resolves to a real heading but can't be turned into an absolute URL -- a Warning distinct from the &quot;link not resolved&quot; one a genuinely unpublished sibling produces (#118), since the target here is this file itself.</p>\n<p>Jump to <a href=\"main.md#Getting-Started\">Getting Started</a>.</p>\n<p>A self-reference whose fragment matches no heading at all is a different case -- it must not claim the anchor resolved, since it didn't: <a href=\"main.md#does-not-exist\">bad self anchor</a>.</p>\n<h2>Getting Started</h2>\n<p>Some getting-started content.</p>\n",
"warnings": [
"line 8: same-page anchor not resolved: #getting-started",
"line 12: anchor not found: main.md#does-not-exist",
"line 12: link not resolved: main.md#does-not-exist"
]
}