git.iko.soy/typeable/pandoc
on master help sign in
Implement task lists (#5139)

Closes #3051

commitf1d83aea12b93b31f5218bed75bd0e9d8d373cb6 authorMauro Bieg <mb21@users.noreply.github.com> dateWed 2 Jan 2019, 20:36 (8 years ago) parent9097ec4
11 files changed+247−20
MANUAL.txt+9−2 src/Text/Pandoc/Extensions.hs+3 src/Text/Pandoc/Readers/CommonMark.hs+5−2 src/Text/Pandoc/Readers/Markdown.hs+2−1 src/Text/Pandoc/Shared.hs+32 src/Text/Pandoc/Writers/CommonMark.hs+16−9 src/Text/Pandoc/Writers/HTML.hs+20−2 src/Text/Pandoc/Writers/LaTeX.hs+14−2 src/Text/Pandoc/Writers/Markdown.hs+4−2 test/command/gfm.md+29 test/command/tasklist.md+113
MANUAL.txt+9−2
@@ -2612,6 +2612,13 @@ If default list markers are desired, use `#.`:
26122612 #. two
26132613 #. three
26142614
2615+#### Extension: `task_lists` ####
2616+
2617+Pandoc supports task lists, using the syntax of GitHub-Flavored Markdown.
2618+
2619+ - [ ] an unchecked task list item
2620+ - [x] checked item
2621+
26152622
26162623### Definition lists ###
26172624
@@ -4223,7 +4230,7 @@ variants are supported:
42234230: `pipe_tables`, `raw_html`, `fenced_code_blocks`, `auto_identifiers`,
42244231 `gfm_auto_identifiers`, `backtick_code_blocks`,
42254232 `autolink_bare_uris`, `space_in_atx_header`,
4226− `intraword_underscores`, `strikeout`, `emoji`,
4233+ `intraword_underscores`, `strikeout`, `task_lists`, `emoji`,
42274234 `shortcut_reference_links`, `angle_brackets_escapable`,
42284235 `lists_without_preceding_blankline`.
42294236
@@ -4254,7 +4261,7 @@ only affects `gfm` output, not input.
42544261: `pipe_tables`, `raw_html`, `fenced_code_blocks`, `auto_identifiers`,
42554262 `gfm_auto_identifiers`, `backtick_code_blocks`,
42564263 `autolink_bare_uris`, `space_in_atx_header`,
4257− `intraword_underscores`, `strikeout`, `emoji`,
4264+ `intraword_underscores`, `strikeout`, `task_lists`, `emoji`,
42584265 `shortcut_reference_links`, `angle_brackets_escapable`,
42594266 `lists_without_preceding_blankline`.
42604267
src/Text/Pandoc/Extensions.hs+3
@@ -167,6 +167,7 @@ data Extension =
167167 | Ext_subscript -- ^ Subscript using ~this~ syntax
168168 | Ext_superscript -- ^ Superscript using ^this^ syntax
169169 | Ext_styles -- ^ Read styles that pandoc doesn't know
170+ | Ext_task_lists -- ^ Parse certain list items as task list items
170171 | Ext_table_captions -- ^ Pandoc-style table captions
171172 | Ext_tex_math_dollars -- ^ TeX math between $..$ or $$..$$
172173 | Ext_tex_math_double_backslash -- ^ TeX math btw \\(..\\) \\[..\\]
@@ -215,6 +216,7 @@ pandocExtensions = extensionsFromList
215216 , Ext_strikeout
216217 , Ext_superscript
217218 , Ext_subscript
219+ , Ext_task_lists
218220 , Ext_auto_identifiers
219221 , Ext_header_attributes
220222 , Ext_link_attributes
@@ -274,6 +276,7 @@ githubMarkdownExtensions = extensionsFromList
274276 , Ext_space_in_atx_header
275277 , Ext_intraword_underscores
276278 , Ext_strikeout
279+ , Ext_task_lists
277280 , Ext_emoji
278281 , Ext_lists_without_preceding_blankline
279282 , Ext_shortcut_reference_links
src/Text/Pandoc/Readers/CommonMark.hs+5−2
@@ -43,7 +43,7 @@ import Text.Pandoc.Class (PandocMonad)
4343import Text.Pandoc.Definition
4444import Text.Pandoc.Emoji (emojiToInline)
4545import Text.Pandoc.Options
46−import Text.Pandoc.Shared (uniqueIdent)
46+import Text.Pandoc.Shared (uniqueIdent, taskListItemFromAscii)
4747import Text.Pandoc.Walk (walkM)
4848
4949-- | Parse a CommonMark formatted string into a 'Pandoc' structure.
@@ -111,12 +111,14 @@ addBlock _ (Node _ (CODE_BLOCK info t) _) =
111111addBlock opts (Node _ (HEADING lev) nodes) =
112112 (Header lev ("",[],[]) (addInlines opts nodes) :)
113113addBlock opts (Node _ (LIST listAttrs) nodes) =
114− (constructor (map (setTightness . addBlocks opts . children) nodes) :)
114+ (constructor (map listItem nodes) :)
115115 where constructor = case listType listAttrs of
116116 BULLET_LIST -> BulletList
117117 ORDERED_LIST -> OrderedList
118118 (start, DefaultStyle, delim)
119119 start = listStart listAttrs
120+ listItem = taskListItemFromAscii exts . setTightness
121+ . addBlocks opts . children
120122 setTightness = if listTight listAttrs
121123 then map paraToPlain
122124 else id
@@ -125,6 +127,7 @@ addBlock opts (Node _ (LIST listAttrs) nodes) =
125127 delim = case listDelim listAttrs of
126128 PERIOD_DELIM -> Period
127129 PAREN_DELIM -> OneParen
130+ exts = readerExtensions opts
128131addBlock opts (Node _ (TABLE alignments) nodes) =
129132 (Table [] aligns widths headers rows :)
130133 where aligns = map fromTableCellAlignment alignments
src/Text/Pandoc/Readers/Markdown.hs+2−1
@@ -958,7 +958,8 @@ listItem fourSpaceRule start = try $ do
958958 let raw = concat (first:continuations)
959959 contents <- parseFromString' parseBlocks raw
960960 updateState (\st -> st {stateParserContext = oldContext})
961− return contents
961+ exts <- getOption readerExtensions
962+ return $ B.fromList . taskListItemFromAscii exts . B.toList <$> contents
962963
963964orderedList :: PandocMonad m => MarkdownParser m (F Blocks)
964965orderedList = try $ do
src/Text/Pandoc/Shared.hs+32
@@ -79,6 +79,8 @@ module Text.Pandoc.Shared (
7979 headerShift,
8080 stripEmptyParagraphs,
8181 isTightList,
82+ taskListItemFromAscii,
83+ taskListItemToAscii,
8284 addMetaField,
8385 makeMeta,
8486 eastAsianLineBreakFilter,
@@ -588,6 +590,36 @@ isTightList = all firstIsPlain
588590 where firstIsPlain (Plain _ : _) = True
589591 firstIsPlain _ = False
590592
593+-- | Convert a list item containing tasklist syntax (e.g. @[x]@)
594+-- to using @U+2610 BALLOT BOX@ or @U+2612 BALLOT BOX WITH X@.
595+taskListItemFromAscii :: Extensions -> [Block] -> [Block]
596+taskListItemFromAscii = handleTaskListItem fromMd
597+ where
598+ fromMd (Str "[" : Space : Str "]" : Space : is) = (Str "☐") : Space : is
599+ fromMd (Str "[x]" : Space : is) = (Str "☒") : Space : is
600+ fromMd (Str "[X]" : Space : is) = (Str "☒") : Space : is
601+ fromMd is = is
602+
603+-- | Convert a list item containing text starting with @U+2610 BALLOT BOX@
604+-- or @U+2612 BALLOT BOX WITH X@ to tasklist syntax (e.g. @[x]@).
605+taskListItemToAscii :: Extensions -> [Block] -> [Block]
606+taskListItemToAscii = handleTaskListItem toMd
607+ where
608+ toMd (Str "☐" : Space : is) = rawMd "[ ]" : Space : is
609+ toMd (Str "☒" : Space : is) = rawMd "[x]" : Space : is
610+ toMd is = is
611+ rawMd = RawInline (Format "markdown")
612+
613+handleTaskListItem :: ([Inline] -> [Inline]) -> Extensions -> [Block] -> [Block]
614+handleTaskListItem handleInlines exts bls =
615+ if Ext_task_lists `extensionEnabled` exts
616+ then handleItem bls
617+ else bls
618+ where
619+ handleItem (Plain is : bs) = Plain (handleInlines is) : bs
620+ handleItem (Para is : bs) = Para (handleInlines is) : bs
621+ handleItem bs = bs
622+
591623-- | Set a field of a 'Meta' object. If the field already has a value,
592624-- convert it into a list with the new value appended to the old value(s).
593625addMetaField :: ToMetaValue a
src/Text/Pandoc/Writers/CommonMark.hs+16−9
@@ -46,7 +46,8 @@ import Network.HTTP (urlEncode)
4646import Text.Pandoc.Class (PandocMonad)
4747import Text.Pandoc.Definition
4848import Text.Pandoc.Options
49−import Text.Pandoc.Shared (isTightList, linesToPara, substitute, capitalize)
49+import Text.Pandoc.Shared (isTightList, taskListItemToAscii, linesToPara,
50+ substitute, capitalize)
5051import Text.Pandoc.Templates (renderTemplate')
5152import Text.Pandoc.Walk (query, walk, walkM)
5253import Text.Pandoc.Writers.HTML (writeHtml5String, tagWithAttributes)
@@ -115,24 +116,28 @@ blockToNodes opts (Para xs) ns =
115116blockToNodes opts (LineBlock lns) ns = blockToNodes opts (linesToPara lns) ns
116117blockToNodes _ (CodeBlock (_,classes,_) xs) ns = return
117118 (node (CODE_BLOCK (T.pack (unwords classes)) (T.pack xs)) [] : ns)
118−blockToNodes opts (RawBlock fmt xs) ns
119− | fmt == Format "html" && isEnabled Ext_raw_html opts
119+blockToNodes opts (RawBlock (Format f) xs) ns
120+ | f == "html" && isEnabled Ext_raw_html opts
120121 = return (node (HTML_BLOCK (T.pack xs)) [] : ns)
121− | (fmt == Format "latex" || fmt == Format "tex") && isEnabled Ext_raw_tex opts
122+ | (f == "latex" || f == "tex") && isEnabled Ext_raw_tex opts
123+ = return (node (CUSTOM_BLOCK (T.pack xs) T.empty) [] : ns)
124+ | f == "markdown"
122125 = return (node (CUSTOM_BLOCK (T.pack xs) T.empty) [] : ns)
123126 | otherwise = return ns
124127blockToNodes opts (BlockQuote bs) ns = do
125128 nodes <- blocksToNodes opts bs
126129 return (node BLOCK_QUOTE nodes : ns)
127130blockToNodes opts (BulletList items) ns = do
128− nodes <- mapM (blocksToNodes opts) items
131+ let exts = writerExtensions opts
132+ nodes <- mapM (blocksToNodes opts . taskListItemToAscii exts) items
129133 return (node (LIST ListAttributes{
130134 listType = BULLET_LIST,
131135 listDelim = PERIOD_DELIM,
132136 listTight = isTightList items,
133137 listStart = 1 }) (map (node ITEM) nodes) : ns)
134138blockToNodes opts (OrderedList (start, _sty, delim) items) ns = do
135− nodes <- mapM (blocksToNodes opts) items
139+ let exts = writerExtensions opts
140+ nodes <- mapM (blocksToNodes opts . taskListItemToAscii exts) items
136141 return (node (LIST ListAttributes{
137142 listType = ORDERED_LIST,
138143 listDelim = case delim of
@@ -292,10 +297,12 @@ inlineToNodes opts (Image alt ils (url,'f':'i':'g':':':tit)) =
292297 inlineToNodes opts (Image alt ils (url,tit))
293298inlineToNodes opts (Image _ ils (url,tit)) =
294299 (node (IMAGE (T.pack url) (T.pack tit)) (inlinesToNodes opts ils) :)
295−inlineToNodes opts (RawInline fmt xs)
296− | fmt == Format "html" && isEnabled Ext_raw_html opts
300+inlineToNodes opts (RawInline (Format f) xs)
301+ | f == "html" && isEnabled Ext_raw_html opts
297302 = (node (HTML_INLINE (T.pack xs)) [] :)
298− | (fmt == Format "latex" || fmt == Format "tex") && isEnabled Ext_raw_tex opts
303+ | (f == "latex" || f == "tex") && isEnabled Ext_raw_tex opts
304+ = (node (CUSTOM_INLINE (T.pack xs) T.empty) [] :)
305+ | f == "markdown"
299306 = (node (CUSTOM_INLINE (T.pack xs) T.empty) [] :)
300307 | otherwise = id
301308inlineToNodes opts (Quoted qt ils) =
src/Text/Pandoc/Writers/HTML.hs+20−2
@@ -365,6 +365,24 @@ defList :: PandocMonad m
365365 => WriterOptions -> [Html] -> StateT WriterState m Html
366366defList opts items = toList H.dl opts (items ++ [nl opts])
367367
368+listItemToHtml :: PandocMonad m
369+ => WriterOptions -> [Block] -> StateT WriterState m Html
370+listItemToHtml opts bls
371+ | Plain (Str "☐":Space:is) : bs <- bls = taskListItem False id is bs
372+ | Plain (Str "☒":Space:is) : bs <- bls = taskListItem True id is bs
373+ | Para (Str "☐":Space:is) : bs <- bls = taskListItem False H.p is bs
374+ | Para (Str "☒":Space:is) : bs <- bls = taskListItem True H.p is bs
375+ | otherwise = blockListToHtml opts bls
376+ where
377+ taskListItem checked constr is bs = do
378+ let checkbox = if checked
379+ then checkbox' ! A.checked ""
380+ else checkbox'
381+ checkbox' = H.input ! A.type_ "checkbox" ! A.disabled "" >> nl opts
382+ isContents <- inlineListToHtml opts is
383+ bsContents <- blockListToHtml opts bs
384+ return $ constr (checkbox >> isContents) >> bsContents
385+
368386-- | Construct table of contents from list of elements.
369387tableOfContents :: PandocMonad m => WriterOptions -> [Element]
370388 -> StateT WriterState m (Maybe Html)
@@ -824,10 +842,10 @@ blockToHtml opts (Header level attr@(_,classes,_) lst) = do
824842 6 -> H.h6 contents'
825843 _ -> H.p contents'
826844blockToHtml opts (BulletList lst) = do
827− contents <- mapM (blockListToHtml opts) lst
845+ contents <- mapM (listItemToHtml opts) lst
828846 unordList opts contents
829847blockToHtml opts (OrderedList (startnum, numstyle, _) lst) = do
830− contents <- mapM (blockListToHtml opts) lst
848+ contents <- mapM (listItemToHtml opts) lst
831849 html5 <- gets stHtml5
832850 let numstyle' = case numstyle of
833851 Example -> "decimal"
src/Text/Pandoc/Writers/LaTeX.hs+14−2
@@ -924,8 +924,20 @@ listItemToLaTeX lst
924924 -- this will keep the typesetter from throwing an error.
925925 | (Header{} :_) <- lst =
926926 blockListToLaTeX lst >>= return . (text "\\item ~" $$) . nest 2
927− | otherwise = blockListToLaTeX lst >>= return . (text "\\item" $$) .
928− nest 2
927+ | Plain (Str "☐":Space:is) : bs <- lst = taskListItem False is bs
928+ | Plain (Str "☒":Space:is) : bs <- lst = taskListItem True is bs
929+ | Para (Str "☐":Space:is) : bs <- lst = taskListItem False is bs
930+ | Para (Str "☒":Space:is) : bs <- lst = taskListItem True is bs
931+ | otherwise = blockListToLaTeX lst >>= return . (text "\\item" $$) . nest 2
932+ where
933+ taskListItem checked is bs = do
934+ let checkbox = if checked
935+ then "$\\boxtimes$"
936+ else "$\\square$"
937+ isContents <- inlineListToLaTeX is
938+ bsContents <- blockListToLaTeX bs
939+ return $ "\\item" <> brackets checkbox
940+ $$ nest 2 (isContents $+$ bsContents)
929941
930942defListItemToLaTeX :: PandocMonad m => ([Inline], [[Block]]) -> LW m Doc
931943defListItemToLaTeX (term, defs) = do
src/Text/Pandoc/Writers/Markdown.hs+4−2
@@ -765,7 +765,8 @@ itemEndsWithTightList bs =
765765-- | Convert bullet list item (list of blocks) to markdown.
766766bulletListItemToMarkdown :: PandocMonad m => WriterOptions -> [Block] -> MD m Doc
767767bulletListItemToMarkdown opts bs = do
768− contents <- blockListToMarkdown opts bs
768+ let exts = writerExtensions opts
769+ contents <- blockListToMarkdown opts $ taskListItemToAscii exts bs
769770 let sps = replicate (writerTabStop opts - 2) ' '
770771 let start = text ('-' : ' ' : sps)
771772 -- remove trailing blank line if item ends with a tight list
@@ -781,7 +782,8 @@ orderedListItemToMarkdown :: PandocMonad m
781782 -> [Block] -- ^ list item (list of blocks)
782783 -> MD m Doc
783784orderedListItemToMarkdown opts marker bs = do
784− contents <- blockListToMarkdown opts bs
785+ let exts = writerExtensions opts
786+ contents <- blockListToMarkdown opts $ taskListItemToAscii exts bs
785787 let sps = case length marker - writerTabStop opts of
786788 n | n > 0 -> text $ replicate n ' '
787789 _ -> text " "
test/command/gfm.md+29
@@ -101,3 +101,32 @@ hi
101101^D
102102[Para [Str "hi",LineBreak,Str "hi"]]
103103```
104+
105+```
106+% pandoc -f gfm -t native
107+- [ ] foo
108+- [x] bar
109+^D
110+[BulletList
111+ [[Plain [Str "\9744",Space,Str "foo"]]
112+ ,[Plain [Str "\9746",Space,Str "bar"]]]]
113+```
114+
115+```
116+% pandoc -f gfm-task_lists -t native
117+- [ ] foo
118+- [x] bar
119+^D
120+[BulletList
121+ [[Plain [Str "[",Space,Str "]",Space,Str "foo"]]
122+ ,[Plain [Str "[x]",Space,Str "bar"]]]]
123+```
124+
125+```
126+% pandoc -f gfm -t gfm
127+- [ ] foo
128+- [x] bar
129+^D
130+ - [ ] foo
131+ - [x] bar
132+```
test/command/tasklist.mdnew file+113
@@ -0,0 +1,113 @@
1+tests adapted from <https://github.github.com/gfm/#task-list-items-extension->
2+
3+```
4+% pandoc
5+- [ ] foo
6+- [x] bar
7+^D
8+<ul>
9+<li><input type="checkbox" disabled="" />
10+foo</li>
11+<li><input type="checkbox" disabled="" checked="" />
12+bar</li>
13+</ul>
14+```
15+
16+
17+```
18+% pandoc
19+- [x] foo
20+ - [ ] bar
21+ - [x] baz
22+- [ ] bim
23+^D
24+<ul>
25+<li><input type="checkbox" disabled="" checked="" />
26+foo<ul>
27+<li><input type="checkbox" disabled="" />
28+bar</li>
29+<li><input type="checkbox" disabled="" checked="" />
30+baz</li>
31+</ul></li>
32+<li><input type="checkbox" disabled="" />
33+bim</li>
34+</ul>
35+```
36+
37+
38+custom html task list test:
39+
40+```
41+% pandoc
42+- [ ] unchecked
43+- plain item
44+- [x] checked
45+
46+paragraph
47+
48+1. [ ] ordered unchecked
49+2. [] plain item
50+3. [x] ordered checked
51+
52+paragraph
53+
54+- [ ] list item with a
55+
56+ second paragraph
57+
58+- [x] checked
59+^D
60+<ul>
61+<li><input type="checkbox" disabled="" />
62+unchecked</li>
63+<li>plain item</li>
64+<li><input type="checkbox" disabled="" checked="" />
65+checked</li>
66+</ul>
67+<p>paragraph</p>
68+<ol type="1">
69+<li><input type="checkbox" disabled="" />
70+ordered unchecked</li>
71+<li>[] plain item</li>
72+<li><input type="checkbox" disabled="" checked="" />
73+ordered checked</li>
74+</ol>
75+<p>paragraph</p>
76+<ul>
77+<li><p><input type="checkbox" disabled="" />
78+list item with a</p><p>second paragraph</p></li>
79+<li><p><input type="checkbox" disabled="" checked="" />
80+checked</p></li>
81+</ul>
82+```
83+
84+latex task list test:
85+
86+```
87+% pandoc -t latex
88+- [ ] foo bar
89+
90+ baz
91+
92+- [x] ok
93+^D
94+\begin{itemize}
95+\item[$\square$]
96+ foo bar
97+
98+ baz
99+\item[$\boxtimes$]
100+ ok
101+\end{itemize}
102+```
103+
104+round trip:
105+
106+```
107+% pandoc -f markdown -t markdown
108+- [ ] foo
109+- [x] bar
110+^D
111+- [ ] foo
112+- [x] bar
113+```