2
Copyright (c) 2009 by Chad Nelson
3
Released under the MIT License.
4
See the provided LICENSE.TXT file for details.
7
#include "markdown.hpp"
8
#include "markdown-tokens.hpp"
13
#include <boost/regex.hpp>
14
#include <boost/lexical_cast.hpp>
15
#include <boost/algorithm/string/case_conv.hpp>
20
using boost::optional;
22
using markdown::TokenPtr;
23
using markdown::CTokenGroupIter;
28
std::string tagName, extra;
30
size_t lengthOfToken; // In original string
33
const std::string cHtmlTokenSource("<((/?)([a-zA-Z0-9]+)(?:( +[a-zA-Z0-9]+?(?: ?= ?(\"|').*?\\5))*? */? *))>");
34
const boost::regex cHtmlTokenExpression(cHtmlTokenSource),
35
cStartHtmlTokenExpression("^"+cHtmlTokenSource),
36
cOneHtmlTokenExpression("^"+cHtmlTokenSource+"$");
38
enum ParseHtmlTagFlags { cAlone, cStarts };
40
optional<HtmlTagInfo> parseHtmlTag(std::string::const_iterator begin,
41
std::string::const_iterator end, ParseHtmlTagFlags flags)
44
if (boost::regex_search(begin, end, m, (flags==cAlone ?
45
cOneHtmlTokenExpression : cStartHtmlTokenExpression)))
49
if (m[4].matched) r.extra=m[4];
50
r.isClosingTag=(m[2].length()>0);
51
r.lengthOfToken=m[0].length();
57
markdown::TokenGroup parseInlineHtmlText(const std::string& src) {
58
markdown::TokenGroup r;
59
std::string::const_iterator prev=src.begin(), end=src.end();
62
if (boost::regex_search(prev, end, m, cHtmlTokenExpression)) {
63
if (prev!=m[0].first) {
64
//cerr << " Non-tag (" << std::distance(prev, m[0].first) << "): " << std::string(prev, m[0].first) << endl;
65
r.push_back(TokenPtr(new markdown::token::InlineHtmlContents(std::string(prev, m[0].first))));
67
//cerr << " Tag: " << m[1] << endl;
68
r.push_back(TokenPtr(new markdown::token::HtmlTag(m[1])));
73
eol=std::string(prev, end);
74
//cerr << " Non-tag: " << eol << endl;
77
r.push_back(TokenPtr(new markdown::token::InlineHtmlContents(eol)));
84
bool isHtmlCommentStart(std::string::const_iterator begin,
85
std::string::const_iterator end)
87
// It can't be a single-line comment, those will already have been parsed
89
static const boost::regex cExpression("^<!--");
90
return boost::regex_search(begin, end, cExpression);
93
bool isHtmlCommentEnd(std::string::const_iterator begin,
94
std::string::const_iterator end)
96
static const boost::regex cExpression(".*-- *>$");
97
return boost::regex_match(begin, end, cExpression);
100
bool isBlankLine(const std::string& line) {
101
static const boost::regex cExpression(" {0,3}(<--(.*)-- *> *)* *");
102
return boost::regex_match(line, cExpression);
105
optional<TokenPtr> parseInlineHtml(CTokenGroupIter& i, CTokenGroupIter end) {
106
// Preconditions: Previous line was blank, or this is the first line.
108
const std::string& line(*(*i)->text());
110
bool tag=false, comment=false;
111
optional<HtmlTagInfo> tagInfo=parseHtmlTag(line.begin(), line.end(), cStarts);
112
if (tagInfo && markdown::token::isValidTag(tagInfo->tagName)>1) {
114
} else if (isHtmlCommentStart(line.begin(), line.end())) {
119
// Block continues until an HTML tag (alone) on a line followed by a
121
markdown::TokenGroup contents;
122
CTokenGroupIter firstLine=i, prevLine=i;
127
// We encode HTML tags so that their contents gets properly
128
// handled -- i.e. "<div style=">"/>" becomes <div style=">"/>
130
markdown::TokenGroup t=parseInlineHtmlText(*(*i)->text());
131
contents.splice(contents.end(), t);
132
} else contents.push_back(*i);
138
if (i!=end && (*i)->isBlankLine() && (*prevLine)->text()) {
139
if (prevLine==firstLine) {
142
const std::string& text(*(*prevLine)->text());
143
if (parseHtmlTag(text.begin(), text.end(), cAlone)) done=true;
146
} while (i!=end && !done);
148
if (lines>1 || markdown::token::isValidTag(tagInfo->tagName, true)>1) {
150
return TokenPtr(new markdown::token::InlineHtmlBlock(contents));
152
// Single-line HTML "blocks" whose initial tags are span-tags
153
// don't qualify as inline HTML.
157
} else if (comment) {
158
// Comment continues until a closing tag is found; at present, it
159
// also has to be the last thing on the line, and has to be
160
// immediately followed by a blank line too.
161
markdown::TokenGroup contents;
162
CTokenGroupIter firstLine=i, prevLine=i;
166
if ((*i)->text()) contents.push_back(TokenPtr(new markdown::token::InlineHtmlComment(*(*i)->text()+'\n')));
167
else contents.push_back(*i);
172
if (i!=end && (*i)->isBlankLine() && (*prevLine)->text()) {
173
if (prevLine==firstLine) {
176
const std::string& text(*(*prevLine)->text());
177
if (isHtmlCommentEnd(text.begin(), text.end())) done=true;
180
} while (i!=end && !done);
182
return TokenPtr(new markdown::token::InlineHtmlBlock(contents));
189
optional<std::string> isCodeBlockLine(CTokenGroupIter& i, CTokenGroupIter end) {
190
if ((*i)->isBlankLine()) {
191
// If we get here, we're already in a code block.
194
optional<std::string> r=isCodeBlockLine(i, end);
195
if (r) return std::string("\n"+*r);
198
} else if ((*i)->text() && (*i)->canContainMarkup()) {
199
const std::string& line(*(*i)->text());
200
if (line.length()>=4) {
201
std::string::const_iterator si=line.begin(), sie=si+4;
202
while (si!=sie && *si==' ') ++si;
205
return std::string(si, line.end());
212
optional<TokenPtr> parseCodeBlock(CTokenGroupIter& i, CTokenGroupIter end) {
213
if (!(*i)->isBlankLine()) {
214
optional<std::string> contents=isCodeBlockLine(i, end);
216
std::ostringstream out;
217
out << *contents << '\n';
219
contents=isCodeBlockLine(i, end);
220
if (contents) out << *contents << '\n';
223
return TokenPtr(new markdown::token::CodeBlock(out.str()));
231
size_t countQuoteLevel(const std::string& prefixString) {
233
for (std::string::const_iterator qi=prefixString.begin(),
234
qie=prefixString.end(); qi!=qie; ++qi)
239
optional<TokenPtr> parseBlockQuote(CTokenGroupIter& i, CTokenGroupIter end) {
240
static const boost::regex cBlockQuoteExpression("^((?: {0,3}>)+) (.*)$");
241
// Useful captures: 1=prefix, 2=content
243
if (!(*i)->isBlankLine() && (*i)->text() && (*i)->canContainMarkup()) {
244
const std::string& line(*(*i)->text());
246
if (boost::regex_match(line, m, cBlockQuoteExpression)) {
247
size_t quoteLevel=countQuoteLevel(m[1]);
248
boost::regex continuationExpression=boost::regex("^((?: {0,3}>){"+boost::lexical_cast<std::string>(quoteLevel)+"}) ?(.*)$");
250
markdown::TokenGroup subTokens;
251
subTokens.push_back(TokenPtr(new markdown::token::RawText(m[2])));
253
// The next line can be a continuation of this quote (with or
254
// without the prefix string) or a blank line. Blank lines are
255
// treated as part of this quote if the following line is a
256
// properly-prefixed quote line too, otherwise they terminate the
260
if ((*i)->isBlankLine()) {
261
CTokenGroupIter ii=i;
267
const std::string& line(*(*ii)->text());
268
if (boost::regex_match(line, m, continuationExpression)) {
269
if (m[1].matched && m[1].length()>0) {
271
subTokens.push_back(TokenPtr(new markdown::token::BlankLine));
272
subTokens.push_back(TokenPtr(new markdown::token::RawText(m[2])));
277
const std::string& line(*(*i)->text());
278
if (boost::regex_match(line, m, continuationExpression)) {
279
assert(m[2].matched);
280
if (!isBlankLine(m[2])) subTokens.push_back(TokenPtr(new markdown::token::RawText(m[2])));
281
else subTokens.push_back(TokenPtr(new markdown::token::BlankLine(m[2])));
287
return TokenPtr(new markdown::token::BlockQuote(subTokens));
293
optional<TokenPtr> parseListBlock(CTokenGroupIter& i, CTokenGroupIter end, bool sub=false) {
294
static const boost::regex cUnorderedListExpression("^( *)([*+-]) +([^*-].*)$");
295
static const boost::regex cOrderedListExpression("^( *)([0-9]+)\\. +(.*)$");
297
enum ListType { cNone, cUnordered, cOrdered };
299
if (!(*i)->isBlankLine() && (*i)->text() && (*i)->canContainMarkup()) {
300
boost::regex nextItemExpression, startSublistExpression;
303
const std::string& line(*(*i)->text());
305
//cerr << "IsList? " << line << endl;
307
markdown::TokenGroup subTokens, subItemTokens;
310
if (boost::regex_match(line, m, cUnorderedListExpression)) {
311
indent=m[1].length();
312
if (sub || indent<4) {
314
char startChar=*m[2].first;
315
subItemTokens.push_back(TokenPtr(new markdown::token::RawText(m[3])));
317
std::ostringstream next;
318
next << "^" << std::string(indent, ' ') << "\\" << startChar << " +([^*-].*)$";
319
nextItemExpression=next.str();
321
} else if (boost::regex_match(line, m, cOrderedListExpression)) {
322
indent=m[1].length();
323
if (sub || indent<4) {
325
subItemTokens.push_back(TokenPtr(new markdown::token::RawText(m[3])));
327
std::ostringstream next;
328
next << "^" << std::string(indent, ' ') << "[0-9]+\\. +(.*)$";
329
nextItemExpression=next.str();
334
CTokenGroupIter originalI=i;
336
std::ostringstream sub;
337
sub << "^" << std::string(indent, ' ') << " +(([*+-])|([0-9]+\\.)) +.*$";
338
startSublistExpression=sub.str();
340
// There are several options for the next line. It's another item in
341
// this list (in which case this one is done); it's a continuation
342
// of this line (collect it and keep going); it's the first item in
343
// a sub-list (call this function recursively to collect it), it's
344
// the next item in the parent list (this one is ended); or it's
347
// A blank line requires looking ahead. If the next line is an item
348
// for this list, then switch this list into paragraph-items mode
349
// and continue processing. If it's indented by four or more spaces
350
// (more than the list itself), then it's another continuation of
351
// the current item. Otherwise it's either a new paragraph (and this
352
// list is ended) or the beginning of a sub-list.
353
static const boost::regex cContinuedItemExpression("^ *([^ ].*)$");
355
boost::regex continuedAfterBlankLineExpression("^ {"+
356
boost::lexical_cast<std::string>(indent+4)+"}([^ ].*)$");
357
boost::regex codeBlockAfterBlankLineExpression("^ {"+
358
boost::lexical_cast<std::string>(indent+8)+"}(.*)$");
360
enum NextItemType { cUnknown, cEndOfList, cAnotherItem };
361
NextItemType nextItem=cUnknown;
362
bool setParagraphMode=false;
366
if ((*i)->isBlankLine()) {
367
CTokenGroupIter ii=i;
372
} else if ((*ii)->text()) {
373
const std::string& line(*(*ii)->text());
374
if (boost::regex_match(line, startSublistExpression)) {
375
setParagraphMode=true;
378
optional<TokenPtr> p=parseListBlock(i, end, true);
380
subItemTokens.push_back(*p);
382
} else if (boost::regex_match(line, m, nextItemExpression)) {
383
setParagraphMode=true;
385
nextItem=cAnotherItem;
386
} else if (boost::regex_match(line, m, continuedAfterBlankLineExpression)) {
387
assert(m[1].matched);
388
subItemTokens.push_back(TokenPtr(new markdown::token::BlankLine()));
389
subItemTokens.push_back(TokenPtr(new markdown::token::RawText(m[1])));
392
} else if (boost::regex_match(line, m, codeBlockAfterBlankLineExpression)) {
393
setParagraphMode=true;
395
assert(m[1].matched);
396
subItemTokens.push_back(TokenPtr(new markdown::token::BlankLine()));
398
std::string codeBlock=m[1]+'\n';
401
if ((*ii)->isBlankLine()) {
402
CTokenGroupIter iii=ii;
404
const std::string& nextLine(*(*iii)->text());
405
if (boost::regex_match(nextLine, m, codeBlockAfterBlankLineExpression)) {
406
codeBlock+='\n'+m[1]+'\n';
409
} else if ((*ii)->text()) {
410
const std::string& line(*(*ii)->text());
411
if (boost::regex_match(line, m, codeBlockAfterBlankLineExpression)) {
412
codeBlock+=m[1]+'\n';
418
subItemTokens.push_back(TokenPtr(new markdown::token::CodeBlock(codeBlock)));
425
} else if ((*i)->text()) {
426
const std::string& line(*(*i)->text());
427
if (boost::regex_match(line, startSublistExpression)) {
429
optional<TokenPtr> p=parseListBlock(i, end, true);
431
subItemTokens.push_back(*p);
433
} else if (boost::regex_match(line, m, nextItemExpression)) {
434
nextItem=cAnotherItem;
436
if (boost::regex_match(line, m, cUnorderedListExpression)
437
|| boost::regex_match(line, m, cOrderedListExpression))
439
// Belongs to the parent list
442
boost::regex_match(line, m, cContinuedItemExpression);
443
assert(m[1].matched);
444
subItemTokens.push_back(TokenPtr(new markdown::token::RawText(m[1])));
449
} else nextItem=cEndOfList;
451
if (!subItemTokens.empty()) {
452
subTokens.push_back(TokenPtr(new markdown::token::ListItem(subItemTokens)));
453
subItemTokens.clear();
456
assert(nextItem!=cUnknown);
457
if (nextItem==cAnotherItem) {
458
subItemTokens.push_back(TokenPtr(new markdown::token::RawText(m[1])));
461
} else { // nextItem==cEndOfList
466
// In case we hit the end with an unterminated item...
467
if (!subItemTokens.empty()) {
468
subTokens.push_back(TokenPtr(new markdown::token::ListItem(subItemTokens)));
469
subItemTokens.clear();
472
if (itemCount>1 || indent!=0) {
473
if (type==cUnordered) {
474
return TokenPtr(new markdown::token::UnorderedList(subTokens, setParagraphMode));
476
return TokenPtr(new markdown::token::OrderedList(subTokens, setParagraphMode));
479
// It looked like a list, but turned out to be a false alarm.
488
bool parseReference(CTokenGroupIter& i, CTokenGroupIter end, markdown::LinkIds &idTable) {
490
static const boost::regex cReference("^ {0,3}\\[(.+)\\]: +<?([^ >]+)>?(?: *(?:('|\")(.*)\\3)|(?:\\((.*)\\)))?$");
491
// Useful captures: 1=id, 2=url, 4/5=title
493
const std::string& line1(*(*i)->text());
495
if (boost::regex_match(line1, m, cReference)) {
496
std::string id(m[1]), url(m[2]), title;
497
if (m[4].matched) title=m[4];
498
else if (m[5].matched) title=m[5];
500
CTokenGroupIter ii=i;
502
if (ii!=end && (*ii)->text()) {
503
// It could be on the next line
504
static const boost::regex cSeparateTitle("^ *(?:(?:('|\")(.*)\\1)|(?:\\((.*)\\))) *$");
505
// Useful Captures: 2/3=title
507
const std::string& line2(*(*ii)->text());
508
if (boost::regex_match(line2, m, cSeparateTitle)) {
510
title=(m[2].matched ? m[2] : m[3]);
515
idTable.add(id, url, title);
522
void flushParagraph(std::string& paragraphText, markdown::TokenGroup&
523
paragraphTokens, markdown::TokenGroup& finalTokens, bool noParagraphs)
525
if (!paragraphText.empty()) {
526
paragraphTokens.push_back(TokenPtr(new markdown::token::RawText(paragraphText)));
527
paragraphText.clear();
530
if (!paragraphTokens.empty()) {
532
if (paragraphTokens.size()>1) {
533
finalTokens.push_back(TokenPtr(new markdown::token::Container(paragraphTokens)));
534
} else finalTokens.push_back(*paragraphTokens.begin());
535
} else finalTokens.push_back(TokenPtr(new markdown::token::Paragraph(paragraphTokens)));
536
paragraphTokens.clear();
540
optional<TokenPtr> parseHeader(CTokenGroupIter& i, CTokenGroupIter end) {
541
if (!(*i)->isBlankLine() && (*i)->text() && (*i)->canContainMarkup()) {
543
static const boost::regex cHashHeaders("^(#{1,6}) +(.*?) *#*$");
544
const std::string& line=*(*i)->text();
546
if (boost::regex_match(line, m, cHashHeaders))
547
return TokenPtr(new markdown::token::Header(m[1].length(), m[2]));
550
CTokenGroupIter ii=i;
552
if (ii!=end && !(*ii)->isBlankLine() && (*ii)->text() && (*ii)->canContainMarkup()) {
553
static const boost::regex cUnderlinedHeaders("^([-=])\\1*$");
554
const std::string& line=*(*ii)->text();
555
if (boost::regex_match(line, m, cUnderlinedHeaders)) {
556
char typeChar=std::string(m[1])[0];
557
TokenPtr p=TokenPtr(new markdown::token::Header((typeChar=='='
558
? 1 : 2), *(*i)->text()));
567
optional<TokenPtr> parseHorizontalRule(CTokenGroupIter& i, CTokenGroupIter end) {
568
if (!(*i)->isBlankLine() && (*i)->text() && (*i)->canContainMarkup()) {
569
static const boost::regex cHorizontalRules("^ {0,3}((?:-|\\*|_) *){3,}$");
570
const std::string& line=*(*i)->text();
571
if (boost::regex_match(line, cHorizontalRules)) {
572
return TokenPtr(new markdown::token::HtmlTag("hr/"));
584
optional<LinkIds::Target> LinkIds::find(const std::string& id) const {
585
Table::const_iterator i=mTable.find(_scrubKey(id));
586
if (i!=mTable.end()) return i->second;
590
void LinkIds::add(const std::string& id, const std::string& url, const
593
mTable.insert(std::make_pair(_scrubKey(id), Target(url, title)));
596
std::string LinkIds::_scrubKey(std::string str) {
597
boost::algorithm::to_lower(str);
603
const size_t Document::cSpacesPerInitialTab=4; // Required by Markdown format
604
const size_t Document::cDefaultSpacesPerTab=cSpacesPerInitialTab;
606
Document::Document(size_t spacesPerTab): cSpacesPerTab(spacesPerTab),
607
mTokenContainer(new token::Container), mIdTable(new LinkIds),
610
// This space deliberately blank ;-)
613
Document::Document(std::istream& in, size_t spacesPerTab):
614
cSpacesPerTab(spacesPerTab), mTokenContainer(new token::Container),
615
mIdTable(new LinkIds), mProcessed(false)
620
Document::~Document() {
624
bool Document::read(const std::string& src) {
625
std::istringstream in(src);
629
bool Document::_getline(std::istream& in, std::string& line) {
630
// Handles \n, \r, and \r\n (and even \n\r) on any system. Also does tab-
631
// expansion, since this is the most efficient place for it.
634
bool initialWhitespace=true;
638
if ((in.get(c)) && c!='\n') in.unget();
640
} else if (c=='\n') {
641
if ((in.get(c)) && c!='\r') in.unget();
643
} else if (c=='\t') {
644
size_t convert=(initialWhitespace ? cSpacesPerInitialTab :
646
line+=std::string(convert-(line.length()%convert), ' ');
649
if (c!=' ') initialWhitespace=false;
652
return !line.empty();
655
bool Document::read(std::istream& in) {
656
if (mProcessed) return false;
658
token::Container *tokens=dynamic_cast<token::Container*>(mTokenContainer.get());
663
while (_getline(in, line)) {
664
if (isBlankLine(line)) {
665
tgt.push_back(TokenPtr(new token::BlankLine(line)));
667
tgt.push_back(TokenPtr(new token::RawText(line)));
670
tokens->appendSubtokens(tgt);
675
void Document::write(std::ostream& out) {
677
mTokenContainer->writeAsHtml(out);
680
void Document::writeTokens(std::ostream& out) {
682
mTokenContainer->writeToken(0, out);
685
std::string Document::asHtml() {
689
std::stringstream ss( std::ios_base::in | std::ios_base::out );
691
mTokenContainer->writeAsHtml(ss);
692
int size = ss.tellp();
695
char *buffer = new char[ size + 1 ];
696
ss.read( buffer, size );
698
return std::string( buffer );
701
void Document::_process() {
703
_mergeMultilineHtmlTags();
704
_processInlineHtmlAndReferences();
705
_processBlocksItems(mTokenContainer);
706
_processParagraphLines(mTokenContainer);
707
mTokenContainer->processSpanElements(*mIdTable);
712
void Document::_mergeMultilineHtmlTags() {
713
static const boost::regex cHtmlTokenStart("<((/?)([a-zA-Z0-9]+)(?:( +[a-zA-Z0-9]+?(?: ?= ?(\"|').*?\\5))*? */? *))$");
714
static const boost::regex cHtmlTokenEnd("^ *((?:( +[a-zA-Z0-9]+?(?: ?= ?(\"|').*?\\3))*? */? *))>");
716
TokenGroup processed;
718
token::Container *tokens=dynamic_cast<token::Container*>(mTokenContainer.get());
721
for (TokenGroup::const_iterator i=tokens->subTokens().begin(),
722
ie=tokens->subTokens().end(); i!=ie; ++i)
724
if ((*i)->text() && boost::regex_match(*(*i)->text(), cHtmlTokenStart)) {
725
TokenGroup::const_iterator i2=i;
727
if (i2!=tokens->subTokens().end() && (*i2)->text() &&
728
boost::regex_match(*(*i2)->text(), cHtmlTokenEnd))
730
processed.push_back(TokenPtr(new markdown::token::RawText(*(*i)->text()+' '+*(*i2)->text())));
735
processed.push_back(*i);
737
tokens->swapSubtokens(processed);
740
void Document::_processInlineHtmlAndReferences() {
741
TokenGroup processed;
743
token::Container *tokens=dynamic_cast<token::Container*>(mTokenContainer.get());
746
for (TokenGroup::const_iterator ii=tokens->subTokens().begin(),
747
iie=tokens->subTokens().end(); ii!=iie; ++ii)
750
if (processed.empty() || processed.back()->isBlankLine()) {
751
optional<TokenPtr> inlineHtml=parseInlineHtml(ii, iie);
753
processed.push_back(*inlineHtml);
759
if (parseReference(ii, iie, *mIdTable)) {
764
// If it gets down here, just store it in its current (raw text)
765
// form. We'll group the raw text lines into paragraphs in a
766
// later pass, since we can't easily tell where paragraphs
769
processed.push_back(*ii);
771
tokens->swapSubtokens(processed);
774
void Document::_processBlocksItems(TokenPtr inTokenContainer) {
775
if (!inTokenContainer->isContainer()) return;
777
token::Container *tokens=dynamic_cast<token::Container*>(inTokenContainer.get());
780
TokenGroup processed;
782
for (TokenGroup::const_iterator ii=tokens->subTokens().begin(),
783
iie=tokens->subTokens().end(); ii!=iie; ++ii)
786
optional<TokenPtr> subitem;
787
if (!subitem) subitem=parseHeader(ii, iie);
788
if (!subitem) subitem=parseHorizontalRule(ii, iie);
789
if (!subitem) subitem=parseListBlock(ii, iie);
790
if (!subitem) subitem=parseBlockQuote(ii, iie);
791
if (!subitem) subitem=parseCodeBlock(ii, iie);
794
_processBlocksItems(*subitem);
795
processed.push_back(*subitem);
798
} else processed.push_back(*ii);
799
} else if ((*ii)->isContainer()) {
800
_processBlocksItems(*ii);
801
processed.push_back(*ii);
804
tokens->swapSubtokens(processed);
807
void Document::_processParagraphLines(TokenPtr inTokenContainer) {
808
token::Container *tokens=dynamic_cast<token::Container*>(inTokenContainer.get());
811
bool noPara=tokens->inhibitParagraphs();
812
for (TokenGroup::const_iterator ii=tokens->subTokens().begin(),
813
iie=tokens->subTokens().end(); ii!=iie; ++ii)
814
if ((*ii)->isContainer()) _processParagraphLines(*ii);
816
TokenGroup processed;
817
std::string paragraphText;
818
TokenGroup paragraphTokens;
819
for (TokenGroup::const_iterator ii=tokens->subTokens().begin(),
820
iie=tokens->subTokens().end(); ii!=iie; ++ii)
822
if ((*ii)->text() && (*ii)->canContainMarkup() && !(*ii)->inhibitParagraphs()) {
823
static const boost::regex cExpression("^(.*) $");
824
if (!paragraphText.empty()) paragraphText+=" ";
827
if (boost::regex_match(*(*ii)->text(), m, cExpression)) {
828
paragraphText += m[1];
829
flushParagraph(paragraphText, paragraphTokens, processed, noPara);
830
processed.push_back(TokenPtr(new markdown::token::HtmlTag("br/")));
831
} else paragraphText += *(*ii)->text();
833
flushParagraph(paragraphText, paragraphTokens, processed, noPara);
834
processed.push_back(*ii);
838
// Make sure the last paragraph is properly flushed too.
839
flushParagraph(paragraphText, paragraphTokens, processed, noPara);
841
tokens->swapSubtokens(processed);
844
} // namespace markdown