Module: OKF::Markdown::Links
- Defined in:
- lib/okf/markdown/links.rb
Overview
Markdown cross-link extraction and resolution โ the single source of truth for "which concepts does this body point at". Shared by OKF::Bundle::Graph (to build edges) and OKF::Bundle::Validator (to warn on broken cross-links, ยง5.3), so both agree on what counts as a link and where it resolves.
Constant Summary collapse
- FENCE =
/\A(```|~~~)/.freeze
- CODE_SPAN =
Inline code span: a run of N backticks, the shortest content (which may hold shorter backtick runs, per CommonMark), then a matching run of N backticks. Links inside inline code render as literal text, not edges, so these are blanked before scanning โ the inline analogue of FENCE.
/(`+).*?\1/.freeze
- INLINE_LINK =
/(?<!!)\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/.freeze
- REFERENCE_LINK =
Reference-style use: full [text][label] or collapsed [label][]; (?<!!) skips images. group 1 = text/label, group 2 = the explicit label (empty if collapsed).
/(?<!!)\[([^\]]*)\]\[([^\]]*)\]/.freeze
- DEFINITION =
Reference definition: [label]: target (optionally followed by a "title").
/\A[ \t]{0,3}\[([^\]]+)\]:[ \t]*(\S+)/.freeze
- SCHEME =
%r{\A[a-z][a-z0-9+.-]*://}.freeze
Class Method Summary collapse
-
.each_prose_line(text) ⇒ Object
Yield each line outside a fenced code block, with inline code spans blanked.
-
.extract(body) ⇒ Object
Raw link targets in
body, in document order, skipping fenced code blocks and image links. -
.reference_definitions(text) ⇒ Object
Map every reference definition (+[label]: target+) to its target, keyed by the lowercased label.
-
.resolve(raw, from:, bundle:) ⇒ Object
Resolve a raw link target to a bundle-relative
.mdpath, ornilwhen the target is not an in-scope markdown cross-link (external scheme, mailto, non-+.md+, directory, or empty).
Class Method Details
.each_prose_line(text) ⇒ Object
Yield each line outside a fenced code block, with inline code spans blanked. Both exclusions mirror the rendered document: fenced and inline code are literal text, so a link written inside them is not a cross-link.
61 62 63 64 65 66 67 68 69 70 |
# File 'lib/okf/markdown/links.rb', line 61 def each_prose_line(text) in_fence = false text.each_line do |line| if FENCE.match?(line.strip) in_fence = !in_fence next end yield line.gsub(CODE_SPAN, " ") unless in_fence end end |
.extract(body) ⇒ Object
Raw link targets in body, in document order, skipping fenced code blocks
and image links. Handles both inline links and standard reference-style links
(resolving [text][label] against its [label]: target definition). Targets
keep any #anchor โ resolve strips it.
31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/okf/markdown/links.rb', line 31 def extract(body) text = body.to_s definitions = reference_definitions(text) found = [] each_prose_line(text) do |line| found.concat(line.scan(INLINE_LINK).flatten) line.scan(REFERENCE_LINK).each do |label, explicit| key = (explicit.empty? ? label : explicit).strip.downcase target = definitions[key] found << target if target end end found end |
.reference_definitions(text) ⇒ Object
Map every reference definition (+[label]: target+) to its target, keyed by the lowercased label. Definitions may appear anywhere, so they are collected before uses are resolved.
49 50 51 52 53 54 55 56 |
# File 'lib/okf/markdown/links.rb', line 49 def reference_definitions(text) definitions = {} each_prose_line(text) do |line| match = DEFINITION.match(line) definitions[match[1].strip.downcase] = match[2] if match end definitions end |
.resolve(raw, from:, bundle:) ⇒ Object
Resolve a raw link target to a bundle-relative .md path, or nil when the
target is not an in-scope markdown cross-link (external scheme, mailto,
non-+.md+, directory, or empty). A relative link that escapes the bundle root
is returned verbatim, so the validator can flag it "not found" and the graph
can drop it.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/okf/markdown/links.rb', line 80 def resolve(raw, from:, bundle:) target = raw.to_s.split("#", 2).first.to_s return nil if target.empty? || target.end_with?("/") return nil if target.match?(SCHEME) || target.start_with?("mailto:") return nil unless target.end_with?(".md") return target.sub(%r{\A/+}, "") if target.start_with?("/") bundle_abs = File.(bundle) source_dir = File.dirname(File.(from, bundle_abs)) candidate = File.(target, source_dir) if candidate == bundle_abs || candidate.start_with?("#{bundle_abs}/") Pathname.new(candidate).relative_path_from(Pathname.new(bundle_abs)).to_s else target end end |