{-
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.Parsing
Copyright : Copyright (C) 2006-2010 John MacFarlane
License : GNU GPL, version 2 or above
Maintainer : John MacFarlane <jgm@berkeley.edu>
Stability : alpha
Portability : portable
A utility library with parsers used in pandoc readers.
-}moduleText.Pandoc.Parsing((>>~),anyLine,many1Till,notFollowedBy',oneOfStrings,spaceChar,nonspaceChar,skipSpaces,blankline,blanklines,enclosed,stringAnyCase,parseFromString,lineClump,charsInBalanced,romanNumeral,emailAddress,uri,withHorizDisplacement,withRaw,nullBlock,failIfStrict,failUnlessLHS,escaped,characterReference,updateLastStrPos,anyOrderedListMarker,orderedListMarker,charRef,tableWith,gridTableWith,readWith,testStringWith,ParserState(..),defaultParserState,HeaderType(..),ParserContext(..),QuoteContext(..),NoteTable,KeyTable,Key,toKey,fromKey,lookupKeySrc,smartPunctuation,macro,applyMacros')whereimportText.Pandoc.DefinitionimportText.Pandoc.GenericimportqualifiedText.Pandoc.UTF8asUTF8(putStrLn)importText.ParserCombinators.ParsecimportData.Char(toLower,toUpper,ord,isAscii,isAlphaNum,isDigit,isPunctuation)importData.List(intercalate,transpose)importNetwork.URI(parseURI,URI(..),isAllowedInURI)importControl.Monad(join,liftM,guard)importText.Pandoc.SharedimportqualifiedData.MapasMimportText.TeXMath.Macros(applyMacros,Macro,parseMacroDefinitions)importText.HTML.TagSoup.Entity(lookupEntity)-- | Like >>, but returns the operation on the left.-- (Suggested by Tillmann Rendel on Haskell-cafe list.)(>>~)::(Monadm)=>ma->mb->maa>>~b=a>>=\x->b>>returnx-- | Parse any line of textanyLine::GenParserCharst[Char]anyLine=manyTillanyCharnewline-- | Like @manyTill@, but reads at least one item.many1Till::GenParsertoksta->GenParsertokstend->GenParsertokst[a]many1Tillpend=dofirst<-prest<-manyTillpendreturn(first:rest)-- | A more general form of @notFollowedBy@. This one allows any -- type of parser to be specified, and succeeds only if that parser fails.-- It does not consume any input.notFollowedBy'::Showb=>GenParserastb->GenParserast()notFollowedBy'p=try$join$doa<-trypreturn(unexpected(showa))<|>return(return())-- (This version due to Andrew Pimlott on the Haskell mailing list.)-- | Parses one of a list of strings (tried in order). oneOfStrings::[String]->GenParserCharstStringoneOfStringslistOfStrings=choice$map(try.string)listOfStrings-- | Parses a space or tab.spaceChar::CharParserstCharspaceChar=satisfy$\c->c==' '||c=='\t'-- | Parses a nonspace, nonnewline character.nonspaceChar::CharParserstCharnonspaceChar=satisfy$\x->x/='\t'&&x/='\n'&&x/=' '&&x/='\r'-- | Skips zero or more spaces or tabs.skipSpaces::GenParserCharst()skipSpaces=skipManyspaceChar-- | Skips zero or more spaces or tabs, then reads a newline.blankline::GenParserCharstCharblankline=try$skipSpaces>>newline-- | Parses one or more blank lines and returns a string of newlines.blanklines::GenParserCharst[Char]blanklines=many1blankline-- | Parses material enclosed between start and end parsers.enclosed::GenParserCharstt-- ^ start parser->GenParserCharstend-- ^ end parser->GenParserCharsta-- ^ content parser (to be used repeatedly)->GenParserCharst[a]enclosedstartendparser=try$start>>notFollowedByspace>>many1Tillparserend-- | Parse string, case insensitive.stringAnyCase::[Char]->CharParserstStringstringAnyCase[]=string""stringAnyCase(x:xs)=dofirstChar<-char(toUpperx)<|>char(toLowerx)rest<-stringAnyCasexsreturn(firstChar:rest)-- | Parse contents of 'str' using 'parser' and return result.parseFromString::GenParsertoksta->[tok]->GenParsertokstaparseFromStringparserstr=dooldPos<-getPositionoldInput<-getInputsetInputstrresult<-parsersetInputoldInputsetPositionoldPosreturnresult-- | Parse raw line block up to and including blank lines.lineClump::GenParserCharstStringlineClump=blanklines<|>(many1(notFollowedByblankline>>anyLine)>>=return.unlines)-- | Parse a string of characters between an open character-- and a close character, including text between balanced-- pairs of open and close, which must be different. For example,-- @charsInBalanced '(' ')' anyChar@ will parse "(hello (there))"-- and return "hello (there)".charsInBalanced::Char->Char->GenParserCharstChar->GenParserCharstStringcharsInBalancedopencloseparser=try$docharopenletisDelimc=c==open||c==closeraw<-many$many1(notFollowedBy(satisfyisDelim)>>parser)<|>(dores<-charsInBalancedopencloseparserreturn$[open]++res++[close])charclosereturn$concatraw-- old charsInBalanced would be:-- charsInBalanced open close (noneOf "\n" <|> char '\n' >> notFollowedBy blankline)-- old charsInBalanced' would be:-- charsInBalanced open close anyChar-- Auxiliary functions for romanNumeral:lowercaseRomanDigits::[Char]lowercaseRomanDigits=['i','v','x','l','c','d','m']uppercaseRomanDigits::[Char]uppercaseRomanDigits=maptoUpperlowercaseRomanDigits-- | Parses a roman numeral (uppercase or lowercase), returns number.romanNumeral::Bool-- ^ Uppercase if true->GenParserCharstIntromanNumeralupperCase=doletromanDigits=ifupperCasethenuppercaseRomanDigitselselowercaseRomanDigitslookAhead$oneOfromanDigitslet[one,five,ten,fifty,hundred,fivehundred,thousand]=mapcharromanDigitsthousands<-manythousand>>=(return.(1000*).length)ninehundreds<-option0$try$hundred>>thousand>>return900fivehundreds<-manyfivehundred>>=(return.(500*).length)fourhundreds<-option0$try$hundred>>fivehundred>>return400hundreds<-manyhundred>>=(return.(100*).length)nineties<-option0$try$ten>>hundred>>return90fifties<-manyfifty>>=(return.(50*).length)forties<-option0$try$ten>>fifty>>return40tens<-manyten>>=(return.(10*).length)nines<-option0$try$one>>ten>>return9fives<-manyfive>>=(return.(5*).length)fours<-option0$try$one>>five>>return4ones<-manyone>>=(return.length)lettotal=thousands+ninehundreds+fivehundreds+fourhundreds+hundreds+nineties+fifties+forties+tens+nines+fives+fours+onesiftotal==0thenfail"not a roman numeral"elsereturntotal-- Parsers for email addresses and URIsemailChar::GenParserCharstCharemailChar=alphaNum<|>satisfy(\c->c=='-'||c=='+'||c=='_'||c=='.')domainChar::GenParserCharstChardomainChar=alphaNum<|>char'-'domain::GenParserCharst[Char]domain=dofirst<-many1domainChardom<-many1$try(char'.'>>many1domainChar)return$intercalate"."(first:dom)-- | Parses an email address; returns original and corresponding-- escaped mailto: URI.emailAddress::GenParserCharst(String,String)emailAddress=try$dofirstLetter<-alphaNumrestAddr<-manyemailCharletaddr=firstLetter:restAddrchar'@'dom<-domainletfull=addr++'@':domreturn(full,escapeURI$"mailto:"++full)-- | Parses a URI. Returns pair of original and URI-escaped version.uri::GenParserCharst(String,String)uri=try$doletprotocols=["http:","https:","ftp:","file:","mailto:","news:","telnet:"]lookAhead$oneOfStringsprotocols-- Scan non-ascii characters and ascii characters allowed in a URI.-- We allow punctuation except when followed by a space, since-- we don't want the trailing '.' in 'http://google.com.'letinnerPunct=try$satisfyisPunctuation>>~notFollowedBy(newline<|>spaceChar)leturiChar=innerPunct<|>satisfy(\c->not(isPunctuationc)&&(not(isAsciic)||isAllowedInURIc))-- We want to allow-- http://en.wikipedia.org/wiki/State_of_emergency_(disambiguation)-- as a URL, while NOT picking up the closing paren in-- (http://wikipedia.org)-- So we include balanced parens in the URL.letinParens=try$dochar'('res<-manyuriCharchar')'return$'(':res++")"str<-liftMconcat$many1$inParens<|>count1(innerPunct<|>uriChar)-- now see if they amount to an absolute URIcaseparseURI(escapeURIstr)ofJusturi'->ifuriSchemeuri'`elem`protocolsthenreturn(str,showuri')elsefail"not a URI"Nothing->fail"not a URI"-- | Applies a parser, returns tuple of its results and its horizontal-- displacement (the difference between the source column at the end-- and the source column at the beginning). Vertical displacement-- (source row) is ignored.withHorizDisplacement::GenParserCharsta-- ^ Parser to apply->GenParserCharst(a,Int)-- ^ (result, displacement)withHorizDisplacementparser=dopos1<-getPositionresult<-parserpos2<-getPositionreturn(result,sourceColumnpos2-sourceColumnpos1)-- | Applies a parser and returns the raw string that was parsed,-- along with the value produced by the parser.withRaw::GenParserCharsta->GenParserCharst(a,[Char])withRawparser=dopos1<-getPositioninp<-getInputresult<-parserpos2<-getPositionlet(l1,c1)=(sourceLinepos1,sourceColumnpos1)let(l2,c2)=(sourceLinepos2,sourceColumnpos2)letinplines=take((l2-l1)+1)$linesinpletraw=caseinplinesof[]->error"raw: inplines is null"-- shouldn't happen[l]->take(c2-c1)lls->unlines(initls)++take(c2-1)(lastls)return(result,raw)-- | Parses a character and returns 'Null' (so that the parser can move on-- if it gets stuck).nullBlock::GenParserCharstBlocknullBlock=anyChar>>returnNull-- | Fail if reader is in strict markdown syntax mode.failIfStrict::GenParseraParserState()failIfStrict=dostate<-getStateifstateStrictstatethenfail"strict mode"elsereturn()-- | Fail unless we're in literate haskell mode.failUnlessLHS::GenParsertokParserState()failUnlessLHS=getState>>=guard.stateLiterateHaskell-- | Parses backslash, then applies character parser.escaped::GenParserCharstChar-- ^ Parser for character to escape->GenParserCharstCharescapedparser=try$char'\\'>>parser-- | Parse character entity.characterReference::GenParserCharstCharcharacterReference=try$dochar'&'ent<-many1TillnonspaceChar(char';')caselookupEntityentofJustc->returncNothing->fail"entity not found"-- | Parses an uppercase roman numeral and returns (UpperRoman, number).upperRoman::GenParserCharst(ListNumberStyle,Int)upperRoman=donum<-romanNumeralTruereturn(UpperRoman,num)-- | Parses a lowercase roman numeral and returns (LowerRoman, number).lowerRoman::GenParserCharst(ListNumberStyle,Int)lowerRoman=donum<-romanNumeralFalsereturn(LowerRoman,num)-- | Parses a decimal numeral and returns (Decimal, number).decimal::GenParserCharst(ListNumberStyle,Int)decimal=donum<-many1digitreturn(Decimal,readnum)-- | Parses a '@' and optional label and-- returns (DefaultStyle, [next example number]). The next-- example number is incremented in parser state, and the label-- (if present) is added to the label table.exampleNum::GenParserCharParserState(ListNumberStyle,Int)exampleNum=dochar'@'lab<-many(alphaNum<|>satisfy(\c->c=='_'||c=='-'))st<-getStateletnum=stateNextExamplestletnewlabels=ifnulllabthenstateExamplesstelseM.insertlabnum$stateExamplesstupdateState$\s->s{stateNextExample=num+1,stateExamples=newlabels}return(Example,num)-- | Parses a '#' returns (DefaultStyle, 1).defaultNum::GenParserCharst(ListNumberStyle,Int)defaultNum=dochar'#'return(DefaultStyle,1)-- | Parses a lowercase letter and returns (LowerAlpha, number).lowerAlpha::GenParserCharst(ListNumberStyle,Int)lowerAlpha=doch<-oneOf['a'..'z']return(LowerAlpha,ordch-ord'a'+1)-- | Parses an uppercase letter and returns (UpperAlpha, number).upperAlpha::GenParserCharst(ListNumberStyle,Int)upperAlpha=doch<-oneOf['A'..'Z']return(UpperAlpha,ordch-ord'A'+1)-- | Parses a roman numeral i or IromanOne::GenParserCharst(ListNumberStyle,Int)romanOne=(char'i'>>return(LowerRoman,1))<|>(char'I'>>return(UpperRoman,1))-- | Parses an ordered list marker and returns list attributes.anyOrderedListMarker::GenParserCharParserStateListAttributesanyOrderedListMarker=choice$[delimParsernumParser|delimParser<-[inPeriod,inOneParen,inTwoParens],numParser<-[decimal,exampleNum,defaultNum,romanOne,lowerAlpha,lowerRoman,upperAlpha,upperRoman]]-- | Parses a list number (num) followed by a period, returns list attributes.inPeriod::GenParserCharst(ListNumberStyle,Int)->GenParserCharstListAttributesinPeriodnum=try$do(style,start)<-numchar'.'letdelim=ifstyle==DefaultStylethenDefaultDelimelsePeriodreturn(start,style,delim)-- | Parses a list number (num) followed by a paren, returns list attributes.inOneParen::GenParserCharst(ListNumberStyle,Int)->GenParserCharstListAttributesinOneParennum=try$do(style,start)<-numchar')'return(start,style,OneParen)-- | Parses a list number (num) enclosed in parens, returns list attributes.inTwoParens::GenParserCharst(ListNumberStyle,Int)->GenParserCharstListAttributesinTwoParensnum=try$dochar'('(style,start)<-numchar')'return(start,style,TwoParens)-- | Parses an ordered list marker with a given style and delimiter,-- returns number.orderedListMarker::ListNumberStyle->ListNumberDelim->GenParserCharParserStateIntorderedListMarkerstyledelim=doletnum=defaultNum<|>-- # can continue any kind of listcasestyleofDefaultStyle->decimalExample->exampleNumDecimal->decimalUpperRoman->upperRomanLowerRoman->lowerRomanUpperAlpha->upperAlphaLowerAlpha->lowerAlphaletcontext=casedelimofDefaultDelim->inPeriodPeriod->inPeriodOneParen->inOneParenTwoParens->inTwoParens(start,_,_)<-contextnumreturnstart-- | Parses a character reference and returns a Str element.charRef::GenParserCharstInlinecharRef=doc<-characterReferencereturn$Str[c]-- | Parse a table using 'headerParser', 'rowParser',-- 'lineParser', and 'footerParser'.tableWith::GenParserCharParserState([[Block]],[Alignment],[Int])->([Int]->GenParserCharParserState[[Block]])->GenParserCharParserStatesep->GenParserCharParserStateend->GenParserCharParserState[Inline]->GenParserCharParserStateBlocktableWithheaderParserrowParserlineParserfooterParsercaptionParser=try$docaption'<-option[]captionParser(heads,aligns,indices)<-headerParserlines'<-rowParserindices`sepEndBy`lineParserfooterParsercaption<-ifnullcaption'thenoption[]captionParserelsereturncaption'state<-getStateletnumColumns=stateColumnsstateletwidths=widthsFromIndicesnumColumnsindicesreturn$Tablecaptionalignswidthsheadslines'-- Calculate relative widths of table columns, based on indiceswidthsFromIndices::Int-- Number of columns on terminal->[Int]-- Indices->[Double]-- Fractional relative sizes of columnswidthsFromIndices_[]=[]widthsFromIndicesnumColumns'indices=letnumColumns=maxnumColumns'(ifnullindicesthen0elselastindices)lengths'=zipWith(-)indices(0:indices)lengths=reverse$casereverselengths'of[]->[][x]->[x]-- compensate for the fact that intercolumn-- spaces are counted in widths of all columns-- but the last...(x:y:zs)->ifx<y&&y-x<=2theny:y:zselsex:y:zstotLength=sumlengthsquotient=iftotLength>numColumnsthenfromIntegraltotLengthelsefromIntegralnumColumnsfracs=map(\l->(fromIntegrall)/quotient)lengthsintailfracs-- Parse a grid table: starts with row of '-' on top, then header-- (which may be grid), then the rows,-- which may be grid, separated by blank lines, and-- ending with a footer (dashed line followed by blank line).gridTableWith::GenParserCharParserStateBlock-- ^ Block parser->GenParserCharParserState[Inline]-- ^ Caption parser->Bool-- ^ Headerless table->GenParserCharParserStateBlockgridTableWithblocktableCaptionheadless=tableWith(gridTableHeaderheadlessblock)(gridTableRowblock)(gridTableSep'-')gridTableFootertableCaptiongridTableSplitLine::[Int]->String->[String]gridTableSplitLineindicesline=mapremoveFinalBar$tail$splitStringByIndices(initindices)$removeTrailingSpacelinegridPart::Char->GenParserCharst(Int,Int)gridPartch=dodashes<-many1(charch)char'+'return(lengthdashes,lengthdashes+1)gridDashedLines::Char->GenParserCharst[(Int,Int)]gridDashedLinesch=try$char'+'>>many1(gridPartch)>>~blanklineremoveFinalBar::String->StringremoveFinalBar=reverse.dropWhile(`elem`" \t").dropWhile(=='|').reverse-- | Separator between rows of grid table.gridTableSep::Char->GenParserCharParserStateChargridTableSepch=try$gridDashedLinesch>>return'\n'-- | Parse header for a grid table.gridTableHeader::Bool-- ^ Headerless table->GenParserCharParserStateBlock->GenParserCharParserState([[Block]],[Alignment],[Int])gridTableHeaderheadlessblock=try$dooptionalblanklinesdashes<-gridDashedLines'-'rawContent<-ifheadlessthenreturn$repeat""elsemany1(notFollowedBy(gridTableSep'=')>>char'|'>>many1TillanyCharnewline)ifheadlessthenreturn()elsegridTableSep'='>>return()letlines'=mapsnddashesletindices=scanl(+)0lines'letaligns=replicate(lengthlines')AlignDefault-- RST does not have a notion of alignmentsletrawHeads=ifheadlessthenreplicate(lengthdashes)""elsemap(intercalate" ")$transpose$map(gridTableSplitLineindices)rawContentheads<-mapM(parseFromString$manyblock)$mapremoveLeadingTrailingSpacerawHeadsreturn(heads,aligns,indices)gridTableRawLine::[Int]->GenParserCharParserState[String]gridTableRawLineindices=dochar'|'line<-many1TillanyCharnewlinereturn(gridTableSplitLineindicesline)-- | Parse row of grid table.gridTableRow::GenParserCharParserStateBlock->[Int]->GenParserCharParserState[[Block]]gridTableRowblockindices=docolLines<-many1(gridTableRawLineindices)letcols=map((++"\n").unlines.removeOneLeadingSpace)$transposecolLinesmapM(liftMcompactifyCell.parseFromString(manyblock))colsremoveOneLeadingSpace::[String]->[String]removeOneLeadingSpacexs=ifallstartsWithSpacexsthenmap(drop1)xselsexswherestartsWithSpace""=TruestartsWithSpace(y:_)=y==' 'compactifyCell::[Block]->[Block]compactifyCellbs=head$compactify[bs]-- | Parse footer for a grid table.gridTableFooter::GenParserCharParserState[Char]gridTableFooter=blanklines----- | Parse a string with a given parser and state.readWith::GenParsertParserStatea-- ^ parser->ParserState-- ^ initial state->[t]-- ^ input->areadWithparserstateinput=caserunParserparserstate"source"inputofLefterr'->error$"\nError:\n"++showerr'Rightresult->result-- | Parse a string with @parser@ (for testing).testStringWith::(Showa)=>GenParserCharParserStatea->String->IO()testStringWithparserstr=UTF8.putStrLn$show$readWithparserdefaultParserStatestr-- | Parsing options.dataParserState=ParserState{stateParseRaw::Bool,-- ^ Parse raw HTML and LaTeX?stateParserContext::ParserContext,-- ^ Inside list?stateQuoteContext::QuoteContext,-- ^ Inside quoted environment?stateMaxNestingLevel::Int,-- ^ Max # of nested Strong/EmphstateLastStrPos::MaybeSourcePos,-- ^ Position after last str parsedstateKeys::KeyTable,-- ^ List of reference keysstateCitations::[String],-- ^ List of available citationsstateNotes::NoteTable,-- ^ List of notesstateTabStop::Int,-- ^ Tab stopstateStandalone::Bool,-- ^ Parse bibliographic info?stateTitle::[Inline],-- ^ Title of documentstateAuthors::[[Inline]],-- ^ Authors of documentstateDate::[Inline],-- ^ Date of documentstateStrict::Bool,-- ^ Use strict markdown syntax?stateSmart::Bool,-- ^ Use smart typography?stateOldDashes::Bool,-- ^ Use pandoc <= 1.8.2.1 behavior-- in parsing dashes; -- is em-dash;-- before numeral is en-dashstateLiterateHaskell::Bool,-- ^ Treat input as literate haskellstateColumns::Int,-- ^ Number of columns in terminalstateHeaderTable::[HeaderType],-- ^ Ordered list of header types usedstateIndentedCodeClasses::[String],-- ^ Classes to use for indented code blocksstateNextExample::Int,-- ^ Number of next examplestateExamples::M.MapStringInt,-- ^ Map from example labels to numbers stateHasChapters::Bool,-- ^ True if \chapter encounteredstateApplyMacros::Bool,-- ^ Apply LaTeX macros?stateMacros::[Macro],-- ^ List of macros defined so farstateRstDefaultRole::String-- ^ Current rST default interpreted text role}derivingShowdefaultParserState::ParserStatedefaultParserState=ParserState{stateParseRaw=False,stateParserContext=NullState,stateQuoteContext=NoQuote,stateMaxNestingLevel=6,stateLastStrPos=Nothing,stateKeys=M.empty,stateCitations=[],stateNotes=[],stateTabStop=4,stateStandalone=False,stateTitle=[],stateAuthors=[],stateDate=[],stateStrict=False,stateSmart=False,stateOldDashes=False,stateLiterateHaskell=False,stateColumns=80,stateHeaderTable=[],stateIndentedCodeClasses=[],stateNextExample=1,stateExamples=M.empty,stateHasChapters=False,stateApplyMacros=True,stateMacros=[],stateRstDefaultRole="title-reference"}dataHeaderType=SingleHeaderChar-- ^ Single line of characters underneath|DoubleHeaderChar-- ^ Lines of characters above and belowderiving(Eq,Show)dataParserContext=ListItemState-- ^ Used when running parser on list item contents|NullState-- ^ Default statederiving(Eq,Show)dataQuoteContext=InSingleQuote-- ^ Used when parsing inside single quotes|InDoubleQuote-- ^ Used when parsing inside double quotes|NoQuote-- ^ Used when not parsing inside quotesderiving(Eq,Show)typeNoteTable=[(String,String)]newtypeKey=Key[Inline]deriving(Show,Read,Eq,Ord)toKey::[Inline]->KeytoKey=Key.bottomUplowercasewherelowercase::Inline->Inlinelowercase(Strxs)=Str(maptoLowerxs)lowercase(Mathtxs)=Matht(maptoLowerxs)lowercase(Codeattrxs)=Codeattr(maptoLowerxs)lowercase(RawInlinefxs)=RawInlinef(maptoLowerxs)lowercaseLineBreak=Spacelowercasex=xfromKey::Key->[Inline]fromKey(Keyxs)=xstypeKeyTable=M.MapKeyTarget-- | Look up key in key table and return target object.lookupKeySrc::KeyTable-- ^ Key table->Key-- ^ Key->MaybeTargetlookupKeySrctablekey=caseM.lookupkeytableofNothing->NothingJustsrc->Justsrc-- | Fail unless we're in "smart typography" mode.failUnlessSmart::GenParsertokParserState()failUnlessSmart=getState>>=guard.stateSmartsmartPunctuation::GenParserCharParserStateInline->GenParserCharParserStateInlinesmartPunctuationinlineParser=dofailUnlessSmartchoice[quotedinlineParser,apostrophe,dash,ellipses]apostrophe::GenParserCharParserStateInlineapostrophe=(char'\''<|>char'\8217')>>return(Str"\x2019")quoted::GenParserCharParserStateInline->GenParserCharParserStateInlinequotedinlineParser=doubleQuotedinlineParser<|>singleQuotedinlineParserwithQuoteContext::QuoteContext->(GenParserCharParserStateInline)->GenParserCharParserStateInlinewithQuoteContextcontextparser=dooldState<-getStateletoldQuoteContext=stateQuoteContextoldStatesetStateoldState{stateQuoteContext=context}result<-parsernewState<-getStatesetStatenewState{stateQuoteContext=oldQuoteContext}returnresultsingleQuoted::GenParserCharParserStateInline->GenParserCharParserStateInlinesingleQuotedinlineParser=try$dosingleQuoteStartwithQuoteContextInSingleQuote$many1TillinlineParsersingleQuoteEnd>>=return.QuotedSingleQuote.normalizeSpacesdoubleQuoted::GenParserCharParserStateInline->GenParserCharParserStateInlinedoubleQuotedinlineParser=try$dodoubleQuoteStartwithQuoteContextInDoubleQuote$docontents<-manyTillinlineParserdoubleQuoteEndreturn.QuotedDoubleQuote.normalizeSpaces$contentsfailIfInQuoteContext::QuoteContext->GenParsertokParserState()failIfInQuoteContextcontext=dost<-getStateifstateQuoteContextst==contextthenfail"already inside quotes"elsereturn()charOrRef::[Char]->GenParserCharstCharcharOrRefcs=oneOfcs<|>try(doc<-characterReferenceguard(c`elem`cs)returnc)updateLastStrPos::GenParserCharParserState()updateLastStrPos=getPosition>>=\p->updateState$\s->s{stateLastStrPos=Justp}singleQuoteStart::GenParserCharParserState()singleQuoteStart=dofailIfInQuoteContextInSingleQuotepos<-getPositionst<-getState-- single quote start can't be right after strguard$stateLastStrPosst/=Justpostry$docharOrRef"'\8216\145"notFollowedBy(oneOf")!],;:-? \t\n")notFollowedBy(char'.')<|>lookAhead(string"...">>return())notFollowedBy(try(oneOfStrings["s","t","m","ve","ll","re"]>>satisfy(not.isAlphaNum)))-- possess/contractionreturn()singleQuoteEnd::GenParserCharst()singleQuoteEnd=try$docharOrRef"'\8217\146"notFollowedByalphaNumdoubleQuoteStart::GenParserCharParserState()doubleQuoteStart=dofailIfInQuoteContextInDoubleQuotetry$docharOrRef"\"\8220\147"notFollowedBy(satisfy(\c->c==' '||c=='\t'||c=='\n'))doubleQuoteEnd::GenParserCharst()doubleQuoteEnd=docharOrRef"\"\8221\148"return()ellipses::GenParserCharstInlineellipses=dotry(charOrRef"\8230\133")<|>try(string"...">>return'…')return(Str"\8230")dash::GenParserCharParserStateInlinedash=dooldDashes<-stateOldDashes`fmap`getStateifoldDashesthenemDashOld<|>enDashOldelseStr`fmap`(hyphenDash<|>emDash<|>enDash)-- Two hyphens = en-dash, three = em-dashhyphenDash::GenParserCharstStringhyphenDash=dotry$string"--"option"\8211"(char'-'>>return"\8212")emDash::GenParserCharstStringemDash=dotry(charOrRef"\8212\151")return"\8212"enDash::GenParserCharstStringenDash=dotry(charOrRef"\8212\151")return"\8211"enDashOld::GenParserCharstInlineenDashOld=dotry(charOrRef"\8211\150")<|>try(char'-'>>lookAhead(satisfyisDigit)>>return'–')return(Str"\8211")emDashOld::GenParserCharstInlineemDashOld=dotry(charOrRef"\8212\151")<|>(try$string"--">>optional(char'-')>>return'-')return(Str"\8212")---- Macros---- | Parse a \newcommand or \renewcommand macro definition.macro::GenParserCharParserStateBlockmacro=dogetState>>=guard.stateApplyMacrosinp<-getInputcaseparseMacroDefinitionsinpof([],_)->pzero(ms,rest)->docount(lengthinp-lengthrest)anyCharupdateState$\st->st{stateMacros=ms++stateMacrosst}returnNull-- | Apply current macros to string.applyMacros'::String->GenParserCharParserStateStringapplyMacros'target=doapply<-liftMstateApplyMacrosgetStateifapplythendomacros<-liftMstateMacrosgetStatereturn$applyMacrosmacrostargetelsereturntarget