├── config
├── config_gfm
├── template.html
├── license.txt
├── readme.md
└── ngx_markdown_filter_module.c
/config:
--------------------------------------------------------------------------------
1 | ngx_module_type=HTTP_FILTER
2 | ngx_module_name=ngx_markdown_filter_module
3 | ngx_module_srcs="$ngx_addon_dir/ngx_markdown_filter_module.c"
4 | ngx_module_libs=-lcmark
5 |
6 | . auto/module
7 |
8 | ngx_addon_name=$ngx_module_name
--------------------------------------------------------------------------------
/config_gfm:
--------------------------------------------------------------------------------
1 | ngx_module_type=HTTP_FILTER
2 | ngx_module_name=ngx_markdown_filter_module
3 | ngx_module_srcs="$ngx_addon_dir/ngx_markdown_filter_module.c"
4 | ngx_module_libs="-lcmark-gfm -lcmark-gfm-extensions"
5 |
6 | . auto/module
7 |
8 | ngx_addon_name=$ngx_module_name
--------------------------------------------------------------------------------
/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
18 |
19 |
20 | {{content}}
21 |
22 |
23 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Karim Ulzhabayev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## ngx_markdown_filter_module
2 |
3 | The `ngx_markdown_filter_module` module is a filter that transforms markdown files to html format.
4 |
5 | This module utilizes the [cmark](https://github.com/commonmark/cmark) library.
6 |
7 | ### Example configuration
8 |
9 | ```
10 | location ~ \.md {
11 | markdown_filter on;
12 | markdown_template html/template.html;
13 | }
14 | ```
15 |
16 | This works on proxy locations as well.
17 |
18 | ### Directives
19 |
20 | ```
21 | Syntax: markdown_filter on|off;
22 | Context: location
23 | ```
24 |
25 | ```
26 | Syntax: markdown_template html/template.html;
27 | Context: location
28 | ```
29 |
30 | ```
31 | # enable `unsafe` mode for cmark
32 | Syntax: markdown_unsafe on|off;
33 | Context: location;
34 | ```
35 |
36 | ```
37 | # enable `tagfilter` extension for cmark-gfm
38 | Syntax: markdown_gfm_tagfilter on|off;
39 | Context: location;
40 | ```
41 |
42 | ```
43 | # enable `tasklist` extension for cmark-gfm
44 | Syntax: markdown_gfm_tasklist on|off;
45 | Context: location;
46 | ```
47 |
48 | ```
49 | # enable `strikethrough` extension for cmark-gfm
50 | Syntax: markdown_gfm_strikethrough on|off;
51 | Context: location;
52 | ```
53 |
54 | ```
55 | # enable `autolink` extension for cmark-gfm
56 | Syntax: markdown_gfm_autolink on|off;
57 | Context: location;
58 | ```
59 |
60 | ### Build
61 |
62 | 1. Clone this repo
63 |
64 | 2. Install `cmark` lib with development headers
65 |
66 | ```
67 | dnf install cmark-devel
68 | ```
69 |
70 | 3. Download [nginx src archive](http://nginx.org/en/download.html) and unpack it
71 |
72 | 4. Run `configure` script (see nginx src) and build nginx
73 |
74 | ```
75 | > ./configure --add-module=/path/to/ngx_markdown_filter_module
76 | > make
77 | ```
78 |
79 | 5. Apply markdown directives to nginx conf and run it
80 |
81 | ### Build with cmark-gfm (tables support)
82 |
83 | Original cmark library doesn't support tables. But there is [cmark-gfm](https://github.com/github/cmark-gfm)
84 | fork with table extension, supported by Github.
85 |
86 | 1. Clone this repo
87 |
88 | 2. Rename `config_gfm` to `config`
89 |
90 | 3. Install `cmark-gfm` lib
91 |
92 | 4. Download [nginx src archive](http://nginx.org/en/download.html) and unpack it
93 |
94 | 5. Run `configure` script (see nginx src) and build nginx
95 |
96 | ```
97 | > ./configure --add-module=/path/to/ngx_markdown_filter_module --with-cc-opt=-DWITH_CMARK_GFM=1
98 | > make
99 | ```
100 |
101 | 6. Apply markdown directives to nginx conf and run it
102 |
--------------------------------------------------------------------------------
/ngx_markdown_filter_module.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #ifdef WITH_CMARK_GFM
6 | #include
7 | #include
8 | #include
9 | #else
10 | #include
11 | #endif
12 |
13 |
14 | // pointers to next handlers
15 |
16 | static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
17 |
18 | static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
19 |
20 |
21 | // location conf
22 |
23 | typedef struct {
24 | ngx_flag_t enable;
25 | u_char *header;
26 | u_char *footer;
27 | ngx_int_t header_len;
28 | ngx_int_t footer_len;
29 | ngx_flag_t unsafe;
30 | ngx_flag_t gfm_tagfilter_enabled;
31 | ngx_flag_t gfm_tasklist_enabled;
32 | ngx_flag_t gfm_strikethrough_enabled;
33 | ngx_flag_t gfm_autolink_enabled;
34 | } ngx_markdown_filter_conf_t;
35 |
36 |
37 | // request context
38 |
39 | typedef struct {
40 | cmark_parser *parser;
41 | cmark_llist *extensions;
42 | } ngx_markdown_filter_ctx_t;
43 |
44 |
45 | static void *ngx_markdown_filter_create_conf(ngx_conf_t *cf);
46 |
47 | static char *ngx_markdown_filter_merge_conf(ngx_conf_t *cf, void *parent, void *child);
48 |
49 | static ngx_int_t ngx_markdown_header_filter(ngx_http_request_t *r);
50 |
51 | static ngx_int_t ngx_markdown_body_filter(ngx_http_request_t *r, ngx_chain_t *chain);
52 |
53 | static ngx_int_t ngx_markdown_filter_init(ngx_conf_t *cf);
54 |
55 | static void cmark_parser_cleanup(void *parser);
56 |
57 | static void cmark_extensions_cleanup(void *data);
58 |
59 | static char *ngx_conf_set_template(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
60 |
61 | /* module directives */
62 |
63 | static ngx_command_t ngx_markdown_filter_commands[] = {
64 |
65 | { ngx_string("markdown_filter"),
66 | NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
67 | ngx_conf_set_flag_slot,
68 | NGX_HTTP_LOC_CONF_OFFSET,
69 | offsetof(ngx_markdown_filter_conf_t, enable),
70 | NULL },
71 |
72 | { ngx_string("markdown_template"),
73 | NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
74 | ngx_conf_set_template,
75 | NGX_HTTP_LOC_CONF_OFFSET,
76 | 0, // unused
77 | NULL },
78 |
79 | { ngx_string("markdown_unsafe"),
80 | NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
81 | ngx_conf_set_flag_slot,
82 | NGX_HTTP_LOC_CONF_OFFSET,
83 | offsetof(ngx_markdown_filter_conf_t, unsafe),
84 | NULL },
85 |
86 | { ngx_string("markdown_gfm_tagfilter"),
87 | NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
88 | ngx_conf_set_flag_slot,
89 | NGX_HTTP_LOC_CONF_OFFSET,
90 | offsetof(ngx_markdown_filter_conf_t, gfm_tagfilter_enabled),
91 | NULL },
92 |
93 | { ngx_string("markdown_gfm_tasklist"),
94 | NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
95 | ngx_conf_set_flag_slot,
96 | NGX_HTTP_LOC_CONF_OFFSET,
97 | offsetof(ngx_markdown_filter_conf_t, gfm_tasklist_enabled),
98 | NULL },
99 |
100 | { ngx_string("markdown_gfm_strikethrough"),
101 | NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
102 | ngx_conf_set_flag_slot,
103 | NGX_HTTP_LOC_CONF_OFFSET,
104 | offsetof(ngx_markdown_filter_conf_t, gfm_strikethrough_enabled),
105 | NULL },
106 |
107 | { ngx_string("markdown_gfm_autolink"),
108 | NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
109 | ngx_conf_set_flag_slot,
110 | NGX_HTTP_LOC_CONF_OFFSET,
111 | offsetof(ngx_markdown_filter_conf_t, gfm_autolink_enabled),
112 | NULL },
113 |
114 | ngx_null_command
115 | };
116 |
117 |
118 | /* module context */
119 |
120 | static ngx_http_module_t ngx_markdown_filter_module_ctx = {
121 | NULL, /* preconfiguration */
122 | ngx_markdown_filter_init, /* postconfiguration */
123 |
124 | NULL, /* create main configuration */
125 | NULL, /* init main configuration */
126 |
127 | NULL, /* create server configuration */
128 | NULL, /* merge server configuration */
129 |
130 | ngx_markdown_filter_create_conf, /* create location configuration */
131 | ngx_markdown_filter_merge_conf /* merge location configuration */
132 | };
133 |
134 |
135 | /* module itself */
136 |
137 | ngx_module_t ngx_markdown_filter_module = {
138 | NGX_MODULE_V1,
139 | &ngx_markdown_filter_module_ctx, /* module context */
140 | ngx_markdown_filter_commands, /* module directives */
141 | NGX_HTTP_MODULE, /* module type */
142 | NULL, /* init master */
143 | NULL, /* init module */
144 | NULL, /* init process */
145 | NULL, /* init thread */
146 | NULL, /* exit thread */
147 | NULL, /* exit process */
148 | NULL, /* exit master */
149 | NGX_MODULE_V1_PADDING
150 | };
151 |
152 |
153 | static char *ngx_conf_set_template(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
154 | {
155 | ngx_str_t *value = cf->args->elts;
156 | ngx_str_t filename = value[1];
157 | ngx_markdown_filter_conf_t *markdown_conf = (ngx_markdown_filter_conf_t *) conf;
158 |
159 | ngx_fd_t fd = ngx_open_file(filename.data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
160 | if (fd == NGX_INVALID_FILE) {
161 | ngx_conf_log_error(NGX_LOG_ERR, cf, 0, "cannot open template file %s", filename.data);
162 | return NGX_CONF_ERROR;
163 | }
164 |
165 | ngx_file_info_t fi;
166 | if (ngx_fd_info(fd, &fi) == NGX_FILE_ERROR){
167 | ngx_close_file(fd);
168 | ngx_conf_log_error(NGX_LOG_ERR, cf, 0, "cannot get stats for template file %s", filename.data);
169 | return NGX_CONF_ERROR;
170 | }
171 |
172 | u_char *template = ngx_calloc(fi.st_size + 1, cf->log);
173 | if (template == NULL) {
174 | ngx_close_file(fd);
175 | ngx_conf_log_error(NGX_LOG_ERR, cf, 0, "cannot allocate memory for template content");
176 | return NGX_CONF_ERROR;
177 | }
178 |
179 | ngx_file_t file;
180 | file.fd = fd;
181 | file.info = fi;
182 | file.log = cf->log;
183 |
184 | ngx_int_t n = ngx_read_file(&file, template, fi.st_size, 0);
185 | template[fi.st_size] = '\0';
186 |
187 | ngx_close_file(fd);
188 |
189 | for (ngx_int_t i = 0; i < n; i++) {
190 | if (template[i] == '{' && template[i+1] == '{') {
191 | template[i] = '\0';
192 | markdown_conf->header = template;
193 | markdown_conf->header_len = ngx_strlen(template);
194 | continue;
195 | }
196 | if (template[i] == '}' && template[i+1] == '}') {
197 | markdown_conf->footer = template + (i+2); // Note!! pointer arithmetic
198 | markdown_conf->footer_len = ngx_strlen(markdown_conf->footer);
199 | break;
200 | }
201 | }
202 |
203 | return NGX_CONF_OK;
204 | }
205 |
206 |
207 | static void *ngx_markdown_filter_create_conf(ngx_conf_t *cf)
208 | {
209 | ngx_markdown_filter_conf_t *conf = ngx_pcalloc(cf->pool, sizeof(ngx_markdown_filter_conf_t));
210 | if (conf == NULL) {
211 | return NULL;
212 | }
213 | conf->enable = NGX_CONF_UNSET;
214 | conf->header = NGX_CONF_UNSET_PTR;
215 | conf->footer = NGX_CONF_UNSET_PTR;
216 | conf->header_len = NGX_CONF_UNSET;
217 | conf->footer_len = NGX_CONF_UNSET;
218 | conf->unsafe = NGX_CONF_UNSET;
219 | conf->gfm_tagfilter_enabled = NGX_CONF_UNSET;
220 | conf->gfm_tasklist_enabled = NGX_CONF_UNSET;
221 | conf->gfm_strikethrough_enabled = NGX_CONF_UNSET;
222 | conf->gfm_autolink_enabled = NGX_CONF_UNSET;
223 | return conf;
224 | }
225 |
226 |
227 | static char *ngx_markdown_filter_merge_conf(ngx_conf_t *cf, void *parent, void *child)
228 | {
229 | ngx_markdown_filter_conf_t *prev = parent;
230 | ngx_markdown_filter_conf_t *conf = child;
231 | ngx_conf_merge_value(conf->enable, prev->enable, 0);
232 | ngx_conf_merge_ptr_value(conf->header, prev->header, NULL);
233 | ngx_conf_merge_ptr_value(conf->footer, prev->footer, NULL);
234 | ngx_conf_merge_value(conf->header_len, prev->header_len, 0);
235 | ngx_conf_merge_value(conf->footer_len, prev->footer_len, 0);
236 | ngx_conf_merge_value(conf->unsafe, prev->unsafe, 0);
237 | ngx_conf_merge_value(conf->gfm_tagfilter_enabled, prev->gfm_tagfilter_enabled, 0);
238 | ngx_conf_merge_value(conf->gfm_tasklist_enabled, prev->gfm_tasklist_enabled, 0);
239 | ngx_conf_merge_value(conf->gfm_strikethrough_enabled, prev->gfm_strikethrough_enabled, 0);
240 | ngx_conf_merge_value(conf->gfm_autolink_enabled, prev->gfm_autolink_enabled, 0);
241 | return NGX_CONF_OK;
242 | }
243 |
244 |
245 | static ngx_int_t ngx_markdown_filter_init(ngx_conf_t *cf)
246 | {
247 |
248 | ngx_http_next_header_filter = ngx_http_top_header_filter;
249 | ngx_http_top_header_filter = ngx_markdown_header_filter;
250 |
251 | ngx_http_next_body_filter = ngx_http_top_body_filter;
252 | ngx_http_top_body_filter = ngx_markdown_body_filter;
253 |
254 | return NGX_OK;
255 | }
256 |
257 |
258 | static void cmark_parser_cleanup(void *data)
259 | {
260 | if (data == NULL) {
261 | return;
262 | }
263 | cmark_parser *parser = data;
264 | cmark_parser_free(parser);
265 | }
266 |
267 | static void cmark_extensions_cleanup(void *data)
268 | {
269 | if (data == NULL) {
270 | return;
271 | }
272 | cmark_llist *extensions = data;
273 | cmark_llist_free(cmark_get_default_mem_allocator(), extensions);
274 | }
275 |
276 | static ngx_int_t ngx_markdown_header_filter(ngx_http_request_t *r)
277 | {
278 | ngx_markdown_filter_conf_t *lc = ngx_http_get_module_loc_conf(r, ngx_markdown_filter_module);
279 | if (lc->enable && r->headers_out.status == NGX_HTTP_OK) {
280 | ngx_markdown_filter_ctx_t *ctx = ngx_pcalloc(r->pool, sizeof(ngx_markdown_filter_ctx_t));
281 | if (ctx == NULL) {
282 | return NGX_ERROR;
283 | }
284 | int cmark_opts = lc->unsafe ? CMARK_OPT_UNSAFE : CMARK_OPT_DEFAULT;
285 | cmark_parser *parser = cmark_parser_new(cmark_opts);
286 | if (parser == NULL) {
287 | return NGX_ERROR;
288 | }
289 |
290 | cmark_llist *extensions = NULL;
291 |
292 | #ifdef WITH_CMARK_GFM
293 | cmark_gfm_core_extensions_ensure_registered();
294 | cmark_syntax_extension *ext_table = cmark_find_syntax_extension("table");
295 | if (ext_table != NULL) {
296 | cmark_parser_attach_syntax_extension(parser, ext_table);
297 | }
298 |
299 | if (lc->gfm_tagfilter_enabled) {
300 | cmark_syntax_extension *ext_tagfilter = cmark_find_syntax_extension("tagfilter");
301 | if (ext_tagfilter != NULL) {
302 | extensions = cmark_llist_append(cmark_get_default_mem_allocator(), extensions, ext_tagfilter);
303 | cmark_parser_attach_syntax_extension(parser, ext_tagfilter);
304 | }
305 | }
306 |
307 | if (lc->gfm_tasklist_enabled) {
308 | cmark_syntax_extension *ext_tasklist = cmark_find_syntax_extension("tasklist");
309 | if (ext_tasklist != NULL) {
310 | extensions = cmark_llist_append(cmark_get_default_mem_allocator(), extensions, ext_tasklist);
311 | cmark_parser_attach_syntax_extension(parser, ext_tasklist);
312 | }
313 | }
314 |
315 | if (lc->gfm_strikethrough_enabled) {
316 | cmark_syntax_extension *ext_strikethrough = cmark_find_syntax_extension("strikethrough");
317 | if (ext_strikethrough != NULL) {
318 | extensions = cmark_llist_append(cmark_get_default_mem_allocator(), extensions, ext_strikethrough);
319 | cmark_parser_attach_syntax_extension(parser, ext_strikethrough);
320 | }
321 | }
322 |
323 | if (lc->gfm_autolink_enabled) {
324 | cmark_syntax_extension *ext_autolink = cmark_find_syntax_extension("autolink");
325 | if (ext_autolink != NULL) {
326 | extensions = cmark_llist_append(cmark_get_default_mem_allocator(), extensions, ext_autolink);
327 | cmark_parser_attach_syntax_extension(parser, ext_autolink);
328 | }
329 | }
330 | #endif
331 |
332 | ctx->parser = parser;
333 | ctx->extensions = extensions;
334 |
335 | ngx_pool_cleanup_t *cln_parser = ngx_pool_cleanup_add(r->pool, 0);
336 | if (cln_parser == NULL) {
337 | cmark_parser_cleanup(parser);
338 | return NGX_ERROR;
339 | }
340 | cln_parser->handler = cmark_parser_cleanup;
341 | cln_parser->data = parser;
342 |
343 | if (extensions != NULL) {
344 | ngx_pool_cleanup_t *cln_extensions = ngx_pool_cleanup_add(r->pool, 0);
345 | if (cln_extensions == NULL) {
346 | cmark_extensions_cleanup(extensions);
347 | return NGX_ERROR;
348 | }
349 | cln_extensions->handler = cmark_extensions_cleanup;
350 | cln_extensions->data = extensions;
351 | }
352 |
353 | ngx_http_set_ctx(r, ctx, ngx_markdown_filter_module);
354 |
355 | ngx_str_t mime = ngx_string("text/html;charset=utf-8");
356 | r->headers_out.content_type = mime;
357 | r->main_filter_need_in_memory = 1;
358 | ngx_http_clear_content_length(r);
359 | }
360 | return ngx_http_next_header_filter(r);
361 | }
362 |
363 |
364 | static ngx_int_t ngx_markdown_body_filter(ngx_http_request_t *r, ngx_chain_t *chain)
365 | {
366 | if (chain == NULL) {
367 | return ngx_http_next_body_filter(r, chain);
368 | }
369 |
370 | if (r->headers_out.status != NGX_HTTP_OK) {
371 | return ngx_http_next_body_filter(r, chain);
372 | }
373 |
374 | ngx_markdown_filter_conf_t *lc = ngx_http_get_module_loc_conf(r, ngx_markdown_filter_module);
375 | if (!(lc->enable)) {
376 | return ngx_http_next_body_filter(r, chain);
377 | }
378 |
379 | ngx_markdown_filter_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_markdown_filter_module);
380 | if (ctx == NULL) {
381 | return NGX_ERROR;
382 | }
383 |
384 | cmark_parser *parser = ctx->parser;
385 | if (parser == NULL) {
386 | return NGX_ERROR;
387 | }
388 |
389 | cmark_llist *extensions = ctx->extensions;
390 |
391 | int last = 0;
392 | for (ngx_chain_t *cl = chain; cl; cl = cl->next) {
393 | ngx_buf_t *buf = cl->buf;
394 |
395 | cmark_parser_feed(parser, (char *)(buf->pos), ngx_buf_size(buf));
396 |
397 | buf->pos = buf->last;
398 | buf->flush = 0;
399 |
400 | if (buf->last_buf) {
401 | last = 1;
402 | }
403 | }
404 | if (last) {
405 | cmark_node *root = cmark_parser_finish(parser);
406 | int cmark_opts = lc->unsafe ? CMARK_OPT_UNSAFE : CMARK_OPT_DEFAULT;
407 |
408 | #ifdef WITH_CMARK_GFM
409 | char *html = cmark_render_html(root, cmark_opts, extensions);
410 | #else
411 | char *html = cmark_render_html(root, cmark_opts);
412 | #endif
413 |
414 | cmark_node_free(root); // remove document tree
415 |
416 | if (html == NULL) {
417 | return NGX_ERROR;
418 | }
419 |
420 | ngx_pool_cleanup_t *cln = ngx_pool_cleanup_add(r->pool, 0);
421 | if (cln == NULL) {
422 | ngx_free(html);
423 | return NGX_ERROR;
424 | }
425 |
426 | cln->handler = ngx_free;
427 | cln->data = html;
428 |
429 | ngx_chain_t *out_chain = NULL;
430 |
431 | // add header
432 |
433 | if (lc->header != NULL) {
434 | out_chain = ngx_alloc_chain_link(r->pool);
435 | if (out_chain == NULL) {
436 | return NGX_ERROR;
437 | }
438 | ngx_buf_t *header_buf = ngx_calloc_buf(r->pool);
439 | if (header_buf == NULL) {
440 | return NGX_ERROR;
441 | }
442 | header_buf->pos = lc->header;
443 | header_buf->last = header_buf->pos + lc->header_len;
444 | header_buf->memory = 1; // Set readonly flag, and do not create copy of lc->header
445 | header_buf->last_buf = 0;
446 | header_buf->last_in_chain = 0;
447 |
448 | out_chain->buf = header_buf;
449 | out_chain->next = NULL;
450 | }
451 |
452 | int footer_missing = lc->footer == NULL ? 1 : 0;
453 |
454 | // add markdown content
455 |
456 | ngx_chain_t *content_chain = ngx_alloc_chain_link(r->pool);
457 | if (content_chain == NULL) {
458 | return NGX_ERROR;
459 | }
460 |
461 | ngx_buf_t *content_buf = ngx_calloc_buf(r->pool);
462 | if (content_buf == NULL) {
463 | return NGX_ERROR;
464 | }
465 | content_buf->pos = (u_char *) html;
466 | content_buf->last = content_buf->pos + strlen(html);
467 | content_buf->memory = 1;
468 | content_buf->last_buf = footer_missing;
469 | content_buf->last_in_chain = footer_missing;
470 |
471 | content_chain->buf = content_buf;
472 | content_chain->next = NULL;
473 |
474 | if (out_chain == NULL) {
475 | out_chain = content_chain;
476 | } else {
477 | out_chain->next = content_chain;
478 | }
479 |
480 | // add footer
481 |
482 | if (!footer_missing) {
483 | ngx_chain_t *footer_chain = ngx_alloc_chain_link(r->pool);
484 | if (footer_chain == NULL) {
485 | return NGX_ERROR;
486 | }
487 | ngx_buf_t *footer_buf = ngx_calloc_buf(r->pool);
488 | if (footer_buf == NULL) {
489 | return NGX_ERROR;
490 | }
491 | footer_buf->pos = lc->footer;
492 | footer_buf->last = footer_buf->pos + lc->footer_len;
493 | footer_buf->memory = 1; // Set readonly flag, and do not create copy of lc->footer
494 | footer_buf->last_buf = 1;
495 | footer_buf->last_in_chain = 1;
496 |
497 | footer_chain->buf = footer_buf;
498 | footer_chain->next = NULL;
499 |
500 | content_chain->next = footer_chain;
501 | }
502 |
503 | return ngx_http_next_body_filter(r, out_chain);
504 | }
505 |
506 | return NGX_OK;
507 | }
508 |
--------------------------------------------------------------------------------