markdowndocs 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -0
- data/README.md +95 -14
- data/app/controllers/markdowndocs/docs_controller.rb +29 -9
- data/app/controllers/markdowndocs/preferences_controller.rb +62 -3
- data/app/helpers/markdowndocs/docs_helper.rb +6 -2
- data/app/models/markdowndocs/documentation.rb +136 -20
- data/app/services/markdowndocs/markdown_renderer.rb +50 -19
- data/app/views/markdowndocs/docs/_mode_switcher.html.erb +2 -1
- data/app/views/markdowndocs/docs/show.html.erb +6 -1
- data/config/routes.rb +11 -0
- data/docs/superpowers/plans/2026-05-15-path-based-audience-routing.md +1619 -0
- data/docs/superpowers/specs/2026-05-15-path-based-audience-routing-design.md +311 -0
- data/lib/markdowndocs/configuration.rb +9 -1
- data/lib/markdowndocs/version.rb +1 -1
- data/lib/markdowndocs.rb +7 -0
- metadata +3 -1
|
@@ -45,8 +45,9 @@ module Markdowndocs
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def render_markdown(markdown)
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
options = markdown_render_options
|
|
49
|
+
doc = Commonmarker.parse(markdown, options: options)
|
|
50
|
+
html = doc.to_html(options: options)
|
|
50
51
|
html = apply_syntax_highlighting(html)
|
|
51
52
|
sanitize_html(html)
|
|
52
53
|
rescue => e
|
|
@@ -56,7 +57,9 @@ module Markdowndocs
|
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def apply_syntax_highlighting(html)
|
|
59
|
-
|
|
60
|
+
# HTML5 parsing preserves case-sensitive SVG/MathML foreign-content
|
|
61
|
+
# attributes (e.g. viewBox) that Nokogiri::HTML would lowercase.
|
|
62
|
+
doc = Nokogiri::HTML5.fragment(html)
|
|
60
63
|
|
|
61
64
|
doc.css("pre[lang]").each do |pre|
|
|
62
65
|
language = pre["lang"]
|
|
@@ -91,25 +94,53 @@ module Markdowndocs
|
|
|
91
94
|
"<pre><code>#{ERB::Util.html_escape(code)}</code></pre>"
|
|
92
95
|
end
|
|
93
96
|
|
|
97
|
+
# Force commonmarker to pass raw HTML through when SVG is allowed, so the
|
|
98
|
+
# markup reaches the sanitizer (the security boundary) instead of being
|
|
99
|
+
# escaped. Returns a copy — does not mutate the configured options.
|
|
100
|
+
def markdown_render_options
|
|
101
|
+
options = Markdowndocs.config.markdown_options
|
|
102
|
+
return options unless Markdowndocs.config.allow_svg
|
|
103
|
+
|
|
104
|
+
options.merge(render: (options[:render] || {}).merge(unsafe: true))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
BASE_SANITIZE_TAGS = %w[
|
|
108
|
+
h1 h2 h3 h4 h5 h6 p br hr blockquote
|
|
109
|
+
ul ol li dl dt dd
|
|
110
|
+
table thead tbody tfoot tr th td
|
|
111
|
+
a img
|
|
112
|
+
strong em b i u del
|
|
113
|
+
code pre span div
|
|
114
|
+
].freeze
|
|
115
|
+
|
|
116
|
+
BASE_SANITIZE_ATTRS = %w[href title src alt align class lang].freeze
|
|
117
|
+
|
|
118
|
+
# Curated structural SVG subset. Deliberately excludes script,
|
|
119
|
+
# foreignObject, and SMIL animate/set tags, plus all on* handlers — the
|
|
120
|
+
# SafeListSanitizer drops anything not listed, so scripts/handlers and
|
|
121
|
+
# javascript: URIs are stripped even with unsafe HTML enabled.
|
|
122
|
+
SVG_SANITIZE_TAGS = %w[
|
|
123
|
+
svg g path rect circle ellipse line polyline polygon
|
|
124
|
+
text tspan defs marker title desc
|
|
125
|
+
].freeze
|
|
126
|
+
|
|
127
|
+
SVG_SANITIZE_ATTRS = %w[
|
|
128
|
+
viewBox d points
|
|
129
|
+
x y x1 y1 x2 y2 cx cy r rx ry width height
|
|
130
|
+
fill stroke stroke-width stroke-linecap stroke-linejoin stroke-dasharray
|
|
131
|
+
transform opacity text-anchor dominant-baseline
|
|
132
|
+
font-size font-family font-weight
|
|
133
|
+
marker-start marker-end markerWidth markerHeight refX refY orient
|
|
134
|
+
role aria-label id
|
|
135
|
+
].freeze
|
|
136
|
+
|
|
94
137
|
def sanitize_html(html)
|
|
95
|
-
|
|
138
|
+
allow_svg = Markdowndocs.config.allow_svg
|
|
96
139
|
|
|
97
|
-
|
|
140
|
+
Rails::HTML5::SafeListSanitizer.new.sanitize(
|
|
98
141
|
html,
|
|
99
|
-
tags:
|
|
100
|
-
|
|
101
|
-
ul ol li dl dt dd
|
|
102
|
-
table thead tbody tfoot tr th td
|
|
103
|
-
a img
|
|
104
|
-
strong em b i u del
|
|
105
|
-
code pre span div
|
|
106
|
-
],
|
|
107
|
-
attributes: %w[
|
|
108
|
-
href title
|
|
109
|
-
src alt
|
|
110
|
-
align
|
|
111
|
-
class lang
|
|
112
|
-
]
|
|
142
|
+
tags: allow_svg ? BASE_SANITIZE_TAGS + SVG_SANITIZE_TAGS : BASE_SANITIZE_TAGS,
|
|
143
|
+
attributes: allow_svg ? BASE_SANITIZE_ATTRS + SVG_SANITIZE_ATTRS : BASE_SANITIZE_ATTRS
|
|
113
144
|
)
|
|
114
145
|
end
|
|
115
146
|
end
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
turbo_action: "replace"
|
|
42
42
|
}
|
|
43
43
|
) do |f| %>
|
|
44
|
-
<%= f.hidden_field :mode, value: mode %>
|
|
44
|
+
<%= f.hidden_field :mode, value: mode, id: nil %>
|
|
45
|
+
<%= f.hidden_field :current_path, value: request.fullpath, id: nil %>
|
|
45
46
|
<button
|
|
46
47
|
type="submit"
|
|
47
48
|
role="radio"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<% content_for :title, "#{@doc.title} — Documentation" %>
|
|
2
|
+
|
|
1
3
|
<div class="min-h-screen bg-gray-50 dark:bg-slate-900">
|
|
2
4
|
<div class="max-w-7xl mx-auto px-4 py-12
|
|
3
5
|
sm:px-6
|
|
@@ -66,7 +68,10 @@
|
|
|
66
68
|
lg:grid-cols-12">
|
|
67
69
|
<!-- Main content -->
|
|
68
70
|
<main class="lg:col-span-8">
|
|
69
|
-
<article
|
|
71
|
+
<article
|
|
72
|
+
tabindex="-1"
|
|
73
|
+
autofocus
|
|
74
|
+
class="bg-white dark:bg-slate-800 rounded-lg shadow-sm p-8">
|
|
70
75
|
<div class="prose prose-indigo dark:prose-invert max-w-none">
|
|
71
76
|
<%= raw @rendered_content %>
|
|
72
77
|
</div>
|
data/config/routes.rb
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
Markdowndocs::Engine.routes.draw do
|
|
4
4
|
root "docs#index"
|
|
5
5
|
get "search_index", to: "docs#search_index", as: :search_index
|
|
6
|
+
|
|
7
|
+
# Mode-scoped doc route: matches /<mode>/<slug> where <mode> is one of
|
|
8
|
+
# the configured modes. Must come BEFORE the unconstrained :slug route
|
|
9
|
+
# so the more specific match wins.
|
|
10
|
+
mode_constraint = if Markdowndocs.config.modes.any?
|
|
11
|
+
Regexp.new("(?:#{Markdowndocs.config.modes.map { |m| Regexp.escape(m) }.join("|")})")
|
|
12
|
+
else
|
|
13
|
+
/impossible/
|
|
14
|
+
end
|
|
15
|
+
get ":mode/:slug", to: "docs#show", as: :scoped_doc, constraints: {mode: mode_constraint}
|
|
16
|
+
|
|
6
17
|
get ":slug", to: "docs#show", as: :doc
|
|
7
18
|
resource :preference, only: [:update]
|
|
8
19
|
end
|