git.iko.soy/typeable/pandoc
on master help sign in
Support shortcut reference links in markdown writer

Issue #1977 Most markdown processors support the [shortcut format] for reference links. Pandoc's markdown reader parsed this shortcuts unoptionally. Pandoc's markdown writer (with --reference-links option) never shortcutted links. This commit adds an extension `shortcut_reference_links`. The extension is enabled by default for those markdown flavors that support reading shortcut reference links, namely: - pandoc - strict pandoc - github flavoured - PHPmarkdown If extension is enabled, reader parses the shortcuts in the same way as it preveously did. Otherwise it would parse them as normal text. If extension is enabled, writer outputs shortcut reference links unless doing so would cause problems (see test cases in `tests/Tests/Writers/Markdown.hs`).

commitb9f77ed03d0e4a0651d7508d563e880556690fcf authorKonstantin Zudov <konstantin@anche.no> dateTue 3 Mar 2015, 03:28 (12 years ago) parent4f0c5c3
5 files changed+158−31
README+5 src/Text/Pandoc/Options.hs+7−1 src/Text/Pandoc/Readers/Markdown.hs+1 src/Text/Pandoc/Writers/Markdown.hs+55−29 tests/Tests/Writers/Markdown.hs+90−1
README+5
@@ -2785,6 +2785,11 @@ in several respects:
27852785 we must either disallow lazy wrapping or require a blank line between
27862786 list items.
27872787
2788+#### Extension: `shortcut_reference_links` ####
2789+
2790+Allows to use shortcut reference links: `[foo]` instead of `[foo][]`. Writer
2791+would shortcut links unless doing so might cause problems.
2792+
27882793Markdown variants
27892794-----------------
27902795
src/Text/Pandoc/Options.hs+7−1
@@ -109,6 +109,7 @@ data Extension =
109109 | Ext_implicit_header_references -- ^ Implicit reference links for headers
110110 | Ext_line_blocks -- ^ RST style line blocks
111111 | Ext_epub_html_exts -- ^ Recognise the EPUB extended version of HTML
112+ | Ext_shortcut_reference_links -- ^ Shortcut reference links
112113 deriving (Show, Read, Enum, Eq, Ord, Bounded)
113114
114115pandocExtensions :: Set Extension
@@ -151,6 +152,7 @@ pandocExtensions = Set.fromList
151152 , Ext_header_attributes
152153 , Ext_implicit_header_references
153154 , Ext_line_blocks
155+ , Ext_shortcut_reference_links
154156 ]
155157
156158phpMarkdownExtraExtensions :: Set Extension
@@ -164,6 +166,7 @@ phpMarkdownExtraExtensions = Set.fromList
164166 , Ext_intraword_underscores
165167 , Ext_header_attributes
166168 , Ext_abbreviations
169+ , Ext_shortcut_reference_links
167170 ]
168171
169172githubMarkdownExtensions :: Set Extension
@@ -180,6 +183,7 @@ githubMarkdownExtensions = Set.fromList
180183 , Ext_strikeout
181184 , Ext_hard_line_breaks
182185 , Ext_lists_without_preceding_blankline
186+ , Ext_shortcut_reference_links
183187 ]
184188
185189multimarkdownExtensions :: Set Extension
@@ -202,7 +206,9 @@ multimarkdownExtensions = Set.fromList
202206
203207strictExtensions :: Set Extension
204208strictExtensions = Set.fromList
205− [ Ext_raw_html ]
209+ [ Ext_raw_html
210+ , Ext_shortcut_reference_links
211+ ]
206212
207213data ReaderOptions = ReaderOptions{
208214 readerExtensions :: Set Extension -- ^ Syntax extensions
src/Text/Pandoc/Readers/Markdown.hs+1
@@ -1679,6 +1679,7 @@ referenceLink constructor (lab, raw) = do
16791679 lookAhead (try (spnl >> normalCite >> return (mempty, "")))
16801680 <|>
16811681 try (spnl >> reference)
1682+ when (raw' == "") $ guardEnabled Ext_shortcut_reference_links
16821683 let labIsRef = raw' == "" || raw' == "[]"
16831684 let key = toKey $ if labIsRef then raw else raw'
16841685 parsedRaw <- parseFromString (mconcat <$> many inline) raw'
src/Text/Pandoc/Writers/Markdown.hs+55−29
@@ -57,14 +57,15 @@ import qualified Data.Text as T
5757
5858type Notes = [[Block]]
5959type Refs = [([Inline], Target)]
60−data WriterState = WriterState { stNotes :: Notes
61− , stRefs :: Refs
62− , stInList :: Bool
63− , stIds :: [String]
64− , stPlain :: Bool }
60+data WriterState = WriterState { stNotes :: Notes
61+ , stRefs :: Refs
62+ , stRefShortcutable :: Bool
63+ , stInList :: Bool
64+ , stIds :: [String]
65+ , stPlain :: Bool }
6566instance Default WriterState
66− where def = WriterState{ stNotes = [], stRefs = [], stInList = False,
67− stIds = [], stPlain = False }
67+ where def = WriterState{ stNotes = [], stRefs = [], stRefShortcutable = True,
68+ stInList = False, stIds = [], stPlain = False }
6869
6970-- | Convert Pandoc to Markdown.
7071writeMarkdown :: WriterOptions -> Pandoc -> String
@@ -690,27 +691,47 @@ getReference label (src, tit) = do
690691inlineListToMarkdown :: WriterOptions -> [Inline] -> State WriterState Doc
691692inlineListToMarkdown opts lst = do
692693 inlist <- gets stInList
693− mapM (inlineToMarkdown opts)
694− (if inlist then avoidBadWraps lst else lst) >>= return . cat
695− where avoidBadWraps [] = []
696− avoidBadWraps (Space:Str ('>':cs):xs) =
697− Str (' ':'>':cs) : avoidBadWraps xs
698− avoidBadWraps (Space:Str [c]:[])
699− | c `elem` "-*+" = Str [' ', c] : []
700− avoidBadWraps (Space:Str [c]:Space:xs)
701− | c `elem` "-*+" = Str [' ', c] : Space : avoidBadWraps xs
702− avoidBadWraps (Space:Str cs:Space:xs)
703− | isOrderedListMarker cs = Str (' ':cs) : Space : avoidBadWraps xs
704− avoidBadWraps (Space:Str cs:[])
705− | isOrderedListMarker cs = Str (' ':cs) : []
706− avoidBadWraps (x:xs) = x : avoidBadWraps xs
707− isOrderedListMarker xs = endsWithListPunct xs &&
708− isRight (runParserT (anyOrderedListMarker >> eof)
709− defaultParserState "" xs)
710− endsWithListPunct xs = case reverse xs of
711− '.':_ -> True
712− ')':_ -> True
713− _ -> False
694+ go (if inlist then avoidBadWrapsInList lst else lst)
695+ where go [] = return empty
696+ go (i:is) = case i of
697+ (Link _ _) -> case is of
698+ -- If a link is followed by another link or '[' we don't shortcut
699+ (Link _ _):_ -> unshortcutable
700+ Space:(Link _ _):_ -> unshortcutable
701+ Space:(Str('[':_)):_ -> unshortcutable
702+ Space:(RawInline _ ('[':_)):_ -> unshortcutable
703+ Space:(Cite _ _):_ -> unshortcutable
704+ (Cite _ _):_ -> unshortcutable
705+ Str ('[':_):_ -> unshortcutable
706+ (RawInline _ ('[':_)):_ -> unshortcutable
707+ (RawInline _ (' ':'[':_)):_ -> unshortcutable
708+ _ -> shortcutable
709+ _ -> shortcutable
710+ where shortcutable = liftM2 (<>) (inlineToMarkdown opts i) (go is)
711+ unshortcutable = do
712+ iMark <- withState (\s -> s { stRefShortcutable = False })
713+ (inlineToMarkdown opts i)
714+ modify (\s -> s {stRefShortcutable = True })
715+ fmap (iMark <>) (go is)
716+
717+avoidBadWrapsInList :: [Inline] -> [Inline]
718+avoidBadWrapsInList [] = []
719+avoidBadWrapsInList (Space:Str ('>':cs):xs) =
720+ Str (' ':'>':cs) : avoidBadWrapsInList xs
721+avoidBadWrapsInList (Space:Str [c]:[])
722+ | c `elem` "-*+" = Str [' ', c] : []
723+avoidBadWrapsInList (Space:Str [c]:Space:xs)
724+ | c `elem` "-*+" = Str [' ', c] : Space : avoidBadWrapsInList xs
725+avoidBadWrapsInList (Space:Str cs:Space:xs)
726+ | isOrderedListMarker cs = Str (' ':cs) : Space : avoidBadWrapsInList xs
727+avoidBadWrapsInList (Space:Str cs:[])
728+ | isOrderedListMarker cs = Str (' ':cs) : []
729+avoidBadWrapsInList (x:xs) = x : avoidBadWrapsInList xs
730+
731+isOrderedListMarker :: String -> Bool
732+isOrderedListMarker xs = (last xs `elem` ".)") &&
733+ isRight (runParserT (anyOrderedListMarker >> eof)
734+ defaultParserState "" xs)
714735
715736isRight :: Either a b -> Bool
716737isRight (Right _) = True
@@ -873,6 +894,9 @@ inlineToMarkdown opts (Link txt (src, tit)) = do
873894 [Str s] | escapeURI s == srcSuffix -> True
874895 _ -> False
875896 let useRefLinks = writerReferenceLinks opts && not useAuto
897+ shortcutable <- gets stRefShortcutable
898+ let useShortcutRefLinks = shortcutable &&
899+ isEnabled Ext_shortcut_reference_links opts
876900 ref <- if useRefLinks then getReference txt (src, tit) else return []
877901 reftext <- inlineListToMarkdown opts ref
878902 return $ if useAuto
@@ -882,7 +906,9 @@ inlineToMarkdown opts (Link txt (src, tit)) = do
882906 else if useRefLinks
883907 then let first = "[" <> linktext <> "]"
884908 second = if txt == ref
885− then "[]"
909+ then if useShortcutRefLinks
910+ then ""
911+ else "[]"
886912 else "[" <> reftext <> "]"
887913 in first <> second
888914 else if plain
tests/Tests/Writers/Markdown.hs+90−1
@@ -1,4 +1,5 @@
11{-# LANGUAGE OverloadedStrings #-}
2+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
23module Tests.Writers.Markdown (tests) where
34
45import Test.Framework
@@ -35,4 +36,92 @@ tests = [ "indented code after list"
3536 =: bulletList [ plain "foo" <> bulletList [ plain "bar" ],
3637 plain "baz" ]
3738 =?> "- foo\n - bar\n- baz\n"
38− ]
39+ ] ++ [shortcutLinkRefsTests]
40+
41+shortcutLinkRefsTests :: Test
42+shortcutLinkRefsTests =
43+ let infix 4 =:
44+ (=:) :: (ToString a, ToPandoc a)
45+ => String -> (a, String) -> Test
46+ (=:) = test (writeMarkdown (def {writerReferenceLinks = True}) . toPandoc)
47+ in testGroup "Shortcut reference links"
48+ [ "Simple link (shortcutable)"
49+ =: (para (link "/url" "title" "foo"))
50+ =?> "[foo]\n\n [foo]: /url \"title\""
51+ , "Followed by another link (unshortcutable)"
52+ =: (para ((link "/url1" "title1" "first")
53+ <> (link "/url2" "title2" "second")))
54+ =?> unlines [ "[first][][second]"
55+ , ""
56+ , " [first]: /url1 \"title1\""
57+ , " [second]: /url2 \"title2\""
58+ ]
59+ , "Followed by space and another link (unshortcutable)"
60+ =: (para ((link "/url1" "title1" "first") <> " "
61+ <> (link "/url2" "title2" "second")))
62+ =?> unlines [ "[first][] [second]"
63+ , ""
64+ , " [first]: /url1 \"title1\""
65+ , " [second]: /url2 \"title2\""
66+ ]
67+ , "Reference link is used multiple times (unshortcutable)"
68+ =: (para ((link "/url1" "" "foo") <> (link "/url2" "" "foo")
69+ <> (link "/url3" "" "foo")))
70+ =?> unlines [ "[foo][][foo][1][foo][2]"
71+ , ""
72+ , " [foo]: /url1"
73+ , " [1]: /url2"
74+ , " [2]: /url3"
75+ ]
76+ , "Reference link is used multiple times (unshortcutable)"
77+ =: (para ((link "/url1" "" "foo") <> " " <> (link "/url2" "" "foo")
78+ <> " " <> (link "/url3" "" "foo")))
79+ =?> unlines [ "[foo][] [foo][1] [foo][2]"
80+ , ""
81+ , " [foo]: /url1"
82+ , " [1]: /url2"
83+ , " [2]: /url3"
84+ ]
85+ , "Reference link is followed by text in brackets"
86+ =: (para ((link "/url" "" "link") <> "[text in brackets]"))
87+ =?> unlines [ "[link][][text in brackets]"
88+ , ""
89+ , " [link]: /url"
90+ ]
91+ , "Reference link is followed by space and text in brackets"
92+ =: (para ((link "/url" "" "link") <> " [text in brackets]"))
93+ =?> unlines [ "[link][] [text in brackets]"
94+ , ""
95+ , " [link]: /url"
96+ ]
97+ , "Reference link is followed by RawInline"
98+ =: (para ((link "/url" "" "link") <> rawInline "markdown" "[rawText]"))
99+ =?> unlines [ "[link][][rawText]"
100+ , ""
101+ , " [link]: /url"
102+ ]
103+ , "Reference link is followed by space and RawInline"
104+ =: (para ((link "/url" "" "link") <> space <> rawInline "markdown" "[rawText]"))
105+ =?> unlines [ "[link][] [rawText]"
106+ , ""
107+ , " [link]: /url"
108+ ]
109+ , "Reference link is followed by RawInline with space"
110+ =: (para ((link "/url" "" "link") <> rawInline "markdown" " [rawText]"))
111+ =?> unlines [ "[link][] [rawText]"
112+ , ""
113+ , " [link]: /url"
114+ ]
115+ , "Reference link is followed by citation"
116+ =: (para ((link "/url" "" "link") <> cite [Citation "author" [] [] NormalCitation 0 0] (str "[@author]")))
117+ =?> unlines [ "[link][][@author]"
118+ , ""
119+ , " [link]: /url"
120+ ]
121+ , "Reference link is followed by space and citation"
122+ =: (para ((link "/url" "" "link") <> space <> cite [Citation "author" [] [] NormalCitation 0 0] (str "[@author]")))
123+ =?> unlines [ "[link][] [@author]"
124+ , ""
125+ , " [link]: /url"
126+ ]
127+ ]