yak(link-validator-never-sees-compiled-html-internal-link-validation-is-vacuous-ddd1): Link validator never sees compiled HTML — internal-link validation is vacuous
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
defmodule Blogex.LinkValidator do
|
||||
@moduledoc """
|
||||
Validates internal blog link semantics in markdown post bodies.
|
||||
Validates internal blog link semantics in compiled post bodies.
|
||||
|
||||
Post bodies reach this module as HTML: `Blogex.Blog` validates them in
|
||||
`@before_compile`, after NimblePublisher has rendered the markdown. Markdown
|
||||
link syntax is still understood so bodies and snippets can be validated
|
||||
before rendering, but the HTML `<a href="…">` anchors are the ones that
|
||||
matter in the real pipeline.
|
||||
|
||||
Checks that internal links follow the pattern `/blog/{blog_id}/{slug}`
|
||||
where `blog_id` is a valid blog identifier and `slug` matches the
|
||||
@@ -50,21 +56,25 @@ defmodule Blogex.LinkValidator do
|
||||
@slug_regex ~r/^[a-z0-9]+(-[a-z0-9]+)*$/
|
||||
|
||||
@doc """
|
||||
Extracts internal blog links from a markdown body.
|
||||
Extracts internal blog links from a post body.
|
||||
|
||||
Returns a list of link paths (strings) that match the pattern
|
||||
`/blog/{engineering|releases}/{slug}`. External links and non-blog
|
||||
internal links are ignored.
|
||||
Returns a list of link paths (strings) that look like `/blog/…` links.
|
||||
External links and non-blog internal links are ignored. Blog IDs and slugs
|
||||
are *not* judged here — `validate_link/1` classifies those.
|
||||
|
||||
Handles both markdown link syntax `[text](url)` and HTML `<a href="url">`.
|
||||
(Post bodies are HTML, rendered by NimblePublisher at compile time.)
|
||||
HTML anchors are read with Floki, so attribute order, quoting and extra
|
||||
attributes do not matter. Markdown link syntax `[text](url)` is extracted
|
||||
too (raw markdown bodies and snippets), and the two scans are merged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> extract_links("[link](/blog/engineering/post)")
|
||||
["/blog/engineering/post"]
|
||||
|
||||
iex> extract_links("<p><a href=\"/blog/engineering/post\">link</a></p>")
|
||||
iex> extract_links("<p><a href=\\"/blog/engineering/post\\">link</a></p>")
|
||||
["/blog/engineering/post"]
|
||||
|
||||
iex> extract_links("<p><a class=\\"x\\" href='/blog/engineering/post'>link</a></p>")
|
||||
["/blog/engineering/post"]
|
||||
|
||||
iex> extract_links("See [GitHub](https://github.com)")
|
||||
@@ -72,21 +82,24 @@ defmodule Blogex.LinkValidator do
|
||||
"""
|
||||
@spec extract_links(String.t()) :: [String.t()]
|
||||
def extract_links(body) when is_binary(body) do
|
||||
markdown_links =
|
||||
~r/\[([^\]]+)\]\(([^)]+)\)/
|
||||
|> Regex.scan(body)
|
||||
|> Enum.map(fn [_, _, path] -> path end)
|
||||
|
||||
html_links =
|
||||
~r/<a\s+href=["']([^"']*)["']/i
|
||||
|> Regex.scan(body)
|
||||
|> Enum.map(fn [_, path] -> path end)
|
||||
|
||||
(markdown_links ++ html_links)
|
||||
(markdown_links(body) ++ html_links(body))
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(&internal_blog_link?/1)
|
||||
end
|
||||
|
||||
defp markdown_links(body) do
|
||||
~r/\[([^\]]+)\]\(([^)]+)\)/
|
||||
|> Regex.scan(body)
|
||||
|> Enum.map(fn [_, _, path] -> path end)
|
||||
end
|
||||
|
||||
defp html_links(body) do
|
||||
body
|
||||
|> Floki.parse_document!()
|
||||
|> Floki.find("a[href]")
|
||||
|> Enum.flat_map(&Floki.attribute(&1, "href"))
|
||||
end
|
||||
|
||||
defp internal_blog_link?(path) do
|
||||
case parse_blog_link(path) do
|
||||
{_, _} -> true
|
||||
|
||||
Reference in New Issue
Block a user