@@ -122,6 +122,43 @@ defmodule ExDoc.DocAST do
122
122
do: text
123
123
end
124
124
125
+ @ doc """
126
+ Extracts the headers which have anchors (aka ids) in them.
127
+ """
128
+ def extract_headers_with_ids ( ast , headers ) do
129
+ ast
130
+ |> reduce_tags ( [ ] , fn { tag , attrs , inner , _ } , acc ->
131
+ with true <- tag in headers ,
132
+ id = Keyword . get ( attrs , :id , "" ) ,
133
+ text = ExDoc.DocAST . text ( inner ) ,
134
+ true <- id != "" and text != "" do
135
+ [ { tag , text , id } | acc ]
136
+ else
137
+ _ -> acc
138
+ end
139
+ end )
140
+ |> Enum . reverse ( )
141
+ end
142
+
143
+ @ doc """
144
+ Adds an id attribute to the given headers.
145
+ """
146
+ def add_ids_to_headers ( doc_ast , headers ) do
147
+ doc_ast
148
+ |> map_reduce_tags ( % { } , fn { tag , attrs , inner , meta } = ast , seen ->
149
+ if tag in headers and not Keyword . has_key? ( attrs , :id ) do
150
+ possible_id = inner |> text ( ) |> ExDoc.Utils . text_to_id ( )
151
+ id_count = Map . get ( seen , possible_id , 0 )
152
+ actual_id = if id_count >= 1 , do: "#{ possible_id } -#{ id_count } " , else: possible_id
153
+ seen = Map . put ( seen , possible_id , id_count + 1 )
154
+ { { tag , [ id: actual_id ] ++ attrs , inner , meta } , seen }
155
+ else
156
+ { ast , seen }
157
+ end
158
+ end )
159
+ |> elem ( 0 )
160
+ end
161
+
125
162
@ doc """
126
163
Compute a synopsis from a document by looking at its first paragraph.
127
164
"""
@@ -144,14 +181,11 @@ defmodule ExDoc.DocAST do
144
181
@ doc """
145
182
Remove ids from elements.
146
183
"""
147
- def remove_ids ( { tag , attrs , inner , meta } ) ,
148
- do: { tag , Keyword . delete ( attrs , :href ) , remove_ids ( inner ) , meta }
149
-
150
- def remove_ids ( list ) when is_list ( list ) ,
151
- do: Enum . map ( list , & remove_ids / 1 )
152
-
153
- def remove_ids ( other ) ,
154
- do: other
184
+ def remove_ids ( ast ) do
185
+ map_tags ( ast , fn { tag , attrs , inner , meta } ->
186
+ { tag , Keyword . delete ( attrs , :href ) , inner , meta }
187
+ end )
188
+ end
155
189
156
190
@ doc """
157
191
Returns text content from the given AST.
@@ -199,57 +233,52 @@ defmodule ExDoc.DocAST do
199
233
defp pivot ( [ head | tail ] , acc , headers ) , do: pivot ( tail , [ head | acc ] , headers )
200
234
defp pivot ( [ ] , acc , _headers ) , do: Enum . reverse ( acc )
201
235
202
- def highlight ( html , language , opts \\ [ ] ) do
203
- do_highlight ( html , language . highlight_info ( ) , opts )
204
- end
205
-
206
- defp do_highlight (
207
- { :pre , pre_attrs , [ { :code , code_attrs , [ code ] , code_meta } ] , pre_meta } = ast ,
208
- highlight_info ,
209
- opts
210
- )
211
- when is_binary ( code ) do
212
- { lang , code_attrs } = Keyword . pop ( code_attrs , :class , "" )
213
-
214
- case pick_language_and_lexer ( lang , highlight_info , code ) do
215
- { _lang , nil , _lexer_opts } ->
216
- ast
236
+ @ doc """
237
+ Highlights the code blocks in the AST.
238
+ """
239
+ def highlight ( ast , language , opts \\ [ ] ) do
240
+ highlight_info = language . highlight_info ( )
217
241
218
- { lang , lexer , lexer_opts } ->
219
- try do
220
- Makeup . highlight_inner_html ( code ,
221
- lexer: lexer ,
222
- lexer_options: lexer_opts ,
223
- formatter_options: opts
224
- )
225
- rescue
226
- exception ->
227
- ExDoc.Utils . warn (
228
- [
229
- "crashed while highlighting #{ lang } snippet:\n \n " ,
230
- ExDoc.DocAST . to_string ( ast ) ,
231
- "\n \n " ,
232
- Exception . format_banner ( :error , exception , __STACKTRACE__ )
233
- ] ,
234
- __STACKTRACE__
235
- )
242
+ map_tags ( ast , fn
243
+ { :pre , pre_attrs , [ { :code , code_attrs , [ code ] , code_meta } ] , pre_meta } = ast
244
+ when is_binary ( code ) ->
245
+ { lang , code_attrs } = Keyword . pop ( code_attrs , :class , "" )
236
246
247
+ case pick_language_and_lexer ( lang , highlight_info , code ) do
248
+ { _lang , nil , _lexer_opts } ->
237
249
ast
238
- else
239
- highlighted ->
240
- code_attrs = [ class: "makeup #{ lang } " , translate: "no" ] ++ code_attrs
241
- code_meta = Map . put ( code_meta , :verbatim , true )
242
- { :pre , pre_attrs , [ { :code , code_attrs , [ highlighted ] , code_meta } ] , pre_meta }
243
- end
244
- end
245
- end
246
250
247
- defp do_highlight ( list , highlight_info , opts ) when is_list ( list ) do
248
- Enum . map ( list , & do_highlight ( & 1 , highlight_info , opts ) )
249
- end
251
+ { lang , lexer , lexer_opts } ->
252
+ try do
253
+ Makeup . highlight_inner_html ( code ,
254
+ lexer: lexer ,
255
+ lexer_options: lexer_opts ,
256
+ formatter_options: opts
257
+ )
258
+ rescue
259
+ exception ->
260
+ ExDoc.Utils . warn (
261
+ [
262
+ "crashed while highlighting #{ lang } snippet:\n \n " ,
263
+ ExDoc.DocAST . to_string ( ast ) ,
264
+ "\n \n " ,
265
+ Exception . format_banner ( :error , exception , __STACKTRACE__ )
266
+ ] ,
267
+ __STACKTRACE__
268
+ )
269
+
270
+ ast
271
+ else
272
+ highlighted ->
273
+ code_attrs = [ class: "makeup #{ lang } " , translate: "no" ] ++ code_attrs
274
+ code_meta = Map . put ( code_meta , :verbatim , true )
275
+ { :pre , pre_attrs , [ { :code , code_attrs , [ highlighted ] , code_meta } ] , pre_meta }
276
+ end
277
+ end
250
278
251
- defp do_highlight ( other , _highlight_info , _opts ) do
252
- other
279
+ ast ->
280
+ ast
281
+ end )
253
282
end
254
283
255
284
defp pick_language_and_lexer ( "" , _highlight_info , "$ " <> _ ) do
@@ -270,4 +299,44 @@ defmodule ExDoc.DocAST do
270
299
:error -> { lang , nil , [ ] }
271
300
end
272
301
end
302
+
303
+ ## Traversal helpers
304
+
305
+ @ doc """
306
+ Maps the tags in the AST, first mapping children tags, then the tag itself.
307
+ """
308
+ def map_tags ( { tag , attrs , inner , meta } , fun ) ,
309
+ do: fun . ( { tag , attrs , Enum . map ( inner , & map_tags ( & 1 , fun ) ) , meta } )
310
+
311
+ def map_tags ( list , fun ) when is_list ( list ) ,
312
+ do: Enum . map ( list , & map_tags ( & 1 , fun ) )
313
+
314
+ def map_tags ( other , _fun ) ,
315
+ do: other
316
+
317
+ @ doc """
318
+ Reduces the tags in the AST, first reducing children tags, then the tag itself.
319
+ """
320
+ def reduce_tags ( { tag , attrs , inner , meta } , acc , fun ) ,
321
+ do: fun . ( { tag , attrs , inner , meta } , Enum . reduce ( inner , acc , & reduce_tags ( & 1 , & 2 , fun ) ) )
322
+
323
+ def reduce_tags ( list , acc , fun ) when is_list ( list ) ,
324
+ do: Enum . reduce ( list , acc , & reduce_tags ( & 1 , & 2 , fun ) )
325
+
326
+ def reduce_tags ( _other , acc , _fun ) ,
327
+ do: acc
328
+
329
+ @ doc """
330
+ Map-reduces the tags in the AST, first mapping children tags, then the tag itself.
331
+ """
332
+ def map_reduce_tags ( { tag , attrs , inner , meta } , acc , fun ) do
333
+ { inner , acc } = Enum . map_reduce ( inner , acc , & map_reduce_tags ( & 1 , & 2 , fun ) )
334
+ fun . ( { tag , attrs , inner , meta } , acc )
335
+ end
336
+
337
+ def map_reduce_tags ( list , acc , fun ) when is_list ( list ) ,
338
+ do: Enum . map_reduce ( list , acc , & map_reduce_tags ( & 1 , & 2 , fun ) )
339
+
340
+ def map_reduce_tags ( other , acc , _fun ) ,
341
+ do: { other , acc }
273
342
end
0 commit comments