{-# OPTIONS_GHC -fno-warn-deprecations #-}{-
Copyright (C) 2006-2010 John MacFarlane <jgm@berkeley.edu>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-}{- |
Module : Text.Pandoc.Writers.HTML
Copyright : Copyright (C) 2006-2010 John MacFarlane
License : GNU GPL, version 2 or above
Maintainer : John MacFarlane <jgm@berkeley.edu>
Stability : alpha
Portability : portable
Conversion of 'Pandoc' documents to HTML.
-}moduleText.Pandoc.Writers.HTML(writeHtml,writeHtmlString)whereimportText.Pandoc.DefinitionimportText.Pandoc.CharacterReferences(decodeCharacterReferences)importText.Pandoc.SharedimportText.Pandoc.TemplatesimportText.Pandoc.Readers.TeXMathimportText.Pandoc.Highlighting(highlightHtml,defaultHighlightingCss)importText.Pandoc.XML(stripTags,escapeStringForXML)importNetwork.HTTP(urlEncode)importNumeric(showHex)importData.Char(ord,toLower)importData.List(isPrefixOf,intersperse)importData.Maybe(catMaybes)importControl.Monad.StateimportText.XHtml.Transitionalhiding(stringToHtml,unordList,ordList)importqualifiedText.XHtml.TransitionalasXHtmlimportText.TeXMathimportText.XML.Light.OutputimportSystem.FilePath(takeExtension)dataWriterState=WriterState{stNotes::[Html]-- ^ List of notes,stMath::Bool-- ^ Math is used in document,stHighlighting::Bool-- ^ Syntax highlighting is used,stSecNum::[Int]-- ^ Number of current section}derivingShowdefaultWriterState::WriterStatedefaultWriterState=WriterState{stNotes=[],stMath=False,stHighlighting=False,stSecNum=[]}-- Helpers to render HTML with the appropriate function.-- | Modified version of Text.XHtml's stringToHtml.-- Use unicode characters wherever possible.stringToHtml::WriterOptions->String->HtmlstringToHtmlopts=ifwriterAsciioptsthenXHtml.stringToHtmlelseprimHtml.escapeStringForXML-- | Hard linebreak.nl::WriterOptions->Htmlnlopts=ifwriterWrapTextoptsthenprimHtml"\n"elsenoHtml-- | Convert Pandoc document to Html string.writeHtmlString::WriterOptions->Pandoc->StringwriteHtmlStringoptsd=let(tit,auths,date,toc,body',newvars)=evalState(pandocToHtmloptsd)defaultWriterStateinifwriterStandaloneoptstheninTemplateoptstitauthsdatetocbody'newvarselsedropWhile(=='\n')$showHtmlFragmentbody'-- | Convert Pandoc document to Html structure.writeHtml::WriterOptions->Pandoc->HtmlwriteHtmloptsd=let(tit,auths,date,toc,body',newvars)=evalState(pandocToHtmloptsd)defaultWriterStateinifwriterStandaloneoptstheninTemplateoptstitauthsdatetocbody'newvarselsebody'-- result is (title, authors, date, toc, body, new variables)pandocToHtml::WriterOptions->Pandoc->StateWriterState(Html,[Html],Html,MaybeHtml,Html,[(String,String)])pandocToHtmlopts(Pandoc(Metatitle'authors'date')blocks)=doletstandalone=writerStandaloneoptstit<-ifstandalonetheninlineListToHtmloptstitle'elsereturnnoHtmlauths<-ifstandalonethenmapM(inlineListToHtmlopts)authors'elsereturn[]date<-ifstandalonetheninlineListToHtmloptsdate'elsereturnnoHtmlletsects=hierarchicalize$ifwriterSlideVariantopts==NoSlidesthenblockselsecaseblocksof(Header1_:_)->blocks_->letisL1(Header1_)=TrueisL1_=False(preBlocks,rest)=breakisL1blocksin(RawBlock"html""<div class=\"slide\">":preBlocks)++(RawBlock"html""</div>":rest)toc<-ifwriterTableOfContentsoptsthentableOfContentsoptssectselsereturnNothingblocks'<-liftM(toHtmlFromList.intersperse(nlopts))$mapM(elementToHtmlopts)sectsst<-getletnotes=reverse(stNotesst)letthebody=blocks'+++footnoteSectionoptsnotesletmath=ifstMathstthencasewriterHTMLMathMethodoptsofLaTeXMathML(Justurl)->script![srcurl,thetype"text/javascript"]$noHtmlMathML(Justurl)->script![srcurl,thetype"text/javascript"]$noHtmlMathJaxurl->script![srcurl,thetype"text/javascript"]$noHtmlJsMath(Justurl)->script![srcurl,thetype"text/javascript"]$noHtml_->caselookup"mathml-script"(writerVariablesopts)ofJusts->script![thetype"text/javascript"]<<primHtml("/*<![CDATA[*/\n"++s++"/*]]>*/\n")Nothing->noHtmlelsenoHtmlletnewvars=[("highlighting-css",defaultHighlightingCss)|stHighlightingst]++[("math",showHtmlFragmentmath)|stMathst]return(tit,auths,date,toc,thebody,newvars)inTemplate::TemplateTargeta=>WriterOptions->Html->[Html]->Html->MaybeHtml->Html->[(String,String)]->ainTemplateoptstitauthsdatetocbody'newvars=letrenderedTit=showHtmlFragmenttittopTitle'=stripTagsrenderedTitauthors=map(stripTags.showHtmlFragment)authsdate'=stripTags$showHtmlFragmentdatevariables=writerVariablesopts++newvarscontext=variables++[("body",dropWhile(=='\n')$showHtmlFragmentbody'),("pagetitle",topTitle'),("title",dropWhile(=='\n')$showHtmlFragmenttit),("date",date'),("idprefix",writerIdentifierPrefixopts),("slidy-url","http://www.w3.org/Talks/Tools/Slidy2"),("s5-url","ui/default")]++[("html5","true")|writerHtml5opts]++(casetocofJustt->[("toc",showHtmlFragmentt)]Nothing->[])++[("author",a)|a<-authors]inrenderTemplatecontext$writerTemplateopts-- | Like Text.XHtml's identifier, but adds the writerIdentifierPrefixprefixedId::WriterOptions->String->HtmlAttrprefixedIdoptss=identifier$writerIdentifierPrefixopts++s-- | Replacement for Text.XHtml's unordList.unordList::WriterOptions->([Html]->Html)unordListoptsitems=ulist<<toListItemsoptsitems-- | Replacement for Text.XHtml's ordList.ordList::WriterOptions->([Html]->Html)ordListoptsitems=olist<<toListItemsoptsitems-- | Construct table of contents from list of elements.tableOfContents::WriterOptions->[Element]->StateWriterState(MaybeHtml)tableOfContents_[]=returnNothingtableOfContentsoptssects=doletopts'=opts{writerIgnoreNotes=True}contents<-mapM(elementToListItemopts')sectslettocList=catMaybescontentsreturn$ifnulltocListthenNothingelseJust$unordListoptstocList-- | Convert section number to stringshowSecNum::[Int]->StringshowSecNum=concat.intersperse".".mapshow-- | Converts an Element to a list item for a table of contents,-- retrieving the appropriate identifier from state.elementToListItem::WriterOptions->Element->StateWriterState(MaybeHtml)elementToListItem_(Blk_)=returnNothingelementToListItemopts(Sec_numid'headerTextsubsecs)=doletsectnum=ifwriterNumberSectionsoptsthen(thespan![theclass"toc-section-number"]<<showSecNumnum)+++stringToHtmlopts" "elsenoHtmltxt<-liftM(sectnum+++)$inlineListToHtmloptsheaderTextsubHeads<-mapM(elementToListItemopts)subsecs>>=return.catMaybesletsubList=ifnullsubHeadsthennoHtmlelseunordListoptssubHeadsreturn$Just$(anchor![href("#"++writerIdentifierPrefixopts++id')]$txt)+++subList-- | Convert an Element to Html.elementToHtml::WriterOptions->Element->StateWriterStateHtmlelementToHtmlopts(BlkHorizontalRule)|writerSlideVariantopts/=NoSlides=return$primHtml"</div>"+++nlopts+++primHtml"<div class=\"slide\">"elementToHtmlopts(Blkblock)=blockToHtmloptsblockelementToHtmlopts(Seclevelnumid'title'elements)=domodify$\st->st{stSecNum=num}-- update section numberheader'<-blockToHtmlopts(Headerleveltitle')innerContents<-mapM(elementToHtmlopts)elementsletheader''=header'![prefixedIdoptsid'|not(writerStrictMarkdownopts||writerSectionDivsopts||writerSlideVariantopts==S5Slides)]letstuff=header'':innerContentsletslide=writerSlideVariantopts/=NoSlides&&level==1letstuff'=ifslidethen[thediv![theclass"slide"]<<(nlopts:intersperse(nlopts)stuff++[nlopts])]elseintersperse(nlopts)stuffletinNlx=nlopts:x++[nlopts]return$ifwriterSectionDivsoptsthenifwriterHtml5optsthentag"section"![prefixedIdoptsid']<<inNlstuff'elsethediv![prefixedIdoptsid']<<inNlstuff'elsetoHtmlFromListstuff'-- | Convert list of Note blocks to a footnote <div>.-- Assumes notes are sorted.footnoteSection::WriterOptions->[Html]->HtmlfootnoteSectionoptsnotes=ifnullnotesthennoHtmlelsenlopts+++(thediv![theclass"footnotes"]$nlopts+++hr+++nlopts+++(olist<<(notes++[nlopts]))+++nlopts)-- | Parse a mailto link; return Just (name, domain) or Nothing.parseMailto::String->Maybe(String,String)parseMailto('m':'a':'i':'l':'t':'o':':':addr)=let(name',rest)=span(/='@')addrdomain=drop1restinJust(name',domain)parseMailto_=Nothing-- | Obfuscate a "mailto:" link.obfuscateLink::WriterOptions->String->String->HtmlobfuscateLinkoptstxts|writerEmailObfuscationopts==NoObfuscation=anchor![hrefs]<<txtobfuscateLinkoptstxts=letmeth=writerEmailObfuscationoptss'=maptoLowersincaseparseMailtos'of(Just(name',domain))->letdomain'=substitute"."" dot "domainat'=obfuscateChar'@'(linkText,altText)=iftxt==drop7s'-- autolinkthen("'<code>'+e+'</code>'",name'++" at "++domain')else("'"++txt++"'",txt++" ("++name'++" at "++domain'++")")incasemethofReferenceObfuscation->-- need to use primHtml or &'s are escaped to &amp; in URLprimHtml$"<a href=\""++(obfuscateStrings')++"\">"++(obfuscateStringtxt)++"</a>"JavascriptObfuscation->(script![thetype"text/javascript"]$primHtml("\n<!--\nh='"++obfuscateStringdomain++"';a='"++at'++"';n='"++obfuscateStringname'++"';e=n+a+h;\n"++"document.write('<a h'+'ref'+'=\"ma'+'ilto'+':'+e+'\">'+"++linkText++"+'<\\/'+'a'+'>');\n// -->\n"))+++noscript(primHtml$obfuscateStringaltText)_->error$"Unknown obfuscation method: "++showmeth_->anchor![hrefs]$stringToHtmloptstxt-- malformed email-- | Obfuscate character as entity.obfuscateChar::Char->StringobfuscateCharchar=letnum=ordcharnumstr=ifevennumthenshownumelse"x"++showHexnum""in"&#"++numstr++";"-- | Obfuscate string using entities.obfuscateString::String->StringobfuscateString=concatMapobfuscateChar.decodeCharacterReferencesattrsToHtml::WriterOptions->Attr->[HtmlAttr]attrsToHtmlopts(id',classes',keyvals)=[theclass(unwordsclasses')|not(nullclasses')]++[prefixedIdoptsid'|not(nullid')]++map(\(x,y)->strAttrxy)keyvalsimageExts::[String]imageExts=["art","bmp","cdr","cdt","cpt","cr2","crw","djvu","erf","gif","ico","ief","jng","jpg","jpeg","nef","orf","pat","pbm","pcx","pgm","png","pnm","ppm","psd","ras","rgb","svg","tiff","wbmp","xbm","xpm","xwd"]treatAsImage::FilePath->BooltreatAsImagefp=letext=maptoLower$drop1$takeExtensionfpinnullext||ext`elem`imageExts-- | Convert Pandoc block element to HTML.blockToHtml::WriterOptions->Block->StateWriterStateHtmlblockToHtml_Null=returnnoHtmlblockToHtmlopts(Plainlst)=inlineListToHtmloptslstblockToHtmlopts(Para[Imagetxt(s,tit)])=doimg<-inlineToHtmlopts(Imagetxt(s,tit))capt<-inlineListToHtmloptstxtreturn$ifwriterHtml5optsthentag"figure"<<[nlopts,img,tag"figcaption"<<capt,nlopts]elsethediv![theclass"figure"]<<[nlopts,img,paragraph![theclass"caption"]<<capt,nlopts]blockToHtmlopts(Paralst)=docontents<-inlineListToHtmloptslstreturn$paragraphcontentsblockToHtml_(RawBlock"html"str)=return$primHtmlstrblockToHtml_(RawBlock__)=returnnoHtmlblockToHtml_(HorizontalRule)=returnhrblockToHtmlopts(CodeBlock(id',classes,keyvals)rawCode)=doletclasses'=ifwriterLiterateHaskelloptsthenclasseselsefilter(/="literate")classescasehighlightHtmlFalse(id',classes',keyvals)rawCodeofLeft_->-- change leading newlines into <br /> tags, because some-- browsers ignore leading newlines in pre blockslet(leadingBreaks,rawCode')=span(=='\n')rawCodeattrs=attrsToHtmlopts(id',classes',keyvals)addBird=if"literate"`elem`classes'thenunlines.map("> "++).lineselseunlines.linesinreturn$pre!attrs$thecode<<(replicate(lengthleadingBreaks)br+++[stringToHtmlopts$addBirdrawCode'])Righth->modify(\st->st{stHighlighting=True})>>returnhblockToHtmlopts(BlockQuoteblocks)=-- in S5, treat list in blockquote specially-- if default is incremental, make it nonincremental;-- otherwise incrementalifwriterSlideVariantopts/=NoSlidesthenletinc=not(writerIncrementalopts)incaseblocksof[BulletListlst]->blockToHtml(opts{writerIncremental=inc})(BulletListlst)[OrderedListattribslst]->blockToHtml(opts{writerIncremental=inc})(OrderedListattribslst)_->docontents<-blockListToHtmloptsblocksreturn$blockquote(nlopts+++contents+++nlopts)elsedocontents<-blockListToHtmloptsblocksreturn$blockquote(nlopts+++contents+++nlopts)blockToHtmlopts(Headerlevellst)=docontents<-inlineListToHtmloptslstsecnum<-liftMstSecNumgetletcontents'=ifwriterNumberSectionsoptsthen(thespan![theclass"header-section-number"]<<showSecNumsecnum)+++stringToHtmlopts" "+++contentselsecontentsletcontents''=ifwriterTableOfContentsoptsthenanchor![href$"#"++writerIdentifierPrefixopts++"TOC"]$contents'elsecontents'return$(caselevelof1->h1contents''2->h2contents''3->h3contents''4->h4contents''5->h5contents''6->h6contents''_->paragraphcontents'')blockToHtmlopts(BulletListlst)=docontents<-mapM(blockListToHtmlopts)lstletattribs=ifwriterIncrementaloptsthen[theclass"incremental"]else[]return$(unordListoptscontents)!attribsblockToHtmlopts(OrderedList(startnum,numstyle,_)lst)=docontents<-mapM(blockListToHtmlopts)lstletnumstyle'=camelCaseToHyphenated$shownumstyleletattribs=(ifwriterIncrementaloptsthen[theclass"incremental"]else[])++(ifstartnum/=1then[startstartnum]else[])++(ifnumstyle/=DefaultStylethenifwriterHtml5optsthen[strAttr"type"$casenumstyleofDecimal->"1"LowerAlpha->"a"UpperAlpha->"A"LowerRoman->"i"UpperRoman->"I"_->"1"]else[thestyle$"list-style-type: "++numstyle']else[])return$(ordListoptscontents)!attribsblockToHtmlopts(DefinitionListlst)=docontents<-mapM(\(term,defs)->doterm'<-liftM(dterm<<)$inlineListToHtmloptstermdefs'<-mapM((liftM(\x->ddef<<(x+++nlopts))).blockListToHtmlopts)defsreturn$nlopts:term':nlopts:defs')lstletattribs=ifwriterIncrementaloptsthen[theclass"incremental"]else[]return$dlist!attribs<<(concatcontents+++nlopts)blockToHtmlopts(Tablecaptalignswidthsheadersrows')=docaptionDoc<-ifnullcaptthenreturnnoHtmlelsedocs<-inlineListToHtmloptscaptreturn$captioncs+++nloptsletpercentw=show(truncate(100*w)::Integer)++"%"letwidthAttrsw=ifwriterHtml5optsthen[thestyle$"width: "++percentw]else[width$percentw]letcoltags=ifall(==0.0)widthsthennoHtmlelseconcatHtml$map(\w->(col!(widthAttrsw))noHtml+++nlopts)widthshead'<-ifallnullheadersthenreturnnoHtmlelsedocontents<-tableRowToHtmloptsaligns0headersreturn$thead<<(nlopts+++contents)+++nloptsbody'<-liftM(\x->tbody<<(nlopts+++x))$zipWithM(tableRowToHtmloptsaligns)[1..]rows'return$table$nlopts+++captionDoc+++coltags+++head'+++body'+++nloptstableRowToHtml::WriterOptions->[Alignment]->Int->[[Block]]->StateWriterStateHtmltableRowToHtmloptsalignsrownumcols'=doletmkcell=ifrownum==0thenthelsetdletrowclass=caserownumof0->"header"x|x`rem`2==1->"odd"_->"even"cols''<-sequence$zipWith(\alignmentitem->tableItemToHtmloptsmkcellalignmentitem)alignscols'return$(tr![theclassrowclass]$nlopts+++toHtmlFromListcols'')+++nloptsalignmentToString::Alignment->[Char]alignmentToStringalignment=casealignmentofAlignLeft->"left"AlignRight->"right"AlignCenter->"center"AlignDefault->"left"tableItemToHtml::WriterOptions->(Html->Html)->Alignment->[Block]->StateWriterStateHtmltableItemToHtmloptstag'align'item=docontents<-blockListToHtmloptsitemletalignAttrs=ifwriterHtml5optsthen[thestyle$"align: "++alignmentToStringalign']else[align$alignmentToStringalign']return$(tag'!alignAttrs)contents+++nloptstoListItems::WriterOptions->[Html]->[Html]toListItemsoptsitems=map(toListItemopts)items++[nlopts]toListItem::WriterOptions->Html->HtmltoListItemoptsitem=nlopts+++liitemblockListToHtml::WriterOptions->[Block]->StateWriterStateHtmlblockListToHtmloptslst=mapM(blockToHtmlopts)lst>>=return.toHtmlFromList.intersperse(nlopts)-- | Convert list of Pandoc inline elements to HTML.inlineListToHtml::WriterOptions->[Inline]->StateWriterStateHtmlinlineListToHtmloptslst=mapM(inlineToHtmlopts)lst>>=return.toHtmlFromList-- | Convert Pandoc inline element to HTML.inlineToHtml::WriterOptions->Inline->StateWriterStateHtmlinlineToHtmloptsinline=caseinlineof(Strstr)->return$stringToHtmloptsstr(Space)->return$stringToHtmlopts" "(LineBreak)->returnbr(EmDash)->return$stringToHtmlopts"—"(EnDash)->return$stringToHtmlopts"–"(Ellipses)->return$stringToHtmlopts"…"(Apostrophe)->return$stringToHtmlopts"’"(Emphlst)->inlineListToHtmloptslst>>=return.emphasize(Stronglst)->inlineListToHtmloptslst>>=return.strong(Codeattrstr)->casehighlightHtmlTrueattrstrofLeft_->return$thecode!(attrsToHtmloptsattr)$stringToHtmloptsstrRighth->returnh(Strikeoutlst)->inlineListToHtmloptslst>>=return.(thespan![thestyle"text-decoration: line-through;"])(SmallCapslst)->inlineListToHtmloptslst>>=return.(thespan![thestyle"font-variant: small-caps;"])(Superscriptlst)->inlineListToHtmloptslst>>=return.sup(Subscriptlst)->inlineListToHtmloptslst>>=return.sub(QuotedquoteTypelst)->let(leftQuote,rightQuote)=casequoteTypeofSingleQuote->(stringToHtmlopts"‘",stringToHtmlopts"’")DoubleQuote->(stringToHtmlopts"“",stringToHtmlopts"”")indocontents<-inlineListToHtmloptslstreturn$leftQuote+++contents+++rightQuote(Mathtstr)->modify(\st->st{stMath=True})>>(casewriterHTMLMathMethodoptsofLaTeXMathML_->-- putting LaTeXMathML in container with class "LaTeX" prevents-- non-math elements on the page from being treated as math by-- the javascriptreturn$thespan![theclass"LaTeX"]$casetofInlineMath->primHtml("$"++str++"$")DisplayMath->primHtml("$$"++str++"$$")JsMath_->doletm=primHtmlstrreturn$casetofInlineMath->thespan![theclass"math"]$mDisplayMath->thediv![theclass"math"]$mWebTeXurl->doletm=image![src(url++urlEncodestr),altstr,titlestr]return$casetofInlineMath->mDisplayMath->br+++m+++brGladTeX->return$casetofInlineMath->primHtml$"<EQ ENV=\"math\">"++str++"</EQ>"DisplayMath->primHtml$"<EQ ENV=\"displaymath\">"++str++"</EQ>"MathML_->doletdt=ift==InlineMaththenDisplayInlineelseDisplayBlockletconf=useShortEmptyTags(constFalse)defaultConfigPPcasetexMathToMathMLdtstrofRightr->return$primHtml$ppcElementconfrLeft_->inlineListToHtmlopts(readTeXMathstr)>>=return.(thespan![theclass"math"])MathJax_->return$primHtml$casetofInlineMath->"\\("++str++"\\)"DisplayMath->"\\["++str++"\\]"PlainMath->dox<-inlineListToHtmlopts(readTeXMathstr)letm=thespan![theclass"math"]$xreturn$casetofInlineMath->mDisplayMath->br+++m+++br)(RawInline"latex"str)->casewriterHTMLMathMethodoptsofLaTeXMathML_->domodify(\st->st{stMath=True})return$primHtmlstr_->returnnoHtml(RawInline"html"str)->return$primHtmlstr(RawInline__)->returnnoHtml(Link[Code_str](s,_))|"mailto:"`isPrefixOf`s->return$obfuscateLinkoptsstrs(Linktxt(s,_))|"mailto:"`isPrefixOf`s->dolinkText<-inlineListToHtmloptstxtreturn$obfuscateLinkopts(showlinkText)s(Linktxt(s,tit))->dolinkText<-inlineListToHtmloptstxtreturn$anchor!([hrefs]++ifnulltitthen[]else[titletit])$linkText(Imagetxt(s,tit))|treatAsImages->doletalternate'=stringifytxtletattributes=[srcs]++(ifnulltitthen[]else[titletit])++ifnulltxtthen[]else[altalternate']return$image!attributes-- note: null title included, as in Markdown.pl(Image_(s,tit))->doletattributes=[srcs]++(ifnulltitthen[]else[titletit])return$itag"embed"!attributes-- note: null title included, as in Markdown.pl(Notecontents)->dost<-getletnotes=stNotesstletnumber=(lengthnotes)+1letref=shownumberhtmlContents<-blockListToNoteoptsrefcontents-- push contents onto front of notesput$st{stNotes=(htmlContents:notes)}return$sup<<anchor![href("#"++writerIdentifierPrefixopts++"fn"++ref),theclass"footnoteRef",prefixedIdopts("fnref"++ref)]<<ref(Cite_il)->inlineListToHtmloptsilblockListToNote::WriterOptions->String->[Block]->StateWriterStateHtmlblockListToNoteoptsrefblocks=-- If last block is Para or Plain, include the backlink at the end of-- that block. Otherwise, insert a new Plain block with the backlink.letbacklink=[RawInline"html"$" <a href=\"#"++writerIdentifierPrefixopts++"fnref"++ref++"\" class=\"footnoteBackLink\">"++(ifwriterAsciioptsthen"&#8617;"else"↩")++"</a>"]blocks'=ifnullblocksthen[]elseletlastBlock=lastblocksotherBlocks=initblocksincaselastBlockof(Paralst)->otherBlocks++[Para(lst++backlink)](Plainlst)->otherBlocks++[Plain(lst++backlink)]_->otherBlocks++[lastBlock,Plainbacklink]indocontents<-blockListToHtmloptsblocks'return$nlopts+++(li![prefixedIdopts("fn"++ref)])contents