├── 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)(.*)','
    1. \3
    2. '); 399 | if l_ul_row = 1 then 400 | l_row_out := l_newline || '
        ' || l_newline || l_row_out; 401 | l_in_ul := true; 402 | end if; 403 | 404 | -- BLOCKQUOTE 405 | when ltrim(substr(l_row_in(i),1,2)) = '> ' then 406 | l_blockq_row := l_blockq_row + 1; 407 | l_row_out := ltrim(substr(l_row_in(i),3)); 408 | if l_blockq_row = 1 then 409 | l_row_out := '
        ' || l_row_out; 410 | l_in_blockq := true; 411 | end if; 412 | 413 | -- Code Block 414 | when ltrim(substr(l_row_in(i),1,3)) = '```' then 415 | l_code_row := l_code_row + 1; 416 | --l_row_out := ltrim(substr(l_row_in(i),4)); you can skip this row 417 | if l_code_row = 1 then 418 | l_row_out := '
        ' || l_newline || '';
        419 | 					l_in_code := true;
        420 | 				else
        421 | 					if l_in_code  then
        422 | 						l_row_out := '' || l_newline || '
        '; 423 | l_in_code := false; 424 | end if; 425 | end if; 426 | 427 | -- TABLE 428 | when substr(l_row_in(i),1,1) = '|' then 429 | l_table_row := l_table_row + 1; 430 | if l_table_row = 1 then 431 | l_text_out := l_text_out || l_newline || '' ; 432 | 433 | l_in_table := true; 434 | -- look to the next row for column headers 435 | if substr(l_row_in(i + 1), 1,3) in ('|--','| -','|:-') then 436 | -- reinitialize the column array 437 | l_column.delete; 438 | l_pipe_count := regexp_count(l_row_in(i),'\|'); 439 | l_column_count := l_pipe_count - 1; 440 | for c in 1 .. l_pipe_count loop 441 | if c < l_pipe_count then 442 | -- left side of column, don't bother with last 443 | l_left_pos := instr(l_row_in(i + 1),'|', 1, c); -- find the pipe 444 | l_left_side := substr(l_row_in(i + 1),l_left_pos, 2); 445 | 446 | l_right_pos := instr(l_row_in(i + 1),'|', 1, c + 1); -- find the pipe 447 | l_right_side := substr(l_row_in(i + 1),l_right_pos - 1 , 2); 448 | case 449 | when trim(l_left_side) = '|' and trim(l_right_side) = '|' then 450 | l_column(c) := 'left'; 451 | when trim(l_left_side) = '|:' and trim(l_right_side) = ':|' then 452 | l_column(c) := 'center'; 453 | when trim(l_left_side) = '|' and trim(l_right_side) = ':|' then 454 | l_column(c) := 'right'; 455 | else 456 | l_column(c) := 'left'; 457 | end case; 458 | end if; -- c < pipe_count 459 | end loop; -- columns for headers 460 | l_row_out := l_row_out || '' ; 461 | end if; -- header row with |--, | -, |:-, etc 462 | -- loop through the columns in the header 463 | if l_column_count is null then 464 | l_pipe_count := regexp_count(l_row_in(i),'\|'); 465 | l_column_count := l_pipe_count - 1; 466 | end if; -- column count is null 467 | l_row_out := l_row_out || l_newline || '' || l_newline ; 468 | for c in 1 .. l_column_count loop 469 | l_left_pos := instr(l_row_in(i),'|', 1, c); -- find the pipe 470 | l_left_side := substr(l_row_in(i),l_left_pos, 2); 471 | 472 | l_right_pos := instr(l_row_in(i),'|', 1, c + 1); -- find the pipe 473 | l_right_side := substr(l_row_in(i),l_right_pos - 1 , 2); 474 | l_row_out := column_align ( 475 | P_COLUMN => l_column, 476 | P_INDEX => c, 477 | P_ROW_OUT => l_row_out, 478 | P_HEADER => 'h' 479 | ); 480 | l_row_out := l_row_out || trim(substr(l_row_in(i),l_left_pos + 1, (l_right_pos - l_left_pos - 1))); 481 | l_row_out := l_row_out || '' || l_newline; 482 | end loop; -- columns in the header 483 | l_row_out := l_row_out || '' || l_newline; 484 | if instr(l_row_out,'') > 0 then 485 | l_row_out := l_row_out || '' || l_newline; 486 | end if; -- is included 487 | l_row_out := l_row_out || '' || l_newline; 488 | else -- table row = 1 489 | -- skip the dash line/header defintions 490 | if substr(l_row_in(i), 1,3) not in ('|--','| -','|:-') then 491 | -- loop through the columns in the header 492 | l_row_out := l_row_out || l_newline || '' || l_newline ; 493 | for c in 1 .. l_column_count loop 494 | l_left_pos := instr(l_row_in(i),'|', 1, c); -- find the pipe 495 | l_left_side := substr(l_row_in(i),l_left_pos, 2); 496 | 497 | l_right_pos := instr(l_row_in(i),'|', 1, c + 1); -- find the pipe 498 | l_right_side := substr(l_row_in(i),l_right_pos - 1 , 2); 499 | l_row_out := column_align ( 500 | P_COLUMN => l_column, 501 | P_INDEX => c, 502 | P_ROW_OUT => l_row_out, 503 | P_HEADER => 'd' 504 | ); 505 | l_row_out := l_row_out || trim(substr(l_row_in(i),l_left_pos + 1, (l_right_pos - l_left_pos - 1))); 506 | l_row_out := l_row_out || '' || l_newline; 507 | end loop; -- columns in the header 508 | l_row_out := l_row_out || ''; 509 | else 510 | l_skip_line := true; -- do not capture the header break 511 | end if; -- not a header 512 | end if; -- table row = 1 513 | else 514 | -- button up tags 515 | if l_in_ul then 516 | l_row_out := '' || l_newline || l_row_in(i); 517 | l_in_ul := false; 518 | l_ul_row := 0; 519 | end if; -- end list 520 | if l_in_ol then 521 | l_row_out := '' || l_newline || l_row_in(i); 522 | l_in_ol := false; 523 | l_ol_row := 0; 524 | end if; -- end list 525 | if l_in_blockq then 526 | l_row_out := '' || l_newline || l_row_in(i); 527 | l_in_blockq := false; 528 | l_blockq_row := 0; 529 | end if; -- end list 530 | if l_in_table then 531 | l_row_out := '' || l_newline || '
        ' || l_newline || l_row_in(i); 532 | l_in_table := false; 533 | l_table_row := 0; 534 | l_column.delete; 535 | end if; 536 | end case; 537 | if not l_skip_line then -- need to skip the line in a table between header and body 538 | if l_row_out is null then 539 | l_row_out := l_row_in(i); 540 | end if; 541 | 542 | -- inline text formatting 543 | -- don't use or '|' for expression because both sides must match 544 | l_row_out := regexp_replace(l_row_out,'(\*\*)(.*?)(\*\*)','' || '\2' || ''); 545 | l_row_out := regexp_replace(l_row_out,'(__)(.*?)(__)','' || '\2' || ''); 546 | l_row_out := regexp_replace(l_row_out,'(_)(.*?)(_)','' || '\2' || ''); 547 | l_row_out := regexp_replace(l_row_out,'(\*)(.*?)(\*)','' || '\2' || ''); 548 | l_row_out := regexp_replace(l_row_out,'(\~\~)(.*?)(\~\~)','' || '\2' || ''); 549 | l_row_out := regexp_replace(l_row_out,'(`)(.*?)(`)','' || '\2' || ''); 550 | l_row_out := regexp_replace(l_row_out,'!\[([^\[]+)\]\(([^\)]+)\)','\1\1'); 551 | l_row_out := regexp_replace(l_row_out,'\[([^\[]+)\]\(([^\)]+)\)','\1'); 552 | 553 | if not l_in_table then 554 | l_row_out := regexp_replace(l_row_out,'-{3,}','
        '); 555 | end if; 556 | 557 | if l_text_out is null then 558 | l_text_out := l_row_out; 559 | else 560 | -- if l_in_code then 561 | -- l_text_out := l_text_out || l_newline || l_newline || l_row_out; 562 | -- else 563 | l_text_out := l_text_out || l_newline ||l_row_out; 564 | -- end if; -- in code block 565 | end if; -- l_text_out null 566 | else -- skip line 567 | l_skip_line := false; 568 | end if; -- skip line 569 | l_row_out := ''; 570 | end loop; 571 | end if; -- text is less than 32K 572 | 573 | --l_text_out := crlf_2_br(l_text_out); 574 | --dbms_output.put_line(l_text_out); 575 | -- you can drop the text into a CLOB use as you see fit. 576 | return l_text_out; 577 | end md_2_html; 578 | 579 | 580 | function md_2_html ( 581 | P_TEXT in varchar2 582 | ) return varchar2 583 | ------------------------------------------------------------------------------- 584 | -- FUNCTION md_2_html 585 | -- returns varchar2 586 | -- converts markdown to HTML in a clob that is less than 32K 587 | -- notes: 588 | -- As of April 2017, the UL/OL process does not accommodate nesting 589 | -- And tables do require a leading and trailing pipe '|' 590 | -- 591 | -- Written 24APR2017 592 | -- cmoore 593 | -- 594 | -- Modifications: 595 | -- 596 | -- 597 | -- 598 | ------------------------------------------------------------------------------- 599 | as 600 | l_text varchar2(32767); 601 | l_row_in wwv_flow_global.vc_arr2; 602 | l_column wwv_flow_global.vc_arr2; 603 | l_pipe_count pls_integer; 604 | l_column_count pls_integer; 605 | l_left_pos pls_integer; 606 | l_right_pos pls_integer; 607 | l_left_side varchar2(5); 608 | l_right_side varchar2(5); 609 | l_row_out varchar2(4000); 610 | l_text_out varchar2(32767); 611 | l_in_ol boolean := false; 612 | l_in_ul boolean := false; 613 | l_in_table boolean := false; 614 | l_in_blockq boolean := false; 615 | l_skip_line boolean := false; 616 | l_newline varchar2(4); 617 | l_ol_row number := 0; 618 | l_ul_row number := 0; 619 | l_table_row number := 0; 620 | l_blockq_row number := 0; 621 | begin 622 | if dbms_lob.getlength(P_TEXT) < 32000 then 623 | --l_text := crlf_2_nl(P_TEXT); 624 | l_text := P_TEXT; 625 | case 626 | when instr(l_text, crlf) > 0 then 627 | l_row_in := apex_util.string_to_table(l_text, crlf); 628 | l_newline := crlf; 629 | when instr(l_text, cr) > 0 then 630 | l_row_in := apex_util.string_to_table(l_text, cr); 631 | l_newline := cr; 632 | when instr(l_text, nl) > 0 then 633 | l_row_in := apex_util.string_to_table(l_text, nl); 634 | l_newline := nl; 635 | else 636 | null; 637 | end case; 638 | 639 | for i in 1 .. l_row_in.count loop 640 | 641 | -- line type 642 | case 643 | -- HEADER 644 | when substr(l_row_in(i),1,1) = '#' then 645 | l_row_out := h(l_row_in(i)); 646 | 647 | -- Note on lists, at present, these are NOT nested. 648 | 649 | -- ORDERED LIST 650 | when regexp_instr(l_row_in(i), '(\A\s*?[0-9]+\.\s)(.*)') > 0 then 651 | l_ol_row := l_ol_row + 1; 652 | l_row_out := regexp_replace(l_row_in(i),'(\A\s*?[0-9]+\.\s)(.*)','
      • \2
      • '); 653 | if l_ol_row = 1 then 654 | l_row_out := l_newline || '
          ' || l_newline || l_row_out; 655 | l_in_ol := true; 656 | end if; 657 | 658 | -- UNORDERED LIST 659 | when ltrim(substr(l_row_in(i),1,2)) in ('* ', '- ', '+ ') then 660 | l_ul_row := l_ul_row + 1; 661 | l_row_out := regexp_replace( l_row_in(i),'(\A\s\+|\-|\*)(\s)(.*)','
        1. \3
        2. '); 662 | if l_ul_row = 1 then 663 | l_row_out := l_newline || '
            ' || l_newline || l_row_out; 664 | l_in_ul := true; 665 | end if; 666 | 667 | -- BLOCKQUOTE 668 | when ltrim(substr(l_row_in(i),1,2)) = '> ' then 669 | l_blockq_row := l_blockq_row + 1; 670 | l_row_out := ltrim(substr(l_row_in(i),3)); 671 | if l_blockq_row = 1 then 672 | l_row_out := '
            ' || l_row_out; 673 | l_in_blockq := true; 674 | end if; 675 | 676 | -- TABLE 677 | when substr(l_row_in(i),1,1) = '|' then 678 | l_table_row := l_table_row + 1; 679 | if l_table_row = 1 then 680 | l_text_out := l_text_out || l_newline || '' ; 681 | 682 | l_in_table := true; 683 | -- look to the next row for column headers 684 | if substr(l_row_in(i + 1), 1,3) in ('|--','| -','|:-') then 685 | -- reinitialize the column array 686 | l_column.delete; 687 | l_pipe_count := regexp_count(l_row_in(i),'\|'); 688 | l_column_count := l_pipe_count - 1; 689 | for c in 1 .. l_pipe_count loop 690 | if c < l_pipe_count then 691 | -- left side of column, don't bother with last 692 | l_left_pos := instr(l_row_in(i + 1),'|', 1, c); -- find the pipe 693 | l_left_side := substr(l_row_in(i + 1),l_left_pos, 2); 694 | 695 | l_right_pos := instr(l_row_in(i + 1),'|', 1, c + 1); -- find the pipe 696 | l_right_side := substr(l_row_in(i + 1),l_right_pos - 1 , 2); 697 | case 698 | when trim(l_left_side) = '|' and trim(l_right_side) = '|' then 699 | l_column(c) := 'left'; 700 | when trim(l_left_side) = '|:' and trim(l_right_side) = ':|' then 701 | l_column(c) := 'center'; 702 | when trim(l_left_side) = '|' and trim(l_right_side) = ':|' then 703 | l_column(c) := 'right'; 704 | else 705 | l_column(c) := 'left'; 706 | end case; 707 | end if; -- c < pipe_count 708 | end loop; -- columns for headers 709 | l_row_out := l_row_out || '' ; 710 | end if; -- header row with |--, | -, |:-, etc 711 | -- loop through the columns in the header 712 | if l_column_count is null then 713 | l_pipe_count := regexp_count(l_row_in(i),'\|'); 714 | l_column_count := l_pipe_count - 1; 715 | end if; -- column count is null 716 | l_row_out := l_row_out || l_newline || '' || l_newline ; 717 | for c in 1 .. l_column_count loop 718 | l_left_pos := instr(l_row_in(i),'|', 1, c); -- find the pipe 719 | l_left_side := substr(l_row_in(i),l_left_pos, 2); 720 | 721 | l_right_pos := instr(l_row_in(i),'|', 1, c + 1); -- find the pipe 722 | l_right_side := substr(l_row_in(i),l_right_pos - 1 , 2); 723 | l_row_out := column_align ( 724 | P_COLUMN => l_column, 725 | P_INDEX => c, 726 | P_ROW_OUT => l_row_out, 727 | P_HEADER => 'h' 728 | ); 729 | l_row_out := l_row_out || trim(substr(l_row_in(i),l_left_pos + 1, (l_right_pos - l_left_pos - 1))); 730 | l_row_out := l_row_out || '' || l_newline; 731 | end loop; -- columns in the header 732 | l_row_out := l_row_out || '' || l_newline; 733 | if instr(l_row_out,'') > 0 then 734 | l_row_out := l_row_out || '' || l_newline; 735 | end if; -- is included 736 | l_row_out := l_row_out || '' || l_newline; 737 | else -- table row = 1 738 | -- skip the dash line/header defintions 739 | if substr(l_row_in(i), 1,3) not in ('|--','| -','|:-') then 740 | -- loop through the columns in the header 741 | l_row_out := l_row_out || l_newline || '' || l_newline ; 742 | for c in 1 .. l_column_count loop 743 | l_left_pos := instr(l_row_in(i),'|', 1, c); -- find the pipe 744 | l_left_side := substr(l_row_in(i),l_left_pos, 2); 745 | 746 | l_right_pos := instr(l_row_in(i),'|', 1, c + 1); -- find the pipe 747 | l_right_side := substr(l_row_in(i),l_right_pos - 1 , 2); 748 | l_row_out := column_align ( 749 | P_COLUMN => l_column, 750 | P_INDEX => c, 751 | P_ROW_OUT => l_row_out, 752 | P_HEADER => 'd' 753 | ); 754 | l_row_out := l_row_out || trim(substr(l_row_in(i),l_left_pos + 1, (l_right_pos - l_left_pos - 1))); 755 | l_row_out := l_row_out || '' || l_newline; 756 | end loop; -- columns in the header 757 | l_row_out := l_row_out || ''; 758 | else 759 | l_skip_line := true; -- do not capture the header break 760 | end if; -- not a header 761 | end if; -- table row = 1 762 | else 763 | -- button up tags 764 | if l_in_ul then 765 | l_row_out := '' || l_newline || l_row_in(i); 766 | l_in_ul := false; 767 | l_ul_row := 0; 768 | end if; -- end list 769 | if l_in_ol then 770 | l_row_out := '' || l_newline || l_row_in(i); 771 | l_in_ol := false; 772 | l_ol_row := 0; 773 | end if; -- end list 774 | if l_in_blockq then 775 | l_row_out := '' || l_newline || l_row_in(i); 776 | l_in_blockq := false; 777 | l_blockq_row := 0; 778 | end if; -- end list 779 | if l_in_table then 780 | l_row_out := '' || l_newline || '
            ' || l_newline || l_row_in(i); 781 | l_in_table := false; 782 | l_table_row := 0; -- reinit the table row count 783 | l_column.delete; -- reinit the column alignment 784 | l_column_count := 0; -- reinit the column count 785 | end if; 786 | end case; 787 | if not l_skip_line then -- need to skip the line in a table between header and body 788 | if l_row_out is null then 789 | l_row_out := l_row_in(i); 790 | end if; 791 | 792 | -- inline text formatting 793 | -- don't use or '|' for expression because both sides must match 794 | l_row_out := regexp_replace(l_row_out,'(\*\*)(.*?)(\*\*)','' || '\2' || ''); 795 | l_row_out := regexp_replace(l_row_out,'(__)(.*?)(__)','' || '\2' || ''); 796 | l_row_out := regexp_replace(l_row_out,'(_)(.*?)(_)','' || '\2' || ''); 797 | l_row_out := regexp_replace(l_row_out,'(\*)(.*?)(\*)','' || '\2' || ''); 798 | l_row_out := regexp_replace(l_row_out,'(\~\~)(.*?)(\~\~)','' || '\2' || ''); 799 | l_row_out := regexp_replace(l_row_out,'(`)(.*?)(`)','' || '\2' || ''); 800 | l_row_out := regexp_replace(l_row_out,'!\[([^\[]+)\]\(([^\)]+)\)','\1\1'); 801 | l_row_out := regexp_replace(l_row_out,'\[([^\[]+)\]\(([^\)]+)\)','\1'); 802 | 803 | if not l_in_table then 804 | l_row_out := regexp_replace(l_row_out,'-{3,}','
            '); 805 | end if; 806 | 807 | if l_text_out is null then 808 | l_text_out := l_row_out; 809 | else 810 | l_text_out := l_text_out || l_newline ||l_row_out; 811 | end if; -- 812 | else -- skip line 813 | l_skip_line := false; 814 | end if; -- skip line 815 | l_row_out := ''; 816 | end loop; 817 | end if; -- text is less than 32K 818 | l_text_out := crlf_2_br(l_text_out); 819 | --dbms_output.put_line(l_text_out); 820 | -- you can drop the text into a varchar2 use as you see fit. 821 | return l_text_out; 822 | end md_2_html; 823 | 824 | end markdown_2_html; 825 | -------------------------------------------------------------------------------- /markdown_2_html.pks: -------------------------------------------------------------------------------- 1 | create or replace package markdown_2_html 2 | as 3 | 4 | function md_2_html ( 5 | P_TEXT in clob 6 | ) return clob; 7 | 8 | end markdown_2_html; 9 | -------------------------------------------------------------------------------- /setup/tables.txt: -------------------------------------------------------------------------------- 1 | # Code for creating a sample table 2 | ```sql 3 | CREATE TABLE "MRK_CLOB" ( 4 | "MARKDOWN_PK" NUMBER GENERATED ALWAYS AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE NOT NULL ENABLE, 5 | "MARKDOWN_CLOB" CLOB, 6 | CONSTRAINT "MARKDOWN" PRIMARY KEY ("MARKDOWN_PK") 7 | USING INDEX (CREATE UNIQUE INDEX "MARKDOWN_PK" ON "MRK_CLOB" ("MARKDOWN_PK")) 8 | ) ; 9 | ``` 10 | # Code for updating the clob 11 | ```sql 12 | update mrk_clob set 13 | markdown_clob = ' 14 | # Header 1 15 | Lorem ipsum dolor sit *amet,* consectetur *adipiscing _elit_*. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero. 16 | 17 | ## Header 2 18 | Sed dignissim ~~lacinia~~ nunc. Curabitur tortor. Pellentesque nibh. Aenean quam. In scelerisque sem at dolor. Maecenas mattis. Sed convallis tristique sem. Proin ut ligula vel nunc egestas porttitor. Morbi lectus risus, iaculis vel, suscipit quis, luctus non, massa. Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. Quisque volutpat condimentum velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. 19 | 20 | ### Header 3 21 | Nam nec ante. Sed **lacinia**, urna non tincidunt mattis, tortor neque adipiscing diam, a cursus ipsum ante quis turpis. Nulla facilisi. Ut fringilla. Suspendisse potenti. Nunc feugiat mi a tellus consequat imperdiet. Vestibulum sapien. Proin quam. Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. Sed lectus. 22 | 23 | #### Header 4 24 | Integer euismod lacus luctus magna. Quisque cursus, metus vitae pharetra auctor, sem massa mattis sem, at interdum magna augue eget diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi lacinia molestie dui. Praesent blandit dolor. Sed non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit amet pede facilisis laoreet. Donec lacus nunc, viverra nec, blandit vel, egestas et, augue. Vestibulum tincidunt malesuada tellus. Ut ultrices ultrices enim. Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper. Nulla facilisi. 25 | 26 | ##### H5 27 | ###### H6 28 | 29 | Integer lacinia sollicitudin massa: 30 | 1. Cras metus. 31 | 1. Sed aliquet 32 | 3. risus a tortor. 33 | 1. Integer id quam. 34 | 35 | Morbi mi. Quisque nisl felis: 36 | * venenatis tristique, 37 | - dignissim in, 38 | + ultrices sit amet, 39 | * augue. 40 | 41 | Proin sodales libero eget ante followed block quote. 42 | 43 | > Nulla quam. Aenean laoreet. Vestibulum nisi lectus, commodo ac, facilisis ac, 44 | > ultricies eu, pede. Ut orci risus, accumsan porttitor, cursus quis, aliquet eget, justo. 45 | 46 | ``` 47 | 10 begin 48 | 49 | 20 print "HELLO WORLD" 50 | 51 | 30 end 52 | ``` 53 | 54 | and now we need a table 55 | 56 | 57 | | Tables | Are | Cool | 58 | | ------ |:-----:| -------:| 59 | | col 3 is | right-aligned | $1600 | 60 | | col 2 is | centered | $12 | 61 | | zebra stripes | are neat | $1 | 62 | 63 | Sed pretium blandit orci. Link to [Storm Petrel LLC](https://storm-petrel.com) Ut eu diam at pede suscipit sodales. 64 | 65 | Quisque cursus, metus vitae pharetra ![auctor](https://storm-petrel.com/wp-content/uploads/2016/05/tempest-gems-T-word-tag-250.gif), sem massa mattis sem, at interdum magna augue eget diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi lacinia molestie dui. Praesent blandit dolor. Sed non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit amet pede facilisis laoreet. Donec lacus nunc, viverra nec, blandit vel, egestas et, augue. Vestibulum tincidunt malesuada tellus. 66 | 67 | Ut ultrices ultrices enim. Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper. 68 | 69 | And other table with limited header stuff. All columns are left aligned 70 | 71 | | Tables | Are | Cool | 72 | | col 3 is | right-aligned | $1600 | 73 | | col 2 is | centered | $12 | 74 | | zebra stripes | are neat | $1 | 75 | 76 | eof' 77 | ``` 78 | --------------------------------------------------------------------------------