2021-11-02 07:32:17 -07:00
import {
2019-12-04 21:14:51 +01:00
noopTest ,
2023-03-22 00:48:56 -05:00
edit
2021-11-02 07:32:17 -07:00
} from './helpers.js' ;
2019-11-05 15:29:42 -06:00
/ * *
* Block - Level Grammar
* /
2021-11-02 07:32:17 -07:00
export const block = {
2021-01-26 08:20:13 -06:00
newline : /^(?: *(?:\n|$))+/ ,
code : /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/ ,
2023-03-21 19:50:28 -05:00
fences : /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/ ,
2022-04-10 18:37:04 -06:00
hr : /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/ ,
2020-12-10 10:28:58 -06:00
heading : /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/ ,
2019-11-05 15:29:42 -06:00
blockquote : /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/ ,
2022-04-10 18:37:04 -06:00
list : /^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/ ,
2019-11-05 15:29:42 -06:00
html : '^ {0,3}(?:' // optional indentation
2021-06-25 16:14:09 -04:00
+ '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)' // (1)
2019-11-05 15:29:42 -06:00
+ '|comment[^\\n]*(\\n+|$)' // (2)
2020-08-07 11:19:26 -05:00
+ '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3)
+ '|<![A-Z][\\s\\S]*?(?:>\\n*|$)' // (4)
+ '|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)' // (5)
2021-05-20 14:42:45 +01:00
+ '|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (6)
2021-06-25 16:14:09 -04:00
+ '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) open tag
+ '|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) closing tag
2019-11-05 15:29:42 -06:00
+ ')' ,
2022-11-20 10:05:52 -06:00
def : /^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/ ,
2019-12-04 21:14:51 +01:00
table : noopTest ,
2022-11-20 10:06:04 -06:00
lheading : /^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/ ,
2019-11-05 15:29:42 -06:00
// regex template, placeholders will be replaced according to different paragraph
// interruption rules of commonmark and the original markdown spec:
2021-11-25 08:10:22 +08:00
_paragraph : /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/ ,
2019-11-05 15:29:42 -06:00
text : /^[^\n]+/
} ;
2022-01-12 19:53:48 -06:00
block . _label = /(?!\s*\])(?:\\.|[^\[\]\\])+/ ;
2019-11-05 15:29:42 -06:00
block . _title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/ ;
block . def = edit ( block . def )
. replace ( 'label' , block . _label )
. replace ( 'title' , block . _title )
. getRegex ( ) ;
2020-06-17 17:27:35 +02:00
block . bullet = /(?:[*+-]|\d{1,9}[.)])/ ;
2021-02-26 23:51:21 -06:00
block . listItemStart = edit ( /^( *)(bull) */ )
2020-11-04 15:23:04 -06:00
. replace ( 'bull' , block . bullet )
. getRegex ( ) ;
2019-11-05 15:29:42 -06:00
block . list = edit ( block . list )
. replace ( /bull/g , block . bullet )
. replace ( 'hr' , '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))' )
. replace ( 'def' , '\\n+(?=' + block . def . source + ')' )
. getRegex ( ) ;
block . _tag = 'address|article|aside|base|basefont|blockquote|body|caption'
+ '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption'
+ '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe'
+ '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option'
+ '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr'
+ '|track|ul' ;
2020-08-07 09:03:47 +03:00
block . _comment = /<!--(?!-?>)[\s\S]*?(?:-->|$)/ ;
2019-11-05 15:29:42 -06:00
block . html = edit ( block . html , 'i' )
. replace ( 'comment' , block . _comment )
. replace ( 'tag' , block . _tag )
. replace ( 'attribute' , / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/ )
. getRegex ( ) ;
block . paragraph = edit ( block . _paragraph )
. replace ( 'hr' , block . hr )
2020-02-11 11:30:33 -05:00
. replace ( 'heading' , ' {0,3}#{1,6} ' )
2019-11-05 15:29:42 -06:00
. replace ( '|lheading' , '' ) // setex headings don't interrupt commonmark paragraphs
2021-11-25 08:10:22 +08:00
. replace ( '|table' , '' )
2019-11-05 15:29:42 -06:00
. replace ( 'blockquote' , ' {0,3}>' )
2020-02-10 16:06:38 -05:00
. replace ( 'fences' , ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n' )
2019-11-05 15:29:42 -06:00
. replace ( 'list' , ' {0,3}(?:[*+-]|1[.)]) ' ) // only lists starting from 1 can interrupt
2021-06-25 16:14:09 -04:00
. replace ( 'html' , '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)' )
2019-11-05 15:29:42 -06:00
. replace ( 'tag' , block . _tag ) // pars can be interrupted by type (6) html blocks
. getRegex ( ) ;
block . blockquote = edit ( block . blockquote )
. replace ( 'paragraph' , block . paragraph )
. getRegex ( ) ;
/ * *
* Normal Block Grammar
* /
2023-03-22 00:48:56 -05:00
block . normal = { ... block } ;
2019-11-05 15:29:42 -06:00
/ * *
* GFM Block Grammar
* /
2023-03-22 00:48:56 -05:00
block . gfm = {
... block . normal ,
2021-08-02 15:12:43 -04:00
table : '^ *([^\\n ].*\\|.*)\\n' // Header
2021-09-08 15:20:45 -05:00
+ ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' // Align
2021-08-24 21:23:53 -05:00
+ '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)' // Cells
2023-03-22 00:48:56 -05:00
} ;
2019-11-05 15:29:42 -06:00
2020-02-04 23:46:34 -05:00
block . gfm . table = edit ( block . gfm . table )
. replace ( 'hr' , block . hr )
2020-02-11 11:30:33 -05:00
. replace ( 'heading' , ' {0,3}#{1,6} ' )
2020-02-04 23:46:34 -05:00
. replace ( 'blockquote' , ' {0,3}>' )
2020-02-10 15:09:19 -05:00
. replace ( 'code' , ' {4}[^\\n]' )
2020-02-11 11:32:43 -05:00
. replace ( 'fences' , ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n' )
2020-02-04 23:46:34 -05:00
. replace ( 'list' , ' {0,3}(?:[*+-]|1[.)]) ' ) // only lists starting from 1 can interrupt
2021-06-25 16:14:09 -04:00
. replace ( 'html' , '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)' )
2020-03-09 11:02:26 -05:00
. replace ( 'tag' , block . _tag ) // tables can be interrupted by type (6) html blocks
2020-02-04 23:46:34 -05:00
. getRegex ( ) ;
2021-11-25 08:10:22 +08:00
block . gfm . paragraph = edit ( block . _paragraph )
. replace ( 'hr' , block . hr )
. replace ( 'heading' , ' {0,3}#{1,6} ' )
. replace ( '|lheading' , '' ) // setex headings don't interrupt commonmark paragraphs
. replace ( 'table' , block . gfm . table ) // interrupt paragraphs with table
. replace ( 'blockquote' , ' {0,3}>' )
. replace ( 'fences' , ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n' )
. replace ( 'list' , ' {0,3}(?:[*+-]|1[.)]) ' ) // only lists starting from 1 can interrupt
. replace ( 'html' , '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)' )
. replace ( 'tag' , block . _tag ) // pars can be interrupted by type (6) html blocks
. getRegex ( ) ;
2019-11-05 15:29:42 -06:00
/ * *
* Pedantic grammar ( original John Gruber ' s loose markdown specification )
* /
2023-03-22 00:48:56 -05:00
block . pedantic = {
... block . normal ,
2019-11-05 15:29:42 -06:00
html : edit (
'^ *(?:comment *(?:\\n|\\s*$)'
+ '|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)' // closed tag
+ '|<tag(?:"[^"]*"|\'[^\']*\'|\\s[^\'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))' )
. replace ( 'comment' , block . _comment )
. replace ( /tag/g , '(?!(?:'
+ 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub'
+ '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)'
+ '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b' )
. getRegex ( ) ,
def : /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/ ,
2020-12-10 10:28:58 -06:00
heading : /^(#{1,6})(.*)(?:\n+|$)/ ,
2019-12-04 21:14:51 +01:00
fences : noopTest , // fences not supported
2022-11-20 10:06:04 -06:00
lheading : /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/ ,
2019-11-05 15:29:42 -06:00
paragraph : edit ( block . normal . _paragraph )
. replace ( 'hr' , block . hr )
. replace ( 'heading' , ' *#{1,6} *[^\n]' )
. replace ( 'lheading' , block . lheading )
. replace ( 'blockquote' , ' {0,3}>' )
. replace ( '|fences' , '' )
. replace ( '|list' , '' )
. replace ( '|html' , '' )
. getRegex ( )
2023-03-22 00:48:56 -05:00
} ;
2019-11-05 15:29:42 -06:00
/ * *
* Inline - Level Grammar
* /
2021-11-02 07:32:17 -07:00
export const inline = {
2019-11-05 15:29:42 -06:00
escape : /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/ ,
autolink : /^<(scheme:[^\s\x00-\x1f<>]*|email)>/ ,
2019-12-04 21:14:51 +01:00
url : noopTest ,
2019-11-05 15:29:42 -06:00
tag : '^comment'
+ '|^</[a-zA-Z][\\w:-]*\\s*>' // self-closing tag
+ '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag
+ '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. <?php ?>
+ '|^<![a-zA-Z]+\\s[\\s\\S]*?>' // declaration, e.g. <!DOCTYPE html>
+ '|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>' , // CDATA section
link : /^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/ ,
2022-01-12 19:53:48 -06:00
reflink : /^!?\[(label)\]\[(ref)\]/ ,
nolink : /^!?\[(ref)\](?:\[\])?/ ,
2020-06-20 10:25:48 -05:00
reflinkSearch : 'reflink|nolink(?!\\()' ,
2021-02-07 17:25:01 -05:00
emStrong : {
lDelim : /^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/ ,
// (1) and (2) can only be a Right Delimiter. (3) and (4) can only be Left. (5) and (6) can be either Left or Right.
2022-11-01 22:03:58 -04:00
// () Skip orphan inside strong () Consume to delim (1) #*** (2) a***#, a*** (3) #***a, ***a (4) ***# (5) #***# (6) a***a
rDelimAst : /^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/ ,
rDelimUnd : /^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/ // ^- Not allowed for _
2020-07-09 19:35:22 -04:00
} ,
2019-11-05 15:29:42 -06:00
code : /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/ ,
br : /^( {2,}|\\)\n(?!\s*$)/ ,
2019-12-04 21:14:51 +01:00
del : noopTest ,
2021-02-07 17:25:01 -05:00
text : /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/ ,
punctuation : /^([\spunctuation])/
2019-11-05 15:29:42 -06:00
} ;
2021-02-07 17:25:01 -05:00
// list of punctuation marks from CommonMark spec
// without * and _ to handle the different emphasis markers * and _
2020-06-12 16:29:25 -04:00
inline . _punctuation = '!"#$%&\'()+\\-.,/:;<=>?@\\[\\]`^{|}~' ;
2020-05-29 16:33:49 -04:00
inline . punctuation = edit ( inline . punctuation ) . replace ( /punctuation/g , inline . _punctuation ) . getRegex ( ) ;
2020-06-17 00:41:06 -04:00
// sequences em should skip over [title](link), `code`, <html>
2021-02-07 17:25:01 -05:00
inline . blockSkip = /\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g ;
2022-11-01 22:03:58 -04:00
// lookbehind is not available on Safari as of version 16
// inline.escapedEmSt = /(?<=(?:^|[^\\)(?:\\[^])*)\\[*_]/g;
inline . escapedEmSt = /(?:^|[^\\])(?:\\\\)*\\[*_]/g ;
2020-06-12 15:30:25 -04:00
2020-08-07 11:19:26 -05:00
inline . _comment = edit ( block . _comment ) . replace ( '(?:-->|$)' , '-->' ) . getRegex ( ) ;
2021-02-07 17:25:01 -05:00
inline . emStrong . lDelim = edit ( inline . emStrong . lDelim )
. replace ( /punct/g , inline . _punctuation )
2020-07-08 16:00:12 -04:00
. getRegex ( ) ;
2021-02-07 17:25:01 -05:00
inline . emStrong . rDelimAst = edit ( inline . emStrong . rDelimAst , 'g' )
. replace ( /punct/g , inline . _punctuation )
2020-07-09 19:35:22 -04:00
. getRegex ( ) ;
2021-02-07 17:25:01 -05:00
inline . emStrong . rDelimUnd = edit ( inline . emStrong . rDelimUnd , 'g' )
. replace ( /punct/g , inline . _punctuation )
2020-07-08 16:00:12 -04:00
. getRegex ( ) ;
2019-11-05 15:29:42 -06:00
inline . _escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g ;
inline . _scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/ ;
inline . _email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/ ;
inline . autolink = edit ( inline . autolink )
. replace ( 'scheme' , inline . _scheme )
. replace ( 'email' , inline . _email )
. getRegex ( ) ;
inline . _attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/ ;
inline . tag = edit ( inline . tag )
2020-08-07 11:19:26 -05:00
. replace ( 'comment' , inline . _comment )
2019-11-05 15:29:42 -06:00
. replace ( 'attribute' , inline . _attribute )
. getRegex ( ) ;
2020-05-20 10:23:42 -05:00
inline . _label = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/ ;
2020-12-10 10:27:58 -06:00
inline . _href = /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/ ;
2019-11-05 15:29:42 -06:00
inline . _title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/ ;
inline . link = edit ( inline . link )
. replace ( 'label' , inline . _label )
. replace ( 'href' , inline . _href )
. replace ( 'title' , inline . _title )
. getRegex ( ) ;
inline . reflink = edit ( inline . reflink )
. replace ( 'label' , inline . _label )
2022-01-12 19:53:48 -06:00
. replace ( 'ref' , block . _label )
. getRegex ( ) ;
inline . nolink = edit ( inline . nolink )
. replace ( 'ref' , block . _label )
2019-11-05 15:29:42 -06:00
. getRegex ( ) ;
2020-06-20 10:25:48 -05:00
inline . reflinkSearch = edit ( inline . reflinkSearch , 'g' )
. replace ( 'reflink' , inline . reflink )
. replace ( 'nolink' , inline . nolink )
. getRegex ( ) ;
2019-11-05 15:29:42 -06:00
/ * *
* Normal Inline Grammar
* /
2023-03-22 00:48:56 -05:00
inline . normal = { ... inline } ;
2019-11-05 15:29:42 -06:00
/ * *
* Pedantic Inline Grammar
* /
2023-03-22 00:48:56 -05:00
inline . pedantic = {
... inline . normal ,
2020-07-09 19:35:22 -04:00
strong : {
start : /^__|\*\*/ ,
middle : /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/ ,
endAst : /\*\*(?!\*)/g ,
endUnd : /__(?!_)/g
} ,
em : {
start : /^_|\*/ ,
middle : /^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/ ,
endAst : /\*(?!\*)/g ,
endUnd : /_(?!_)/g
} ,
2019-11-05 15:29:42 -06:00
link : edit ( /^!?\[(label)\]\((.*?)\)/ )
. replace ( 'label' , inline . _label )
. getRegex ( ) ,
reflink : edit ( /^!?\[(label)\]\s*\[([^\]]*)\]/ )
. replace ( 'label' , inline . _label )
. getRegex ( )
2023-03-22 00:48:56 -05:00
} ;
2019-11-05 15:29:42 -06:00
/ * *
* GFM Inline Grammar
* /
2023-03-22 00:48:56 -05:00
inline . gfm = {
... inline . normal ,
2019-11-05 15:29:42 -06:00
escape : edit ( inline . escape ) . replace ( '])' , '~|])' ) . getRegex ( ) ,
_extended _email : /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/ ,
url : /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/ ,
2022-12-07 01:44:55 -06:00
_backpedal : /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/ ,
2020-11-14 20:03:39 -06:00
del : /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/ ,
2021-05-27 11:15:32 -05:00
text : /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/
2023-03-22 00:48:56 -05:00
} ;
2019-11-05 15:29:42 -06:00
inline . gfm . url = edit ( inline . gfm . url , 'i' )
. replace ( 'email' , inline . gfm . _extended _email )
. getRegex ( ) ;
/ * *
* GFM + Line Breaks Inline Grammar
* /
2023-03-22 00:48:56 -05:00
inline . breaks = {
... inline . gfm ,
2019-11-05 15:29:42 -06:00
br : edit ( inline . br ) . replace ( '{2,}' , '*' ) . getRegex ( ) ,
text : edit ( inline . gfm . text )
. replace ( '\\b_' , '\\b_| {2,}\\n' )
. replace ( /\{2,\}/g , '*' )
. getRegex ( )
2023-03-22 00:48:56 -05:00
} ;