├── LICENSE
├── README.md
├── markdown_2_html.pkb
├── markdown_2_html.pks
└── setup
└── tables.txt
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Christina Moore
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 | # plsql-markdown-2-html
2 | This PL/SQL package includes functions that convert Markdown to HTML. The markdown text is presented via CLOB/VARCHAR2 and return in the same datatype. I just could not find a PL/SQL package that addressed this issue for me.
3 |
4 | My vision for this tool is use in Oracle APEX and letting application users create relatively straight forward HTML pages. I also made the assumption that I'd be happy with the 32K text limit allow the tool to use the APEX Utility for reading text (apex_util.string_to_table). It might be fair to expand the package to includes that exceed 32K.
5 |
6 | ## Limitations
7 | ### CLOB Size < 32K
8 | CLOB that exceed 32K will not be processed unless we (collectively) write include a CLOB parser.
9 |
10 | ### Lists not nested
11 | UL/OL lists are treated as flat. It is not possible with this code to nest lists.
12 |
13 | ### Issue with underscore in link URL
14 | There maybe an issue with underscores inside a link's URL.
15 |
16 | ### Headers Definitions
17 | I did not include the alternate underline-ish means for defining headers.
18 | ```
19 | These styles are NOT included:
20 |
21 | Alt-H1
22 | ======
23 |
24 | Alt-H2
25 | ------
26 | ```
27 |
28 | ## Variations
29 | In the code there are some variations for accommodating newline codes (cr, crlf). With a quick code change, or a new parameter, you can opt to return text with HTML breaks or cr or crlf.
30 |
31 | # Markdown Guidance
32 | I used Adam Pritchard's Markdown guidance as a template for the Markdown to work from. There are some variations.
33 | A link to his document is [here](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
34 |
35 | A tip-of-the-hat to [Johnny Broadway](https://gist.github.com/jbroadway/2836900#file-slimdown-php) for some helpful suggestions on `regexp` for markdown. Thank you Johnny.
36 |
37 | # Documentation
38 | To play, I created a simple table with a primary key and a clob field. I tossed my markdown text into the clob with SQL developer and run the code. I've included a sample of the calling code in the package. The functions are overloaded to accommodate both clob and varchar2.
39 |
40 | As a coder, I am far happier writing PL/SQL then anything in regexp. I can document and read PL/SQL phrases. A reader may notice a blending of approaches. I would rather write more and get clarity and simplify debugging then have code that is so dense you can't see errors.
41 |
--------------------------------------------------------------------------------
/markdown_2_html.pkb:
--------------------------------------------------------------------------------
1 | create or replace package body markdown_2_html
2 | as
3 | cr constant varchar2(1) := chr(10);
4 | crlf constant varchar2(2) := chr(13) || chr(10) ;
5 | nl constant varchar2(4) := '/n';
6 | -- calling code is below
7 | /*
8 | declare
9 | l_clob clob;
10 | begin
11 | for c in (
12 | select
13 | markdown_pk,
14 | markdown_clob
15 | from mrk_clob
16 | where markdown_pk = 1
17 | ) loop
18 | l_clob := markdown_2_html.md_2_html(c.markdown_clob);
19 | end loop; -- clob loop
20 | dbms_output.put_line(l_clob);
21 | end;
22 | */
23 | function h (
24 | P_TEXT in varchar2
25 | ) return varchar2
26 | -------------------------------------------------------------------------------
27 | -- FUNCTION H
28 | -- Prepares HTML headers from H1 to H6.
29 | --
30 | -- Written 25APR2017
31 | -- cmoore
32 | --
33 | -- Modifications:
34 | --
35 | --
36 | --
37 | -------------------------------------------------------------------------------
38 | as
39 | l_count number;
40 | l_return varchar2(2000);
41 | begin
42 | l_count := regexp_count(P_TEXT,'#');
43 | l_return := trim(regexp_replace(P_TEXT,'#',''));
44 | if l_count between 1 and 6 then
45 | l_return := '' ||
46 | l_return ||
47 | '';
48 | else
49 | l_return := '
' || l_return || '/h1>';
50 | end if;
51 | return l_return;
52 | end h;
53 |
54 | function crlf_2_br (
55 | P_TEXT in varchar2
56 | ) return varchar2
57 | -------------------------------------------------------------------------------
58 | -- FUNCTION crlf_2_br
59 | -- converts pairs of end-of-line to
within text
60 | -- function is overloaded to accommodate varchar2 and clob.
61 | -- Looks for both cr and crlf
62 | --
63 | -- Written 24APR2017
64 | -- cmoore
65 | --
66 | -- Modifications:
67 | --
68 | --
69 | --
70 | -------------------------------------------------------------------------------
71 | as
72 | l_position number;
73 | l_return varchar2(4000);
74 | begin
75 | if instr(P_TEXT, cr || cr) > 0 then
76 | l_return := replace(P_TEXT, cr || cr, '
');
77 | end if;
78 | if instr(P_TEXT, crlf || crlf) > 0 then
79 | l_return := replace(P_TEXT, crlf || crlf, '
');
80 | end if;
81 | return l_return;
82 | end crlf_2_br;
83 |
84 | function crlf_2_br (
85 | P_TEXT in clob
86 | ) return clob
87 | -------------------------------------------------------------------------------
88 | -- FUNCTION crlf_2_br
89 | -- converts pairs of end-of-line to
within text
90 | -- function is overloaded to accommodate varchar2 and clob.
91 | -- Looks for both cr and crlf
92 | --
93 | -- Written 24APR2017
94 | -- cmoore
95 | --
96 | -- Modifications:
97 | --
98 | --
99 | --
100 | -------------------------------------------------------------------------------
101 | as
102 | l_position number;
103 | l_return clob;
104 | begin
105 | if instr(P_TEXT, cr || cr) > 0 then
106 | l_return := replace(P_TEXT, cr || cr, '
');
107 | end if;
108 | if instr(P_TEXT, crlf || crlf) > 0 then
109 | l_return := replace(P_TEXT, crlf || crlf, '
');
110 | end if;
111 | return l_return;
112 | end crlf_2_br;
113 |
114 | function crlf_2_nl (
115 | P_TEXT in varchar2
116 | ) return varchar2
117 | -------------------------------------------------------------------------------
118 | -- FUNCTION crlf_2_nl
119 | -- converts pairs of end-of-line to newline (global constant) within text
120 | -- function is overloaded to accommodate varchar2 and clob.
121 | -- Looks for both cr and crlf
122 | --
123 | -- Written 24APR2017
124 | -- cmoore
125 | --
126 | -- Modifications:
127 | --
128 | --
129 | --
130 | -------------------------------------------------------------------------------
131 | as
132 | l_position number;
133 | l_return varchar2(4000);
134 | begin
135 | if instr(P_TEXT, cr) > 0 then
136 | l_return := replace(P_TEXT, cr, nl);
137 | end if;
138 | if instr(P_TEXT, crlf) > 0 then
139 | l_return := replace(P_TEXT, crlf, nl);
140 | end if;
141 | return l_return;
142 | end crlf_2_nl;
143 |
144 | function crlf_2_nl (
145 | P_TEXT in clob
146 | ) return clob
147 | -------------------------------------------------------------------------------
148 | -- FUNCTION crlf_2_nl
149 | -- converts pair end-of-line to newline (global constant) within text
150 | -- function is overloaded to accommodate varchar2 and clob.
151 | -- Looks for both cr and crlf
152 | --
153 | -- Written 24APR2017
154 | -- cmoore
155 | --
156 | -- Modifications:
157 | --
158 | --
159 | --
160 | -------------------------------------------------------------------------------
161 | as
162 | l_position number;
163 | l_return clob;
164 | begin
165 | if instr(P_TEXT, cr) > 0 then
166 | l_return := replace(P_TEXT, cr, nl);
167 | end if;
168 | if instr(P_TEXT, crlf) > 0 then
169 | l_return := replace(P_TEXT, crlf, nl);
170 | end if;
171 | return l_return;
172 | end crlf_2_nl;
173 |
174 | function nl_2_crlf (
175 | P_TEXT in clob
176 | ) return clob
177 | -------------------------------------------------------------------------------
178 | -- FUNCTION nl_2_crlf
179 | -- converts end-of-line to newline (global constant) within text
180 | -- function is overloaded to accommodate varchar2 and clob.
181 | --
182 | -- Written 24APR2017
183 | -- cmoore
184 | --
185 | -- Modifications:
186 | --
187 | --
188 | --
189 | -------------------------------------------------------------------------------
190 | as
191 | l_position number;
192 | l_return clob;
193 | begin
194 | if instr(P_TEXT, nl) > 0 then
195 | l_return := replace(P_TEXT, nl, crlf);
196 | end if;
197 | return l_return;
198 | end nl_2_crlf;
199 |
200 | function nl_2_crlf (
201 | P_TEXT in varchar2
202 | ) return varchar2
203 | -------------------------------------------------------------------------------
204 | -- FUNCTION nl_2_crlf
205 | -- converts end-of-line to newline (global constant) within text
206 | -- function is overloaded to accommodate varchar2 and clob.
207 | --
208 | -- Written 24APR2017
209 | -- cmoore
210 | --
211 | -- Modifications:
212 | --
213 | --
214 | --
215 | -------------------------------------------------------------------------------
216 | as
217 | l_position number;
218 | l_return varchar2(4000);
219 | begin
220 | if instr(P_TEXT, nl) > 0 then
221 | l_return := replace(P_TEXT, nl, crlf);
222 | end if;
223 | return l_return;
224 | end nl_2_crlf;
225 |
226 | function nl_2_br (
227 | P_TEXT in clob
228 | ) return clob
229 | -------------------------------------------------------------------------------
230 | -- FUNCTION nl_2_crlf
231 | -- converts newline (global constant) to
within text
232 | -- function is overloaded to accommodate varchar2 and clob.
233 | --
234 | -- Written 24APR2017
235 | -- cmoore
236 | --
237 | -- Modifications:
238 | --
239 | --
240 | --
241 | -------------------------------------------------------------------------------
242 | as
243 | l_position number;
244 | l_return clob;
245 | begin
246 | if instr(P_TEXT, nl) > 0 then
247 | l_return := replace(P_TEXT, nl, '
');
248 | end if;
249 | return l_return;
250 | end nl_2_br;
251 |
252 | function nl_2_br (
253 | P_TEXT in varchar2
254 | ) return varchar2
255 | -------------------------------------------------------------------------------
256 | -- FUNCTION nl_2_crlf
257 | -- converts newline (global constant) to
within text
258 | -- function is overloaded to accommodate varchar2 and clob.
259 | --
260 | -- Written 24APR2017
261 | -- cmoore
262 | --
263 | -- Modifications:
264 | --
265 | --
266 | --
267 | -------------------------------------------------------------------------------
268 | as
269 | l_position number;
270 | l_return varchar2(4000);
271 | begin
272 | if instr(P_TEXT, nl) > 0 then
273 | l_return := replace(P_TEXT, nl, '
');
274 | end if;
275 | return l_return;
276 | end nl_2_br;
277 |
278 | function column_align (
279 | P_COLUMN in wwv_flow_global.vc_arr2,
280 | P_INDEX in pls_integer,
281 | P_ROW_OUT in varchar2,
282 | P_HEADER in varchar
283 | ) return varchar2
284 | as
285 | -------------------------------------------------------------------------------
286 | -- FUNCTION column_align
287 | -- uses the array P_COLUMN to determin the text alignment for each column
288 | --
289 | -- P_HEADER is either 'h' for header or 'd' for detail
290 | --
291 | -- Written 24APR2017
292 | -- cmoore
293 | --
294 | -- Modifications:
295 | --
296 | --
297 | --
298 | -------------------------------------------------------------------------------
299 | l_row_out varchar2(4000);
300 | begin
301 | l_row_out := P_ROW_OUT;
302 | case
303 | when P_COLUMN(P_INDEX) = 'right' then
304 | l_row_out := l_row_out || '';
305 | when P_COLUMN(P_INDEX) = 'center' then
306 | l_row_out := l_row_out || '';
307 | else
308 | l_row_out := l_row_out || '';
309 | end case;
310 | return l_row_out;
311 | exception when no_data_found then
312 | return l_row_out || '';
313 | end column_align;
314 |
315 | function md_2_html (
316 | P_TEXT in clob
317 | ) return clob
318 | -------------------------------------------------------------------------------
319 | -- FUNCTION md_2_html
320 | -- returns clob
321 | -- converts markdown to HTML in a clob that is less than 32K
322 | -- notes:
323 | -- As of April 2017, the UL/OL process does not accommodate nesting
324 | -- And tables do require a leading and trailing pipe '|'
325 | --
326 | -- Written 24APR2017
327 | -- cmoore
328 | --
329 | -- Modifications:
330 | --
331 | --
332 | --
333 | -------------------------------------------------------------------------------
334 | as
335 | l_text varchar2(32767);
336 | l_row_in wwv_flow_global.vc_arr2;
337 | l_column wwv_flow_global.vc_arr2;
338 | l_pipe_count pls_integer;
339 | l_column_count pls_integer;
340 | l_left_pos pls_integer;
341 | l_right_pos pls_integer;
342 | l_left_side varchar2(5);
343 | l_right_side varchar2(5);
344 | l_row_out varchar2(4000);
345 | l_text_out varchar2(32767);
346 | l_in_ol boolean := false;
347 | l_in_ul boolean := false;
348 | l_in_table boolean := false;
349 | l_in_blockq boolean := false;
350 | l_in_code boolean := false;
351 | l_skip_line boolean := false;
352 | l_newline varchar2(4);
353 | l_ol_row number := 0;
354 | l_ul_row number := 0;
355 | l_table_row number := 0;
356 | l_blockq_row number := 0;
357 | l_code_row number := 0;
358 | begin
359 | if dbms_lob.getlength(P_TEXT) < 32000 then
360 | --l_text := crlf_2_nl(P_TEXT);
361 | l_text := P_TEXT;
362 | case
363 | when instr(l_text, crlf) > 0 then
364 | l_row_in := apex_util.string_to_table(l_text, crlf);
365 | l_newline := crlf;
366 | when instr(l_text, cr) > 0 then
367 | l_row_in := apex_util.string_to_table(l_text, cr);
368 | l_newline := cr;
369 | when instr(l_text, nl) > 0 then
370 | l_row_in := apex_util.string_to_table(l_text, nl);
371 | l_newline := nl;
372 | else
373 | null;
374 | end case;
375 |
376 | for i in 1 .. l_row_in.count loop
377 |
378 | -- line type
379 | case
380 | -- HEADER
381 | when substr(l_row_in(i),1,1) = '#' then
382 | l_row_out := h(l_row_in(i));
383 |
384 | -- Note on lists, at present, these are NOT nested.
385 |
386 | -- ORDERED LIST
387 | when regexp_instr(l_row_in(i), '(\A\s*?[0-9]+\.\s)(.*)') > 0 then
388 | l_ol_row := l_ol_row + 1;
389 | l_row_out := regexp_replace(l_row_in(i),'(\A\s*?[0-9]+\.\s)(.*)','\2');
390 | if l_ol_row = 1 then
391 | l_row_out := l_newline || '' || l_newline || l_row_out;
392 | l_in_ol := true;
393 | end if;
394 |
395 | -- UNORDERED LIST
396 | when ltrim(substr(l_row_in(i),1,2)) in ('+ ', '* ', '- ') then
397 | l_ul_row := l_ul_row + 1;
398 | l_row_out := regexp_replace( l_row_in(i),'(\A\s|\+|\-|\*)(\s)(.*)','- \3
');
399 | if l_ul_row = 1 then
400 | l_row_out := l_newline || '