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.
@@ -45,8 +45,9 @@ module Markdowndocs
45
45
  end
46
46
 
47
47
  def render_markdown(markdown)
48
- doc = Commonmarker.parse(markdown, options: Markdowndocs.config.markdown_options)
49
- html = doc.to_html(options: Markdowndocs.config.markdown_options)
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
- doc = Nokogiri::HTML.fragment(html)
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
- sanitizer = Rails::HTML5::SafeListSanitizer.new
138
+ allow_svg = Markdowndocs.config.allow_svg
96
139
 
97
- sanitizer.sanitize(
140
+ Rails::HTML5::SafeListSanitizer.new.sanitize(
98
141
  html,
99
- tags: %w[
100
- h1 h2 h3 h4 h5 h6 p br hr blockquote
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 class="bg-white dark:bg-slate-800 rounded-lg shadow-sm p-8">
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