git.iko.soy/typeable/pandoc
on master help sign in
Lua: add Version type to simplify comparisons

Version specifiers like `PANDOC_VERSION` and `PANDOC_API_VERSION` are turned into `Version` objects. The objects simplify version-appropriate comparisons while maintaining backward-compatibility. A function `pandoc.types.Version` is added as part of the newly introduced module `pandoc.types`, allowing users to create version objects in scripts.

commit505f5bf5d951a5c4342f7acce9bea5f260dc9d78 authorAlbert Krewinkel <albert@zeitkraut.de> dateSun 19 May 2019, 15:26 (7 years ago) parentd07ed83
12 files changed+353−23
doc/lua-filters.md+42−9 pandoc.cabal+3−2 src/Text/Pandoc/Lua/Global.hs+2−3 src/Text/Pandoc/Lua/Marshaling.hs+1 src/Text/Pandoc/Lua/Marshaling/Version.hs+154 src/Text/Pandoc/Lua/Module/Types.hs+28 src/Text/Pandoc/Lua/Module/Utils.hs+2 src/Text/Pandoc/Lua/Packages.hs+2 test/Tests/Lua.hs+5−8 test/Tests/Lua/Module.hs+2−1 test/lua/module/pandoc-types.lua+112 test/lua/module/pandoc.utils.lua → test/lua/module/pandoc-utils.lua
doc/lua-filters.md+42−9
@@ -151,21 +151,21 @@ variables.
151151: Table of the options which were provided to the parser.
152152
153153`PANDOC_VERSION`
154−: Contains the pandoc version as a numerically indexed table,
155− most significant number first. E.g., for pandoc 2.1.1, the
156− value of the variable is a table `{2, 1, 1}`. Use
157− `table.concat(PANDOC_VERSION, '.')` to produce a version
158− string. This variable is also set in custom writers.
154+: Contains the pandoc version as a [Version object] which
155+ behaves like a numerically indexed table, most significant
156+ number first. E.g., for pandoc 2.7.3, the value of the
157+ variable is equivalent to a table `{2, 7, 3}`. Use
158+ `tostring(PANDOC_VERSION)` to produce a version string. This
159+ variable is also set in custom writers.
159160
160161`PANDOC_API_VERSION`
161162: Contains the version of the pandoc-types API against which
162163 pandoc was compiled. It is given as a numerically indexed
163164 table, most significant number first. E.g., if pandoc was
164165 compiled against pandoc-types 1.17.3, then the value of the
165− variable will be a table `{1, 17, 3}`. Use
166− `table.concat(PANDOC_API_VERSION, '.')` to produce a version
167− string from this table. This variable is also set in custom
168− writers.
166+ variable will behave like the table `{1, 17, 3}`. Use
167+ `tostring(PANDOC_API_VERSION)` to produce a version string.
168+ This variable is also set in custom writers.
169169
170170`PANDOC_SCRIPT_FILE`
171171: The name used to involve the filter. This value can be used
@@ -178,6 +178,8 @@ variables.
178178 variable is of type [CommonState](#type-ref-CommonState) and
179179 is read-only.
180180
181+[Version object]: #type-ref-Version
182+
181183# Pandoc Module
182184
183185The `pandoc` lua module is loaded into the filter's lua
@@ -1353,6 +1355,32 @@ available to readers and writers.
13531355A pandoc log message. Object have no fields, but can be converted
13541356to a string via `tostring`.
13551357
1358+## Version {#type-ref-Version}
1359+
1360+A version object. This represents a software version like
1361+"2.7.3". The object behaves like a numerically indexed table,
1362+i.e., if `version` represents the version `2.7.3`, then
1363+
1364+ version[1] == 2
1365+ version[2] == 7
1366+ version[3] == 3
1367+ #version == 3 -- length
1368+
1369+Comparisons are performed element-wise, i.e.
1370+
1371+ Version '1.12' > Version '1.9'
1372+
1373+### `must_be_at_least`
1374+
1375+`must_be_at_least(actual, expected [, error_message])`
1376+
1377+Raise an error message if the actual version is older than the
1378+expected version.
1379+
1380+Usage:
1381+
1382+ PANDOC_VERSION:must_be_at_least('2.7.3')
1383+
13561384[Block]: #type-ref-Block
13571385[List]: #module-pandoc.list
13581386[MetaValue]: #type-ref-MetaValue
@@ -2726,3 +2754,8 @@ Parameters:
27262754Returns:
27272755
27282756- The result(s) of the call to `callback`
2757+
2758+
2759+# Module pandoc.types
2760+
2761+Constructors for types which are not part of the pandoc AST.
pandoc.cabal+3−2
@@ -339,8 +339,7 @@ extra-source-files:
339339 test/odt/markdown/*.md
340340 test/odt/native/*.native
341341 test/lua/*.lua
342− test/lua/module/pandoc.lua
343− test/lua/module/pandoc.utils.lua
342+ test/lua/module/*.lua
344343source-repository head
345344 type: git
346345 location: git://github.com/jgm/pandoc.git
@@ -593,9 +592,11 @@ library
593592 Text.Pandoc.Lua.Marshaling.AnyValue,
594593 Text.Pandoc.Lua.Marshaling.CommonState,
595594 Text.Pandoc.Lua.Marshaling.ReaderOptions,
595+ Text.Pandoc.Lua.Marshaling.Version,
596596 Text.Pandoc.Lua.Module.MediaBag,
597597 Text.Pandoc.Lua.Module.Pandoc,
598598 Text.Pandoc.Lua.Module.System,
599+ Text.Pandoc.Lua.Module.Types,
599600 Text.Pandoc.Lua.Module.Utils,
600601 Text.Pandoc.Lua.Packages,
601602 Text.Pandoc.Lua.Util,
src/Text/Pandoc/Lua/Global.hs+2−3
@@ -17,7 +17,6 @@ module Text.Pandoc.Lua.Global
1717
1818import Prelude
1919import Data.Data (Data)
20−import Data.Version (Version (versionBranch))
2120import Foreign.Lua (Lua, Peekable, Pushable)
2221import Foreign.Lua.Userdata ( ensureUserdataMetatable, pushAnyWithMetatable
2322 , metatableName)
@@ -52,7 +51,7 @@ setGlobal global = case global of
5251 Lua.push format
5352 Lua.setglobal "FORMAT"
5453 PANDOC_API_VERSION -> do
55− Lua.push (versionBranch pandocTypesVersion)
54+ Lua.push pandocTypesVersion
5655 Lua.setglobal "PANDOC_API_VERSION"
5756 PANDOC_DOCUMENT doc -> do
5857 Lua.push (LazyPandoc doc)
@@ -67,7 +66,7 @@ setGlobal global = case global of
6766 Lua.push commonState
6867 Lua.setglobal "PANDOC_STATE"
6968 PANDOC_VERSION -> do
70− Lua.push (versionBranch version)
69+ Lua.push version
7170 Lua.setglobal "PANDOC_VERSION"
7271
7372-- | Readonly and lazy pandoc objects.
src/Text/Pandoc/Lua/Marshaling.hs+1
@@ -14,3 +14,4 @@ module Text.Pandoc.Lua.Marshaling () where
1414import Text.Pandoc.Lua.Marshaling.AST ()
1515import Text.Pandoc.Lua.Marshaling.CommonState ()
1616import Text.Pandoc.Lua.Marshaling.ReaderOptions ()
17+import Text.Pandoc.Lua.Marshaling.Version ()
src/Text/Pandoc/Lua/Marshaling/Version.hsnew file+154
@@ -0,0 +1,154 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+{-# LANGUAGE LambdaCase #-}
3+{-# LANGUAGE NoImplicitPrelude #-}
4+{- |
5+ Module : Text.Pandoc.Lua.Marshaling.Version
6+ Copyright : © 2019 Albert Krewinkel
7+ License : GNU GPL, version 2 or above
8+
9+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
10+ Stability : alpha
11+
12+Marshaling of @'Version'@s. The marshaled elements can be compared using
13+default comparison operators (like @>@ and @<=@).
14+-}
15+module Text.Pandoc.Lua.Marshaling.Version
16+ ( peekVersion
17+ , pushVersion
18+ )
19+ where
20+
21+import Prelude
22+import Data.Maybe (fromMaybe)
23+import Data.Version (Version (..), makeVersion, parseVersion, showVersion)
24+import Foreign.Lua (Lua, Optional (..), NumResults,
25+ Peekable, Pushable, StackIndex)
26+import Foreign.Lua.Types.Peekable (reportValueOnFailure)
27+import Foreign.Lua.Userdata (ensureUserdataMetatable, pushAnyWithMetatable,
28+ toAnyWithName)
29+import Safe (atMay, lastMay)
30+import Text.Pandoc.Lua.Marshaling.AnyValue (AnyValue (..))
31+import Text.ParserCombinators.ReadP (readP_to_S)
32+
33+import qualified Foreign.Lua as Lua
34+import qualified Text.Pandoc.Lua.Util as LuaUtil
35+
36+-- | Push a @'Version'@ element to the Lua stack.
37+pushVersion :: Version -> Lua ()
38+pushVersion version = pushAnyWithMetatable pushVersionMT version
39+ where
40+ pushVersionMT = ensureUserdataMetatable versionTypeName $ do
41+ LuaUtil.addFunction "__eq" __eq
42+ LuaUtil.addFunction "__le" __le
43+ LuaUtil.addFunction "__lt" __lt
44+ LuaUtil.addFunction "__len" __len
45+ LuaUtil.addFunction "__index" __index
46+ LuaUtil.addFunction "__pairs" __pairs
47+ LuaUtil.addFunction "__tostring" __tostring
48+
49+instance Pushable Version where
50+ push = pushVersion
51+
52+peekVersion :: StackIndex -> Lua Version
53+peekVersion idx = Lua.ltype idx >>= \case
54+ Lua.TypeString -> do
55+ versionStr <- Lua.peek idx
56+ let parses = readP_to_S parseVersion versionStr
57+ case lastMay parses of
58+ Just (v, "") -> return v
59+ _ -> Lua.throwException $ "could not parse as Version: " ++ versionStr
60+
61+ Lua.TypeUserdata ->
62+ reportValueOnFailure versionTypeName
63+ (`toAnyWithName` versionTypeName)
64+ idx
65+ Lua.TypeNumber -> do
66+ n <- Lua.peek idx
67+ return (makeVersion [n])
68+
69+ Lua.TypeTable ->
70+ makeVersion <$> Lua.peek idx
71+
72+ _ ->
73+ Lua.throwException "could not peek Version"
74+
75+instance Peekable Version where
76+ peek = peekVersion
77+
78+-- | Name used by Lua for the @CommonState@ type.
79+versionTypeName :: String
80+versionTypeName = "HsLua Version"
81+
82+__eq :: Version -> Version -> Lua Bool
83+__eq v1 v2 = return (v1 == v2)
84+
85+__le :: Version -> Version -> Lua Bool
86+__le v1 v2 = return (v1 <= v2)
87+
88+__lt :: Version -> Version -> Lua Bool
89+__lt v1 v2 = return (v1 < v2)
90+
91+-- | Get number of version components.
92+__len :: Version -> Lua Int
93+__len = return . length . versionBranch
94+
95+-- | Access fields.
96+__index :: Version -> AnyValue -> Lua NumResults
97+__index v (AnyValue k) = do
98+ ty <- Lua.ltype k
99+ case ty of
100+ Lua.TypeNumber -> do
101+ n <- Lua.peek k
102+ let versionPart = atMay (versionBranch v) (n - 1)
103+ Lua.push (Lua.Optional versionPart)
104+ return 1
105+ Lua.TypeString -> do
106+ str <- Lua.peek k
107+ if str == "must_be_at_least"
108+ then 1 <$ Lua.pushHaskellFunction must_be_at_least
109+ else 1 <$ Lua.pushnil
110+ _ -> 1 <$ Lua.pushnil
111+
112+-- | Create iterator.
113+__pairs :: Version -> Lua NumResults
114+__pairs v = do
115+ Lua.pushHaskellFunction nextFn
116+ Lua.pushnil
117+ Lua.pushnil
118+ return 3
119+ where
120+ nextFn :: AnyValue -> Optional Int -> Lua Lua.NumResults
121+ nextFn _ (Optional key) =
122+ case key of
123+ Nothing -> case versionBranch v of
124+ [] -> 2 <$ (Lua.pushnil *> Lua.pushnil)
125+ n:_ -> 2 <$ (Lua.push (1 :: Int) *> Lua.push n)
126+ Just n -> case atMay (versionBranch v) n of
127+ Nothing -> 2 <$ (Lua.pushnil *> Lua.pushnil)
128+ Just b -> 2 <$ (Lua.push (n + 1) *> Lua.push b)
129+
130+-- | Convert to string.
131+__tostring :: Version -> Lua String
132+__tostring v = return (showVersion v)
133+
134+-- | Default error message when a version is too old. This message is
135+-- formatted in Lua with the expected and actual versions as arguments.
136+versionTooOldMessage :: String
137+versionTooOldMessage = "version too old: expected version %s or newer, got %s"
138+
139+-- | Throw an error if this version is older than the given version.
140+-- FIXME: This function currently requires the string library to be
141+-- loaded.
142+must_be_at_least :: Version -> Version -> Optional String -> Lua NumResults
143+must_be_at_least actual expected optMsg = do
144+ let msg = fromMaybe versionTooOldMessage (fromOptional optMsg)
145+ if expected <= actual
146+ then return 0
147+ else do
148+ Lua.getglobal' "string.format"
149+ Lua.push msg
150+ Lua.push (showVersion expected)
151+ Lua.push (showVersion actual)
152+ Lua.call 3 1
153+ Lua.error
154+
src/Text/Pandoc/Lua/Module/Types.hsnew file+28
@@ -0,0 +1,28 @@
1+{- |
2+ Module : Text.Pandoc.Lua.Module.Types
3+ Copyright : © 2019 Albert Krewinkel
4+ License : GNU GPL, version 2 or above
5+
6+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
7+ Stability : alpha
8+
9+Pandoc data type constructors.
10+-}
11+module Text.Pandoc.Lua.Module.Types
12+ ( pushModule
13+ ) where
14+
15+import Prelude
16+import Data.Version (Version)
17+import Foreign.Lua (Lua, NumResults)
18+import Text.Pandoc.Lua.Marshaling.Version ()
19+import Text.Pandoc.Lua.Util (addFunction)
20+
21+import qualified Foreign.Lua as Lua
22+
23+-- | Push the pandoc.system module on the Lua stack.
24+pushModule :: Lua NumResults
25+pushModule = do
26+ Lua.newtable
27+ addFunction "Version" (return :: Version -> Lua Version)
28+ return 1
src/Text/Pandoc/Lua/Module/Utils.hs+2
@@ -17,6 +17,7 @@ import Prelude
1717import Control.Applicative ((<|>))
1818import Data.Char (toLower)
1919import Data.Default (def)
20+import Data.Version (Version)
2021import Foreign.Lua (Peekable, Lua, NumResults)
2122import Text.Pandoc.Class (runIO, setUserDataDir)
2223import Text.Pandoc.Definition ( Pandoc, Meta, MetaValue (..), Block, Inline
@@ -43,6 +44,7 @@ pushModule mbDatadir = do
4344 addFunction "sha1" sha1
4445 addFunction "stringify" stringify
4546 addFunction "to_roman_numeral" toRomanNumeral
47+ addFunction "Version" (return :: Version -> Lua Version)
4648 return 1
4749
4850-- | Squashes a list of blocks into inlines.
src/Text/Pandoc/Lua/Packages.hs+2
@@ -26,6 +26,7 @@ import qualified Foreign.Lua as Lua
2626import Text.Pandoc.Lua.Module.Pandoc as Pandoc
2727import Text.Pandoc.Lua.Module.MediaBag as MediaBag
2828import Text.Pandoc.Lua.Module.System as System
29+import Text.Pandoc.Lua.Module.Types as Types
2930import Text.Pandoc.Lua.Module.Utils as Utils
3031
3132-- | Parameters used to create lua packages/modules.
@@ -54,6 +55,7 @@ pandocPackageSearcher pkgParams pkgName =
5455 in pushWrappedHsFun (Pandoc.pushModule datadir)
5556 "pandoc.mediabag" -> pushWrappedHsFun MediaBag.pushModule
5657 "pandoc.system" -> pushWrappedHsFun System.pushModule
58+ "pandoc.types" -> pushWrappedHsFun Types.pushModule
5759 "pandoc.utils" -> let datadir = luaPkgDataDir pkgParams
5860 in pushWrappedHsFun (Utils.pushModule datadir)
5961 _ -> searchPureLuaLoader
test/Tests/Lua.hs+5−8
@@ -15,7 +15,6 @@ module Tests.Lua ( runLuaTest, tests ) where
1515
1616import Prelude
1717import Control.Monad (when)
18−import Data.Version (Version (versionBranch))
1918import System.FilePath ((</>))
2019import Test.Tasty (TestTree, localOption)
2120import Test.Tasty.HUnit (Assertion, assertEqual, testCase)
@@ -34,6 +33,7 @@ import Text.Pandoc.Options (def)
3433import Text.Pandoc.Shared (pandocVersion)
3534
3635import qualified Foreign.Lua as Lua
36+import qualified Data.ByteString.Char8 as BS
3737
3838tests :: [TestTree]
3939tests = map (localOption (QuickCheckTests 20))
@@ -135,17 +135,14 @@ tests = map (localOption (QuickCheckTests 20))
135135 (doc $ para (str $ "lua" </> "script-name.lua"))
136136
137137 , testCase "Pandoc version is set" . runLuaTest $ do
138− Lua.getglobal' "table.concat"
139138 Lua.getglobal "PANDOC_VERSION"
140− Lua.push ("." :: String) -- separator
141− Lua.call 2 1
142− Lua.liftIO . assertEqual "pandoc version is wrong" pandocVersion
143− =<< Lua.peek Lua.stackTop
139+ Lua.liftIO .
140+ assertEqual "pandoc version is wrong" (BS.pack pandocVersion)
141+ =<< Lua.tostring' Lua.stackTop
144142
145143 , testCase "Pandoc types version is set" . runLuaTest $ do
146− let versionNums = versionBranch pandocTypesVersion
147144 Lua.getglobal "PANDOC_API_VERSION"
148− Lua.liftIO . assertEqual "pandoc-types version is wrong" versionNums
145+ Lua.liftIO . assertEqual "pandoc-types version is wrong" pandocTypesVersion
149146 =<< Lua.peek Lua.stackTop
150147
151148 , testCase "Allow singleton inline in constructors" . runLuaTest $ do
test/Tests/Lua/Module.hs+2−1
@@ -20,7 +20,8 @@ import Tests.Lua (runLuaTest)
2020tests :: [TestTree]
2121tests =
2222 [ testPandocLua "pandoc" ("lua" </> "module" </> "pandoc.lua")
23− , testPandocLua "pandoc.util" ("lua" </> "module" </> "pandoc.utils.lua")
23+ , testPandocLua "pandoc.types" ("lua" </> "module" </> "pandoc-types.lua")
24+ , testPandocLua "pandoc.util" ("lua" </> "module" </> "pandoc-utils.lua")
2425 ]
2526
2627testPandocLua :: TestName -> FilePath -> TestTree
test/lua/module/pandoc-types.luanew file+112
@@ -0,0 +1,112 @@
1+local tasty = require 'tasty'
2+local types = require 'pandoc.types'
3+local Version = types.Version
4+
5+local assert = tasty.assert
6+local test = tasty.test_case
7+local group = tasty.test_group
8+
9+return {
10+ group 'Version' {
11+
12+ group 'constructor' {
13+ test('has type `userdata`', function ()
14+ assert.are_same(type(Version {2}), 'userdata')
15+ end),
16+ test('accepts list of integers', function ()
17+ assert.are_same(type(Version {2, 7, 3}), 'userdata')
18+ end),
19+ test('accepts a single integer', function ()
20+ assert.are_same(Version(5), Version {5})
21+ end),
22+ test('accepts version as string', function ()
23+ assert.are_same(
24+ Version '4.45.1',
25+ Version {4, 45, 1}
26+ )
27+ end),
28+ test('non-version string is rejected', function ()
29+ assert.error_matches(
30+ function () Version '11friends' end,
31+ '11friends'
32+ )
33+ end)
34+ },
35+
36+ group 'comparison' {
37+ test('smaller (equal) than', function ()
38+ assert.is_truthy(Version {2, 58, 3} < Version {2, 58, 4})
39+ assert.is_falsy(Version {2, 60, 1} < Version {2, 59, 2})
40+ assert.is_truthy(Version {0, 14, 3} < Version {0, 14, 3, 1})
41+ assert.is_truthy(Version {3, 58, 3} <= Version {4})
42+ assert.is_truthy(Version {0, 14, 3} <= Version {0, 14, 3, 1})
43+ end),
44+ test('larger (equal) than', function ()
45+ assert.is_truthy(Version{2,58,3} > Version {2, 57, 4})
46+ assert.is_truthy(Version{2,58,3} > Version {2, 58, 2})
47+ assert.is_truthy(Version {0, 8} >= Version {0, 8})
48+ assert.is_falsy(Version {0, 8} >= Version {0, 8, 2})
49+ end),
50+ test('equality', function ()
51+ assert.is_truthy(Version '8.8', Version {8, 8})
52+ end),
53+ test('second argument can be a version string', function ()
54+ assert.is_truthy(Version '8' < '9.1')
55+ assert.is_falsy(Version '8.8' < '8.7')
56+ end),
57+ },
58+
59+ group 'list-like behavior' {
60+ test('can access version component numbers', function ()
61+ local version = Version '2.7.3'
62+ assert.is_nil(version[0])
63+ assert.are_equal(version[1], 2)
64+ assert.are_equal(version[2], 7)
65+ assert.are_equal(version[3], 3)
66+ end),
67+ test('can be iterated over', function ()
68+ local version_list = {2, 7, 3}
69+ local final_index = 0
70+ for i, v in pairs(Version(version_list)) do
71+ assert.are_equal(v, version_list[i])
72+ final_index = i
73+ end
74+ assert.are_equal(final_index, 3)
75+ end),
76+ test('length is the number of components', function ()
77+ assert.are_equal(#(Version '0'), 1)
78+ assert.are_equal(#(Version '1.6'), 2)
79+ assert.are_equal(#(Version '8.7.5'), 3)
80+ assert.are_equal(#(Version '2.9.1.5'), 4)
81+ end)
82+ },
83+
84+ group 'conversion to string' {
85+ test('converting from and to string is a noop', function ()
86+ local version_string = '1.19.4'
87+ assert.are_equal(tostring(Version(version_string)), version_string)
88+ end)
89+ },
90+
91+ group 'convenience functions' {
92+ test('throws error if version is too old', function ()
93+ local actual = Version {2, 8}
94+ local expected = Version {2, 9}
95+ assert.error_matches(
96+ function () actual:must_be_at_least(expected) end,
97+ 'version too old: expected version 2.9 or newer, got 2.8'
98+ )
99+ end),
100+ test('does nothing if expected version is older than actual', function ()
101+ local actual = Version '2.9'
102+ local expected = Version '2.8'
103+ actual:must_be_at_least(expected)
104+ end),
105+ test('does nothing if expected version equals to actual', function ()
106+ local actual = Version '2.8'
107+ local expected = Version '2.8'
108+ actual:must_be_at_least(expected)
109+ end)
110+ }
111+ }
112+}
test/lua/module/pandoc.utils.lua → test/lua/module/pandoc-utils.luarenamed

The content is the same.