diff --git a/changes.md b/changes.md new file mode 100644 index 00000000..b6e06f87 --- /dev/null +++ b/changes.md @@ -0,0 +1,56 @@ +## Version 1.4.2 + +### Features + + * Can define fields/properties of objects; `readonly` modifier supported (#93) + * Can switch off auto-linking to Lua manual with `no_lua_ref` + * Module sorting is off by default, use `sort_modules=true` + * References to 'classes' now work properly + * Option to use first Markdown title instead of file names with `use_markdown_titles` + * Automatic `Metamethods` and `Methods` sections generated for `classmod` classes + * `unqualified=true` to strip package names on sidebar (#110) + * Custom tags (which may be hidden) + * Custom Display Name handlers + +### Fixes + + * stricter about doc comments, now excludes common '----- XXXXX ----' pattern + * no longer expects space after `##` in Markdown (#96) + * Section lookup was broken + * With `export` tag, decide whether method is static or not + * `classmod` classes now respect custom sections (#113) + * Minor issues with prettification + * Command-line flags set explicitly take precendence over configuration file values. + * Boilerplate Lua block comment ignored properly (#137) + * Inline links with underscores sorted (#22) + * Info section ordering is now consistent (#150) + +## Version 1.4.0 + +### Features + + * `sort=true` to sort items within sections alphabetically + * `@set` tag in module comments; e.g, can say `@set sort=true` + * `@classmod` tag for defining modules that export one class + * can generate Markdown output + * Can prettify C as well as Lua code with built-in prettifier + * lfs and lpeg references understood + * 'pale' template available + * multiple return groups + * experimental `@error` tag + * Moonscript and plain C support + + +### Fixes + + * works with non-compatibily Lua 5.2, including `markdown.lua` + * module names can not be types + * all `builtin` Lua files are requirable without `module` + * backticks expand in copyright and other 'info' tabs + * `-m` tries harder to resolve methods + * auto-scroll in navigation area to avoid breaking identifiers + * better error message for non-luadoc-compatible behaviour + * custom see references fixed + + + diff --git a/config.ld b/config.ld deleted file mode 100644 index 4fb78c9e..00000000 --- a/config.ld +++ /dev/null @@ -1,7 +0,0 @@ -project='LDoc' -title='LDoc documentation' -description='A Lua documentation tool' -format='discount' -file='ldoc.lua' -dir='out' -readme='docs/doc.md' diff --git a/doc/config.ld b/doc/config.ld new file mode 100644 index 00000000..f45b2935 --- /dev/null +++ b/doc/config.ld @@ -0,0 +1,19 @@ +project='LDoc' +title='LDoc documentation' +description='A Lua documentation tool' +format='discount' +backtick_references=false +file='../ldoc.lua' +dir='../out' +readme='doc.md' +style='!pale' +kind_names={topic='Manual',script='Programs'} +examples = { + '../tests/styles/colon.lua', + '../tests/styles/four.lua', + '../tests/styles/three.lua', + '../tests/styles/multiple.lua', + '../tests/example/mylib.c', + '../tests/moonscript/List.moon', +} + diff --git a/docs/doc.md b/doc/doc.md similarity index 62% rename from docs/doc.md rename to doc/doc.md index 49edb4d6..9e3c15dd 100644 --- a/docs/doc.md +++ b/doc/doc.md @@ -4,12 +4,13 @@ ## Introduction -LDoc is a second-generation documentation tool that can be used as a replacement for -[LuaDoc](http://keplerproject.github.com/luadoc/). It arose out of my need to document my -own projects and only depends on the [Penlight](https://github.com/stevedonovan/Penlight) -libraries. +LDoc is a software documentation tool which automatically generates API documentation +out of source code comments (doc comments). It is mainly targeted at Lua and documenting +Lua APIs, but it can also parse C with according doc comments for documenting Lua modules +implemented in C. -It is mostly compatible with LuaDoc, except that certain workarounds are no longer needed. +It is mostly compatible with [LuaDoc](http://keplerproject.github.com/luadoc/), +except that certain workarounds are no longer needed. For instance, it is not so married to the idea that Lua modules should be defined using the `module` function; this is not only a matter of taste since this has been deprecated in Lua 5.2. @@ -32,7 +33,10 @@ end-users to build your documentation using this simple command. ## Commenting Conventions -LDoc follows the conventions established by Javadoc and later by LuaDoc. +LDoc follows the conventions established by Javadoc and later by LuaDoc to document the +modules, functions, tables and types ("classes") of your API. + +### Doc comments Only 'doc comments' are parsed; these can be started with at least 3 hyphens, or by a empty comment line with at least 3 hypens: @@ -55,12 +59,78 @@ Any module or script must start with a doc comment; any other files are ignored warning issued. The only exception is if the module starts with an explicit `module` statement. +If your coding standards require a boilerplate copyright notice, then the `-B` flag or +`boilerplate=true` will make LDoc ignore the first comment of each module. + +Common commenting patterns like '---- (text) -----' are exempted, since they are often used +for programmer-facing documentation. + + +### Tags + All doc comments start with a summary sentence, that ends with a period or a question mark. An optional description may follow. Normally the summary sentence will appear in the module contents. -After this descriptive text, there will typically be _tags_. These follow the convention -established by Javadoc and widely used in tools for other languages. +After this descriptive text, there will typically be _tags_ which are introduced with an @. +These follow the convention established by Javadoc and widely used in tools for other languages. + + --- Some doc comment + -- @tag1 parameters for first tag + -- @tag2 parameters for the second tag + +The order of tags is not important, but as always, consistency is useful. + +Here are all the tags known to LDoc: + + * **@module** A Lua module containing functions and tables, which may be inside sections + * **@classmod** Like **@module** but describing a class + * **@submodule** A file containing definitions that you wish to put into the named _master_ module + * **@script** A Lua program + * **@author** (multiple), **copyright**, **@license**, **@release** only used for _project-level_ tags like **@module** + * **@function**, **@lfunction**. Functions inside a module + * **@param** formal arguments of a function (multiple) + * **@return** returned values of a function (multiple) + * **@raise** unhandled error thrown by this function + * **@local** explicitly marks a function as not being exported (unless `--all`) + * **@see** reference other documented items + * **@usage** give an example of a function's use. (Has a somewhat different meaning when used + with **@module**) + * **@table** a Lua table + * **@field** a named member of a table + * **@section** starting a named section for grouping functions or tables together + * **@type** a section which describes a class + * **@within** puts the function or table into an implicit section + * **@fixme**, **@todo** and **@warning** are _annotations_, which are doc comments that + occur inside a function body. + +The first important tag to know is the module tag: + +#### Modules: naming and describing your API module + +The first thing in your API module should be a name and a description. +This is how a module is commonly done in Lua 5.2 with a **@module** tag at the top +which introduces the name: + + --- a test module + -- @module test + + local test = {} + + function test.my_module_function_1() + ... + end + + ... + + return test + +This sets up a module named 'test' with the description 'a test module'. + +#### Functions + +The next thing to describe are the functions your module has. +This is a simple example of a documented function: --- foo explodes text. -- It is a specialized splitting operation on a string. @@ -70,17 +140,37 @@ established by Javadoc and widely used in tools for other languages. .... end -There are also 'tparam' and 'treturn' which let you [specify a type](#Tag_Modifiers): +You can also give the function name itself as an explicit tag, +which is especially useful when documenting a Lua api exported by C code: + + /// A C function which is exported to Lua with another name, + // because the ways of C can be mysterious! + // @function our_nice_function + int _some_function_for_lua(lua_State* l) { + .... + } + +The tags basically add all the detail that cannot be derived from the source code +automatically. + +#### Function parameters and return values + +Common tags are the 'param' tag which takes a parameter name followed by a parameter +description separated by a space, and the 'return' tag which is simply followed by +a description for a return value: + + -- @param name_of_parameter the description of this parameter as verbose text + -- @return the description of the return value + +If you want to [specify a type](#Tag_Modifiers) for a parameter or a return value, +there are also 'tparam' and 'treturn': - -- @tparam string text the string + -- @tparam string text this parameter is named 'text' and has the fixed type 'string' -- @treturn {string,...} a table of substrings There may be multiple 'param' tags, which should document each formal parameter of the function. For Lua, there can also be multiple 'return' tags - --- solvers for common equations. - module("solvers", package.seeall) - --- solve a quadratic equation. -- @param a first coeff -- @param b second coeff @@ -99,29 +189,80 @@ function. For Lua, there can also be multiple 'return' tags end ... +Of course there is also the 'module' tag which you have already seen. -This is the common module style used in Lua 5.1, but it's increasingly common to see less -'magic' ways of creating modules in Lua. Since `module` is deprecated in Lua 5.2, any -future-proof documentation tool needs to handle these styles gracefully: +#### Tables and constant values (fields) - --- a test module - -- @module test +Modules can of course export tables and other values. The classic way to document a table +looks like this: - local test = {} + --- a useful table of constants + -- @field alpha first correction + -- @field beta second correction + -- @field gamma fudge factor + -- @table constants - --- first test. - function test.one() - ... +Here the kind of item is made explicit by the 'table' tag; tables have 'fields' in the same +way as functions have parameters. + +This can get tedious, so LDoc will attempt to extract table documentation from code: + + --- a useful table of constants + M.constants = { + alpha = 0.23, -- first correction + beta = 0.443, -- second correction + gamma = 0.01 -- fudge factor + } + +The rule followed here is `NAME = `. If LDoc can't work out the name and +type from the following code, then a warning will be issued, pointing to the file and +location. + +Another kind of module-level type is 'field', such as follows: + + --- module version. + M._VERSION = '0.5' + +That is, a module may contain exported functions, local functions, tables and fields. + +#### Explicitly specifying a function or fields + +When the code analysis would lead to the wrong type, you can always be explicit. + + --- module contents with explicitly documented field _CONTENTS. + -- @field _CONTENTS + M._CONTENTS = {constants=true,one=true,...} + + --- an explicitly named function. + -- @function my_function + function my_function() + ... end - ... +As mentioned before, this is often especially useful in C where things +may look different in the C code than they will in the final Lua api which +you want to document. - return test +### Doing modules the Lua 5.1 way + +As an alternative to using the 'module' tag as described before, you +can still start your modules the Lua 5.1 way: + + --- solvers for common equations. + module("solvers", package.seeall) + +However, the 'module' function is deprecated in Lua 5.2 and it is increasingly +common to see less 'magic' ways of creating modules, as seen in the description +of the 'module' tag previously with the explicitely returned module table. + +#### Repeating tags + +Tags like 'param' and 'return' can be specified multiple times, whereas a type +tag like 'function' can only occur once in a comment. -Here the name of the module is explicitly given using the 'module' tag. If you leave this -out, then LDoc will infer the name of the module from the name of the file and its relative -location in the filesystem; this logic is also used for the `module(...)` idiom. (How this -works and when you need to provide extra information is discussed later.) +The basic rule is that a single doc comment can only document one entity. + +### Local module name It is common to use a local name for a module when declaring its contents. In this case the 'alias' tag can tell LDoc that these functions do belong to the module: @@ -149,6 +290,8 @@ explicit assignment to a variable: --- second test. M.two = function(...) ... end +### Local functions + Apart from exported functions, a module usually contains local functions. By default, LDoc does not include these in the documentation, but they can be enabled using the `--all` flag. They can be documented just like 'public' functions: @@ -162,48 +305,24 @@ They can be documented just like 'public' functions: -- @local here function foo(...) .. end -Modules can of course export tables and other values. The classic way to document a table -looks like this: - - --- a useful table of constants - -- @field alpha first correction - -- @field beta second correction - -- @field gamma fudge factor - -- @table constants - -Here the kind of item is made explicit by the 'table' tag; tables have 'fields' in the same -way as functions have parameters. - -This can get tedious, so LDoc will attempt to extract table documentation from code: - - --- a useful table of constants - M.constants = { - alpha = 0.23, -- first correction - beta = 0.443, -- second correction - gamma = 0.01 -- fudge factor - } - -The rule followed here is `NAME = `. If LDoc can't work out the name and -type from the following code, then a warning will be issued, pointing to the file and -location. +### Alternative way of specifying tags -Another kind of module-level type is 'field', such as follows: +Since 1.3, LDoc allows the use of _colons_ instead of @. - --- module version. - M._VERSION = '0.5' + --- a simple function. + -- string name person's name + -- int: age age of person + -- !person: person object + -- treturn: ?string + -- function check(name,age) -That is, a module may contain exported functions, local functions, tables and fields. +However, you must either use the `--colon` flag or set `colon=true` in your `config.ld`. -When the code analysis would lead to the wrong type, you can always be explicit. +In this style, types may be used directly if prefixed with '!' or '?' (for type-or-nil) - --- module contents. - -- @field _CONTENTS - M._CONTENTS = {constants=true,one=true,...} +(see @{colon.lua}, rendered [here](http://stevedonovan.github.io/ldoc/examples/colon)) -The order of tags is not important, but as always, consistency is useful. Tags like 'param' -and 'return' can be specified multiple times, whereas a type tag like 'function' can only -occur once in a comment. The basic rule is that a single doc comment can only document one -entity. +### Which files are processed By default, LDoc will process any file ending in '.lua' or '.luadoc' in a specified directory; you may point it to a single file as well. A 'project' usually consists of many @@ -214,12 +333,18 @@ If only one module or script is documented for a project, then the `index.html` contains the documentation for that module, since an index pointing to one module would be redundant. -(If you want to document a script, there is a project-level type 'script' for that.) +LDoc has a two-layer hierarchy; underneath the project, there are modules, scripts, classes +(containing code) and examples and 'topics' (containing documentation). These then contain +items like functions, tables, sections, and so forth. + +If you want to document scripts, then use **@script** instead of **@module**. New with 1.4 is +**@classmod** which is a module which exports a single class. + ## See References -The tag 'see' is used to reference other parts of the documentation, and 'usage' can provide -examples of use: +**@see** is used to reference other parts of the documentation, and **@usage** can provide +examples of use; there can be multiple such tags: --------- -- split a string in two. @@ -235,9 +360,9 @@ Here it's assumed that 'split' is a function defined in the same module. If you to a function in another module, then the reference has to be qualified. References to methods use a colon: `myclass:method`; this is for instance how you would -refer to members of a `@type` section. +refer to members of a **@type** section. -The example at `tests/complex` shows how @see references are interpreted: +The example at `tests/complex` shows how **@see** references are interpreted: complex.util.parse complex.convert.basic @@ -257,36 +382,38 @@ If a reference is not found within the project, LDoc checks to see if it is a re Lua standard function or table, and links to the online Lua manual. So references like 'table.concat' are handled sensibly. -References may be made inline using the @\{ref} syntax. This may appear anywhere in the -text, and is more flexible than @see. In particular, it provides one way to document the +References may be made inline using the `@{\ref}` syntax. This may appear anywhere in the +text, and is more flexible than **@see**. In particular, it provides one way to document the type of a parameter or return value when that type has a particular structure: ------ -- extract standard variables. -- @param s the string - -- @return @\{stdvars} + -- @return @{\stdvars} function extract_std(s) ... end ------ -- standard variables. - -- Use @\{extract_std} to parse a string containing variables, - -- and @\{pack_std} to make such a string. + -- Use @{\extract_std} to parse a string containing variables, + -- and @{\pack_std} to make such a string. -- @field length -- @field duration -- @field viscosity -- @table stdvars -@\{ref} is very useful for referencing your API from code samples and readme text. (I've had -to throw in a spurious backspace to stop expansion in this example.) +`@{\ref}` is very useful for referencing your API from code samples and readme text. -The link text can be changed from the default by the extended syntax @\{ref|text}. +The link text can be changed from the default by the extended syntax `@{\ref|text}. You can also put references in backticks, like `\`stdvars\``. This is commonly used in Markdown to indicate code, so it comes naturally when writing documents. It is controlled by -the configuration variable `backtick_references`; the default is `true` if you use Markdown -in your project, but can be specified explicitly in your `config.ld`. +the configuration variable `backtick_references` or the `backtick` format; +the default is `true` if you use Markdown in your project, but can be specified explicitly +in `config.ld`. -### Custom @see References +To quote such references so they won't be expanded, say @{\\ref}. + +#### Custom @see References It's useful to define how to handle references external to a project. For instance, in the [luaposix](https://github.com/luaposix/luaposix) project we wanted to have `man` references @@ -305,26 +432,65 @@ online references to the Linux manpages. So in `config.ld` we have: local upat = "http://www.kernel.org/doc/man-pages/online/pages/man%s/%s.%s.html" - custom_see_handler('^(%a+)%((%d)%)$',function(name,section) + custom_see_handler('^([%w_]+)%((%d)%)$',function(name,section) local url = upat:format(section,name,section) local name = name .. '(' ..section..')' return name, url end) -'^(%a+)%((%d)%)$' both matches the pattern and extracts the name and its section. THen it's +`^([%w_]+)%((%d)%)$` both matches the pattern and extracts the name and its section. Then it's a simple matter of building up the appropriate URL. The function is expected to return _link text_ and _link source_ and the patterns are checked before LDoc tries to resolve project references. So it is best to make them match as exactly as possible. +## Module Tags + +LDoc requires you to have a module doc comment. If your code style requires +license blocks that might look like doc comments, then set `boilerplate=true` in your +configuration and they will be skipped. + +This comment does not have to have an explicit **@module** tag and LDoc continues to +respect the use of `module()`. + +There are three types of 'modules' (i.e. 'project-level'); `module`, a library +loadable with `require()`, `script`, a program, and `classmod` which is a class +implemented in a single module. + +There are some tags which are only useful in module comments: `author`,`copyright`, +`license` and `release`. These are presented in a special **Info** section in the +default HTML output. + +The **@usage** tag has a somewhat different presentation when used in modules; the text +is presented formatted as-is in a code font. If you look at the script `ldoc` in +this documentation, you can see how the command-line usage is shown. Since coding +is all about avoiding repetition and the out-of-sync issues that arise, +the **@usage** tag can appear later in the module, before a long string. For instance, +the main script of LDoc is [ldoc.lua](https://github.com/stevedonovan/LDoc/blob/master/ldoc.lua) +and you will see that the usage tag appears on line 36 before the usage string +presented as help. + +**@export** is another module tag that is usually 'detached'. It is for supporting +modules that wish to explicitly export their functions @{three.lua|at the end}. +In that example, both `question` and `answer` are local and therefore private to +the module, but `answer` has been explicitly exported. (If you invoke LDoc with +the `-a` flag on this file, you will see the documentation for the unexported +function as well.) + +**@set** is a powerful tag which assigns a configuration variable to a value _just for this module_. +Saying `@set no_summary=true` in a module comment will temporarily disable summary generation when +the template is expanded. Generally configuration variables that effect template expansion +are modifiable in this way. For instance, if you wish that the contents of a particular module +be sorted, then `@set sort=true` will do it _just_ for that module. + ## Sections -LDoc supports _explicit_ sections. By default, the sections correspond to the pre-existing +LDoc supports _explicit_ sections. By default, the implicit sections correspond to the pre-existing types in a module: 'Functions', 'Tables' and 'Fields' (There is another default section 'Local Functions' which only appears if LDoc is invoked with the `--all` flag.) But new -sections can be added; the first mechanism is when you define a new type (say 'macro') a new -section ('Macros') is created to contain these types. There is also a way to declare ad-hoc -sections using the `@section` tag. +sections can be added; the first mechanism is when you @{Adding_new_Tags|define a new type} +(say 'macro'). Then a new section ('Macros') is created to contain these types. +There is also a way to declare ad-hoc sections using the **@section** tag. The need occurs when a module has a lot of functions that need to be put into logical sections. @@ -346,9 +512,9 @@ sections. A section doc-comment has the same structure as a normal doc-comment; the summary is used as the new section title, and the description will be output at the start of the function -details for that section. +details for that section; the name is not used, but must be unique. -In any case, sections appear under 'Contents' on the left-hand side. See the +Sections appear under 'Contents' on the left-hand side. See the [winapi](http://stevedonovan.github.com/winapi/api.html) documentation for an example of how this looks. @@ -369,17 +535,22 @@ A specialized kind of section is `type`: it is used for documenting classes. The end (In an ideal world, we would use the word 'class' instead of 'type', but this would conflict -with the LuaDoc usage.) +with the LuaDoc `class` tag.) A section continues until the next section is found, `@section end`, or end of file. -You can put items into an implicit section using the @within tag. This allows you to put +You can put items into an implicit section using **@within**. This allows you to put adjacent functions in different sections, so that you are not forced to order your code in a particular way. -Sometimes a module may logically span several files. There will be a master module with name +With 1.4, there is another option for documenting classes, which is the top-level type +`classmod`. It is intended for larger classes which are implemented within one module, +and the advantage that methods can be put into sections. + +Sometimes a module may logically span several files, which can easily happen with large +There will be a master module with name 'foo' and other files which when required add functions to that module. If these files have -a @submodule tag, their contents will be placed in the master module documentation. However, +a **@submodule** tag, their contents will be placed in the master module documentation. However, a current limitation is that the master module must be processed before the submodules. See the `tests/submodule` example for how this works in practice. @@ -396,6 +567,8 @@ One added convenience is that it is easier to name entities: -- @class module -- @name simple +becomes: + ------------ -- a simple module. -- (LDoc) @@ -409,38 +582,10 @@ can define an alias for it, such as 'p'. This can also be specified in the confi LDoc will also work with C/C++ files, since extension writers clearly have the same documentation needs as Lua module writers. -LDoc allows you to attach a _type_ to a parameter or return value - - --- better mangler. - -- @tparam string name - -- @int max length - -- @treturn string mangled name - function strmangler(name,max) - ... - end - -`int` here is short for `tparam int` (see @{Tag_Modifiers}) - -It's common for types to be optional, or have different types, so the type can be like -'?int|string' which renders as '(int or string)', or '?int', which renders as -'(optional int)'. - -LDoc gives the documenter the option to use Markdown to parse the contents of comments. - -Since 1.3, LDoc allows the use of _colons_ instead of @. - - --- a simple function. - -- string name person's name - -- int: age age of person - -- !person: person object - -- treturn: ?string - -- function check(name,age) - -However, you must either use the `--colon` flag or set `colon=true` in your `config.ld`. - -In this style, types may be used directly if prefixed with '!' or '?' (for type-or-nil) - -(see `tests/styles/colon.lua`) +LDoc allows you to attach a _type_ to a parameter or return value with `tparam` or `treturn`, +and gives the documenter the option to use Markdown to parse the contents of comments. +You may also include code examples which will be prettified, and readme files which will be +rendered with Markdown and contain prettified code blocks. ## Adding new Tags @@ -535,16 +680,17 @@ As always, explicit tags can override this behaviour if it is inappropriate. LDoc can process C/C++ files: - @plain - /*** - Create a table with given array and hash slots. - @function createtable - @param narr initial array slots, default 0 - @param nrec initial hash slots, default 0 - @return the new table - */ - static int l_createtable (lua_State *L) { - .... +```c +/*** +Create a table with given array and hash slots. +@function createtable +@param narr initial array slots, default 0 +@param nrec initial hash slots, default 0 +@return the new table +*/ +static int l_createtable (lua_State *L) { +.... +``` Both `/**` and `///` are recognized as starting a comment block. Otherwise, the tags are processed in exactly the same way. It is necessary to specify that this is a function with a @@ -557,18 +703,25 @@ or 'lua'.) An LDoc feature which is particularly useful for C extensions is _module merging_. If several files are all marked as `@module lib` then a single module `lib` is generated, containing all -the docs from the separate files. +the docs from the separate files. For this, use `merge=true`. + +See @{mylib.c} for the full example. + +## Moonscript Support -See 'tests/examples/mylib.c' for the full example. +1.4 introduces basic support for [Moonscript](http://moonscript.org). Moonscript module +conventions are just the same as Lua, except for an explicit class construct. +@{list.moon} shows how **@classmod** can declare modules that export one class, with metamethods +and methods put implicitly into a separate section. ## Basic Usage For example, to process all files in the 'lua' directory: $ ldoc lua - output written to docs/ + output written to doc/ -Thereafter the `docs` directory will contain `index.html` which points to individual modules +Thereafter the `doc` directory will contain `index.html` which points to individual modules in the `modules` subdirectory. The `--dir` flag can specify where the output is generated, and will ensure that the directory exists. The output structure is like LuaDoc: there is an `index.html` and the individual modules are in the `modules` subdirectory. This applies to @@ -590,35 +743,6 @@ For new-style modules, that don't use `module()`, it is recommended that the mod has an explicit `@module PACKAGE.NAME`. If it does not, then `ldoc` will still attempt to deduce the module name, but may need help with `--package/-b` as above. -`format = 'markdown'` can be used in your `config.ld` and will be used to process summaries -and descriptions. This requires [markdown.lua](http://www.frykholm.se/files/markdown.lua) by -Niklas Frykholm to be installed (this can be most easily done with `luarocks install -markdown`.) A much faster alternative is -[lua-discount](http://asbradbury.org/projects/lua-discount/) which you can use by setting -`format` to 'discount' after installing using `luarocks install lua-discount`) The -[discount](http://www.pell.portland.or.us/~orc/Code/discount/) Markdown processor -additionally has more features than the pure Lua version, such as PHP-Extra style tables. -As a special case, LDoc will fall back to using `markdown.lua` if it cannot find `discount`. - -`format = 'markdown'` can be used in your `config.ld` and will be used to process summaries -and descriptions. This requires a markdown processor. -LDoc knows how to use: - - - [markdown.lua](http://www.frykholm.se/files/markdown.lua) a pure Lua processor by -Niklas Frykholm (this can be installed easily with `luarocks install markdown`.) - - [lua-discount](http://asbradbury.org/projects/lua-discount/), a faster alternative -(installed with `luarocks install lua-discount`). lua-discount uses the C -[discount](http://www.pell.portland.or.us/~orc/Code/discount/) Markdown processor which has -more features than the pure Lua version, such as PHP-Extra style tables. - - [lunamark](http://jgm.github.com/lunamark/), another pure Lua processor, faster than -markdown, and with extra features (`luarocks install lunamark`). - -You can request the processor you like with `format = 'markdown|discount|lunamark'`, and -LDoc will attempt to use it. If it can't find it, it will look for one of the other -markdown processors. If it can't find any markdown processer, it will fall back to text -processing. - - A special case is if you simply say 'ldoc .'. Then there _must_ be a `config.ld` file available in the directory, and it can specify the file: @@ -634,6 +758,39 @@ In `config.ld`, `file` may be a Lua table, containing file names or directories; an `exclude` field then that will be used to exclude files from the list, for example `{'examples', exclude = {'examples/slow.lua'}}`. +A particular configuration file can be specified with the `-c` flag. Configuration files don't +_have_ to contain a `file` field, but in that case LDoc does need an explicit file on the command +line. This is useful if you have some defaults you wish to apply to all of your docs. + +## Markdown Support + +`format = 'markdown'` can be used in your `config.ld` and will be used to process summaries +and descriptions; you can also use the `-f` flag. This requires a markdown processor. +LDoc knows how to use: + + - [markdown.lua](http://www.frykholm.se/files/markdown.lua) a pure Lua processor by +Niklas Frykholm. For convenience, LDoc comes with a copy of markdown.lua. + - [lua-discount](http://asbradbury.org/projects/lua-discount/), a faster alternative +(installed with `luarocks install lua-discount`). lua-discount uses the C +[discount](http://www.pell.portland.or.us/~orc/Code/discount/) Markdown processor which has +more features than the pure Lua version, such as PHP-Extra style tables. + - [lunamark](http://jgm.github.com/lunamark/), another pure Lua processor, faster than +markdown, and with extra features (`luarocks install lunamark`). + +You can request the processor you like with `format = 'markdown|discount|lunamark|plain|backticks'`, and +LDoc will attempt to use it. If it can't find it, it will look for one of the other +markdown processors; the original `markdown.lua` ships with LDoc, although it's slow +for larger documents. + +Even with the default of 'plain' some minimal processing takes place, in particular empty lines +are treated as line breaks. If the 'backticks' formatter is used, then it's equivalent to +using 'process_backticks=true` in `config.ld` and backticks will be +expanded into documentation links like `@{\ref}` and converted into `ref` +otherwise. + +This formatting applies to all of a project, including any readmes and so forth. You may want +Markdown for this 'narrative' documentation, but not for your code comments. `plain=true` will +switch off formatting for code. ## Processing Single Modules @@ -641,8 +798,8 @@ an `exclude` field then that will be used to exclude files from the list, for ex special case when a single module file is specified. Here an index would be redundant, so the single HTML file generated contains the module documentation. - $ ldoc mylib.lua --> results in docs/index.html - $ ldoc --output mylib mylib.lua --> results in docs/mylib.html + $ ldoc mylib.lua --> results in doc/index.html + $ ldoc --output mylib mylib.lua --> results in doc/mylib.html $ ldoc --output mylib --dir html mylib.lua --> results in html/mylib.html The default sections used by LDoc are 'Functions', 'Tables' and 'Fields', corresponding to @@ -745,24 +902,25 @@ module that uses extended LDoc features. The _navigation section_ down the left has several parts: - - The project name ('project' in the config) - - A project description ('description') - - ''Contents'' of the current page - - ''Modules'' listing all the modules in this project + - The project name (`project` in the config) + - A project description (`description`) + - **Contents** of the current page + - **Modules** listing all the modules in this project Note that `description` will be passed through Markdown, if it has been specified for the project. This gives you an opportunity to make lists of links, etc; any '##' headers will be formatted like the other top-level items on the navigation bar. -'Contents' is automatically generated. It will contain any explicit sections, if they have -been used. Otherwise you will get the usual categories: 'Functions', 'Tables' and 'Fields'. +**Contents** is automatically generated. It will contain any explicit sections +as well as the usual categories: 'Functions', 'Tables' and 'Fields'. For a documentation page, +the subtitles become the sections shown here. -'Modules' will appear for any project providing Lua libraries; there may also be a 'Scripts' +**Modules** will appear for any project providing Lua libraries; there may also be a 'Scripts' section if the project contains Lua scripts. For example, [LuaMacro](http://stevedonovan.github.com/LuaMacro/docs/api.html) has a driver script `luam` in this section. The [builtin](http://stevedonovan.github.com/LuaMacro/docs/modules/macro.builtin.html) module -only defines macros, which are defined as a _custom tag type_. +only defines macros, which are defined as a _custom tag type[?]_. The _content section_ on the right shows: @@ -778,13 +936,20 @@ description. There are then sections for the following tags: 'param', 'usage', ' 'see' in that order. (For tables, 'Fields' is used instead of 'Parameters' but internally fields of a table are stored as the 'param' tag.) +By default, the items appear in the order of declaration within their section. If `sort=true` +then they will be sorted alphabetically. (This can be set per-module with @{Module_Tags|@set}.) + You can of course customize the default template, but there are some parameters that can -control what the template will generate. Setting `one` to `true` in your configuration file +control what the template will generate. Setting `one=true` in your configuration file will give a _one-column_ layout, which can be easier to use as a programming reference. You can suppress the contents summary with `no_summary`. ## Customizing the Page +A basic customization is to override the default UTF-8 encoding using `charset`. For instance, +Brazillian software would find it useful to put `charset='ISO-8859-1'` in `config.ld`, or use +the **@charset** tag for individual files. + Setting `no_return_or_parms` to `true` will suppress the display of 'param' and 'return' tags. This may appeal to programmers who dislike the traditional @tag soup xDoc style and prefer to comment functions just with a description. This is particularly useful when using @@ -811,7 +976,6 @@ to the original: no_return_or_parms = true format = 'discount' - Generally, using Markdown gives you the opportunity to structure your documentation in any way you want; particularly if using lua-discount and its [table syntax](http://michelf.com/projects/php-markdown/extra/#table); the desired result can often @@ -826,7 +990,7 @@ and writer-generated documentation using different tools, and having to match th LDoc allows for source examples to be included in the documentation. For example, see the online documentation for [winapi](http://stevedonovan.github.com/winapi/api.html). The -function `utf8_expand` has a `@see` reference to 'testu.lua' and following that link gives +function `utf8_expand` has a **@see** reference to 'testu.lua' and following that link gives you a pretty-printed version of the code. The line in the `config.ld` that enables this is: @@ -834,11 +998,19 @@ The line in the `config.ld` that enables this is: examples = {'examples', exclude = {'examples/slow.lua'}} That is, all files in the `examples` folder are to be pretty-printed, except for `slow.lua` -which is meant to be called from one of the examples. The see-reference to `testu.lua` -resolves to 'examples/testu.lua.html'. +which is meant to be called from one of the examples. +To link to an example, use a reference like `@{\testu.lua}` +which resolves to 'examples/testu.lua.html'. Examples may link back to the API documentation, for instance the example `input.lua` has a -@\{spawn_process} inline reference. +`@{\spawn_process}` inline reference. + +By default, LDoc uses a built-in Lua code 'prettifier'. Reference links are allowed in comments, +and also in code if they're enclosed in backticks. Lua and C are known languages. + +[lxsh](https://github.com/xolox/lua-lxsh) +can be used (available from LuaRocks) if you want something more powerful. `pretty='lxsh'` will +cause `lxsh` to be used, if available. ## Readme files @@ -849,16 +1021,16 @@ Like all good Github projects, Winapi has a `readme.md`: This goes under the 'Topics' global section; the 'Contents' of this document is generated from the second-level (##) headings of the readme. -Readme files are always processed with the current Markdown processor, but may also contain @\{} references back +Readme files are always processed with the current Markdown processor, but may also contain `@{\ref}` references back to the documentation and to example files. Any symbols within backticks will be expanded as references, if possible. As with doc comments, a link to a standard Lua function like -@\{os.execute} will work as well. Any code sections will be pretty-printed as Lua, unless +`@{\os.execute}` will work as well. Any code sections will be pretty-printed as Lua, unless the first indented line is '@plain'. (See the source for this readme to see how it's used.) Another name for `readme` is `topics`, which is more descriptive. From LDoc 1.2, `readme/topics` can be a list of documents. These act as a top-level table-of-contents for your documentation. Currently, if you want them in a particular order, then use names like -`01-introduction.md` etc which sort appropriately. +`01-introduction.md` etc, which sort appropriately. The first line of a document may be a Markdown `#` title. If so, then LDoc will regard the next level as the subheadings, normally second-level `##`. But if the title is already @@ -866,41 +1038,51 @@ second-level, then third-level headings will be used `###`, and so forth. The im that the first heading must be top-level relative to the headings that follow, and must start at the first line. -A reference like @\{string.upper} is unambiguous, and will refer to the online Lua manual. +A reference like `@{\string.upper}` is unambiguous, and will refer to the online Lua manual. In a project like Penlight, it can get tedious to have to write out fully qualified names -like @\{pl.utils.printf}. The first simplification is to use the `package` field to resolve +like `@{\pl.utils.printf}`. The first simplification is to use the `package` field to resolve unknown references, which in this case is 'pl'. (Previously we discussed how `package` is used to tell LDoc where the base package is in cases where the module author wishes to remain vague, but it does double-duty here.) A further level of simplification comes from -the @lookup directive in documents, which must start at the first column on its own line. -For instance, if I am talking about `pl.utils`, then I can say "@lookup utils" and -thereafter references like @\{printf} will resolve correctly. +the `@lookup` directive in documents, which must start at the first column on its own line. +For instance, if I am talking about `pl.utils`, then I can say `@lookup utils` and +thereafter references like `@{\printf}` will resolve correctly. If you look at the source for this document, you will see a `@lookup doc.md` which allows -direct references to sections like @{Readme_files|this}. +direct references to sections like @{Readme_files|this} with `@{\Readme_files|this}`. Remember that the default is for references in backticks to be resolved; unlike @ references, it is not an error if the reference cannot be found. The _sections_ of a document (the second-level headings) are also references. This -particular section can be refered to as @\{doc.md.Resolving_References_in_Documents} - the +particular section you are reading can be refered to as `@{\doc.md.Readme_files}` - the rule is that any non-alphabetic character is replaced by an underscore. +Any indented blocks are assumed to be Lua, unless their first line is `@plain`. New +with 1.4 is github-markdown-style fenced code blocks, which start with three backticks +optionally followed by a language. The code continues until another three backticks +is found: the language can be `c`, `cpp` or `cxx` for C/C++, anything else is Lua. ## Tag Modifiers Ay tag may have _tag modifiers_. For instance, you may say -@\param[type=number] and this associates the modifier `type` with value `number` with this -particular param tag. A shorthand can be introduced for this common case, which is "@tparam - "; in the same way @\treturn is defined. +`@param[type=number]` and this associates the modifier `type` with value `number` with this +particular param tag. A shorthand has been introduced for this common case, which is `@tparam + `; in the same way `@treturn` is defined. This is useful for larger projects where you want to provide the argument and return value -types for your API, in a structured way that can be easily extracted later. There is a -useful function for creating new tags that can be used in `config.ld`: +types for your API, in a structured way that can be easily extracted later. + +These types can be combined, so that "?string|number" means "ether a string or a number"; +"?string" is short for "?|nil|string". However, for this last case you should usually use the +`opt` modifier discussed below. + +There is a useful function for creating new tags that can be used in `config.ld`: tparam_alias('string','string') -That is, "@string" will now have the same meaning as "@tparam string". +That is, **@string** will now have the same meaning as "@tparam string"; this also applies +to the optional type syntax "?|T1|T2". From 1.3, the following standard type aliases are predefined: @@ -912,27 +1094,26 @@ From 1.3, the following standard type aliases are predefined: * `tab` 'table' * `thread` -The exact form of `` is not defined, but here is a suggested scheme: +When using 'colon-style' (@{colon.lua}) it's possible to directly use types by prepending +them with '!'; '?' is also naturally understood. - number -- a plain type - Bonzo -- a known type; a reference link will be generated - {string,number} -- a 'list' tuple, built from type expressions - {A=string,N=number} -- a 'struct' tuple, ditto - {Bonzo,...} -- an array of Bonzo objects - {[string]=Bonzo,...} -- a map of Bonzo objects with string keys - Array(Bonzo) -- (assuming that Array is a container) +The exact form of `` is not defined, but here is one suggested scheme: -Currently the `type` modifier is the only one known and used by LDoc when generating HTML -output. However, any other modifiers are allowed and are available for use with your own -templates or for extraction by your own tools. + * `number` -- a plain type + * `Bonzo` -- a known type; a reference link will be generated + * `{string,number}` -- a 'list' tuple of two values, built from type expressions + * `{A=string,N=number}` -- a 'struct', ditto (But it's often better to create a named table and refer to it) + * `{Bonzo,...}` -- an array of Bonzo objects + * `{[string]=Bonzo,...}` -- a map of Bonzo objects with string keys + * `Array(Bonzo)` -- (assuming that Array is a container type) The `alias` function within configuration files has been extended so that alias tags can be defined as a tag plus a set of modifiers. So `tparam` is defined as: alias('tparam',{'param',modifiers={type="$1"}}) -As an extension, you're allowed to use '@param' tags in table definitions. This makes it -possible to use type alias like '@string' to describe fields, since they will expand to +As an extension, you're allowed to use **@param** tags in table definitions. This makes it +possible to use type aliases like **@string** to describe fields, since they will expand to 'param'. Another modifier understood by LDoc is `opt`. For instance, @@ -946,7 +1127,7 @@ Another modifier understood by LDoc is `opt`. For instance, end ----> displayed as: two (one [, two], three [, four]) -This modifier can also be used with type aliases. If a value is given for the modifier +This modifier can also be used with type aliases. If a value is given for `opt` then LDoc can present this as the default value for this optional argument. --- a function with typed args. @@ -961,11 +1142,41 @@ then LDoc can present this as the default value for this optional argument. end ----> displayed as: one (name, age [, calender='gregorian' [, offset=0]]) -(See `tests/styles/four.lua`) + +(See @{four.lua}, rendered [here](http://stevedonovan.github.io/ldoc/examples/four)) + +An experimental feature in 1.4 allows different 'return groups' to be defined. There may be +multiple **@return** tags, and the meaning of this is well-defined, since Lua functions may +return multiple values. However, being a dynamic language it may return a single value if +successful and two values (`nil`,an error message) if there is an error. This is in fact the +convention for returning 'normal' errors (like 'file not found') as opposed to parameter errors +(like 'file must be a string') that are often raised as errors. + +Return groups allow a documenter to specify the various possible return values of a function, +by specifying _number_ modifiers. All `return` tags with the same digit modifier belong together +as a group: + + ----- + -- function with return groups. + -- @return[1] result + -- @return[2] nil + -- @return[2] error message + function mul1() ... end + +This is the first function in @{multiple.lua}, and the [output](http://stevedonovan.github.io/ldoc/examples/multiple) +shows how return groups are presented, with an **Or** between the groups. + +This is rather clumsy, and so there is a shortcut, the **@error** tag which achieves the same result, +with helpful type information. + +Currently the `type`,`opt` and `` modifiers are the only ones known and used by LDoc when generating HTML +output. However, any other modifiers are allowed and are available for use with your own +templates or for extraction by your own tools. + ## Fields allowed in `config.ld` -These mostly have the same meaning as the corresponding parameters: +_Same meaning as the corresponding parameters:_ - `file` a file or directory containing sources. In `config.ld` this can also be a table of files and directories. @@ -975,26 +1186,54 @@ of files and directories. - `all` show local functions, etc as well in the docs - `format` markup processor, can be 'plain' (default), 'markdown' or 'discount' - `output` output name (default 'index') - - `dir` directory for output files (default 'docs') + - `dir` directory for output files (default 'doc') + - `colon` use colon style, instead of @ tag style + - `boilerplate` ignore first comment in all source files (e.g. license comments) - `ext` extension for output (default 'html') - `one` use a one-column layout - `style`, `template`: together these specify the directories for the style and and the template. In `config.ld` they may also be `true`, meaning use the same directory as the configuration file. + - `merge` allow documentation from different files to be merged into modules without +explicit **@submodule** tag -These only appear in `config.ld`: +_These only appear in the configuration file:_ - - `description` a project description used under the project title + - `description` a short project description used under the project title + - `full_description` when you _really_ need a longer project description - `examples` a directory or file: can be a table - - `readme` name of readme file (to be processed with Markdown) + - `readme` or `topics` readme files (to be processed with Markdown) + - `pretty` code prettify 'lua' (default) or 'lxsh' + - `charset` use if you want to override the UTF-8 default (also **@charset** in files) + - `sort` set if you want all items in alphabetical order - `no_return_or_parms` don't show parameters or return values in output - - `backtick_references` whether references in backticks will be resolved + - `backtick_references` whether references in backticks will be resolved. Happens by default +when using Markdown. When explicit will expand non-references in backticks into `` elements + - `plain` set to true if `format` is set but you don't want code comments processed + - `wrap` ?? - `manual_url` point to an alternative or local location for the Lua manual, e.g. 'file:///D:/dev/lua/projects/lua-5.1.4/doc/manual.html' - - `one` use a one-column output format - `no_summary` suppress the Contents summary + - `custom_see_handler` function that filters see-references + - `custom_display_name_handler` function that formats an item's name. The arguments are the item +and the default function used to format the name. For example, to show an icon or label beside any +function tagged with a certain tag: + -- define a @callback tag: + custom_tags = { { 'callback', hidden = true } } + + -- show a label beside functions tagged with @callback. + custom_display_name_handler = function(item, default_handler) + if item.type == 'function' and item.tags.callback then + return item.name .. ' [callback]' + end + return default_handler(item) + end + + - `not_luadoc` set to `true` if the docs break LuaDoc compatibility + - `no_space_before_args` set to `true` if you do not want a space between a function's name and its arguments. + - `template_escape` overrides the usual '#' used for Lua code in templates. This needs to be changed if the output format is Markdown, for instance. -Available functions are: +_Available functions are:_ - `alias(a,tag)` provide an alias `a` for the tag `tag`, for instance `p` as short for `param` @@ -1003,8 +1242,8 @@ an extension to be recognized as this language - `add_section` - `new_type(tag,header,project_level)` used to add new tags, which are put in their own section `header`. They may be 'project level'. - - `tparam_alias(name,type)` for instance, you may wish that `Object` means `@\tparam -Object`. + - `tparam_alias(name,type)` for instance, you may wish that `Object` becomes a new tag alias +that means `@tparam Object`. - `custom_see_handler(pattern,handler)`. If a reference matches `pattern`, then the extracted values will be passed to `handler`. It is expected to return link text and a suitable URI. (This match will happen before default processing.) @@ -1038,9 +1277,10 @@ Although not currently rendered by the template as HTML, they can be extracted b ## Generating HTML -LDoc, like LuaDoc, generates output HTML using a template, in this case `ldoc_ltp.lua`. This +LDoc, like LuaDoc, generates output HTML using a template, in this case `ldoc/html/ldoc_ltp.lua`. This is expanded by the powerful but simple preprocessor devised originally by [Rici Lake](http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor) which is now part of +Lake](http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor) which is now part of Penlight. There are two rules - any line starting with '#' is Lua code, which can also be embedded with '$(...)'. @@ -1053,7 +1293,11 @@ embedded with '$(...)'. This is then styled with `ldoc.css`. Currently the template and stylesheet is very much based on LuaDoc, so the results are mostly equivalent; the main change that the template has -been more generalized. The default location (indicated by '!') is the directory of `ldoc.lua`. +been more generalized. The default location (indicated by '!') is the directory of `ldoc_ltp.lua`. + +You will notice that the built-in templates and stylesheets end in `.lua`; this is simply to +make it easier for LDoc to find them. Where you are customizing one or both of the template +and stylesheet, they will have their usual extensions. You may customize how you generate your documentation by specifying an alternative style sheet and/or template, which can be deployed with your project. The parameters are `--style` @@ -1065,13 +1309,26 @@ be a valid directory relative to the ldoc invocation. An example of fully customized documentation is `tests/example/style`: this is what you could call 'minimal Markdown style' where there is no attempt to tag things (except emphasizing parameter names). The narrative alone _can_ to be sufficient, if it is written -appropriately. +well. + +There are two other stylesheets available in LDoc since 1.4; the first is `ldoc_one.css` which is what +you get from `one=true` and the second is `ldoc_pale.css`. This is a lighter theme which +might give some relief from the heavier colours of the default. You can use this style with +`style="!pale"` or `-s !pale`. +See the [Lake](http://stevedonovan.github.io/lake/modules/lakelibs.html) documentation +as an example of its use. Of course, there's no reason why LDoc must always generate HTML. `--ext` defines what output extension to use; this can also be set in the configuration file. So it's possible to write a template that converts LDoc output to LaTex, for instance. The separation of processing and presentation makes this kind of new application possible with LDoc. +From 1.4, LDoc has some limited support for generating Markdown output, although only +for single files currently. Use `--ext md` for this. 'ldoc/html/ldoc_md_ltp.lua' defines +the template for Markdown, but this can be overriden with `template` as above. It's another +example of minimal structure, and provides a better place to learn about these templates than the +rather elaborate default HTML template. + ## Internal Data Representation The `--dump` flag gives a rough text output on the console. But there is a more @@ -1104,7 +1361,7 @@ The basic data structure is straightforward: it is an array of 'modules' (projec entities, including scripts) which each contain an `item` array (functions, tables and so forth). -For instance, to find all functions which don't have a @return tag: +For instance, to find all functions which don't have a **@return** tag: return { filter = function (t) @@ -1118,8 +1375,8 @@ For instance, to find all functions which don't have a @return tag: end } -The internal naming is not always so consistent; `ret` corresponds to @return, and `params` -corresponds to @param. `item.params` is an array of the function parameters, in order; it +The internal naming is not always so consistent; `ret` corresponds to **@return**, and `params` +corresponds to **@param**. `item.params` is an array of the function parameters, in order; it is also a map from these names to the individual descriptions of the parameters. `item.modifiers` is a table where the keys are the tags and the values are arrays of diff --git a/ldoc-1.3.12-1.rockspec b/ldoc-1.3.12-1.rockspec new file mode 100644 index 00000000..9510efb1 --- /dev/null +++ b/ldoc-1.3.12-1.rockspec @@ -0,0 +1,61 @@ +package = "ldoc" +version = "1.3.12-1" + +source = { + dir="ldoc", + url = "http://stevedonovan.github.com/files/ldoc-1.3.12.zip" +} + +description = { + summary = "A Lua Documentation Tool", + detailed = [[ + LDoc is a LuaDoc-compatible documentation generator which can also + process C extension source. Markdown may be optionally used to + render comments, as well as integrated readme documentation and + pretty-printed example files + ]], + homepage='http://stevedonovan.github.com/ldoc', + maintainer='steve.j.donovan@gmail.com', + license = "MIT/X11", +} + + +dependencies = { + "penlight","markdown" +} + +build = { + type = "builtin", + modules = { + ["ldoc.tools"] = "ldoc/tools.lua", + ["ldoc.lang"] = "ldoc/lang.lua", + ["ldoc.parse"] = "ldoc/parse.lua", + ["ldoc.html"] = "ldoc/html.lua", + ["ldoc.lexer"] = "ldoc/lexer.lua", + ["ldoc.markup"] = "ldoc/markup.lua", + ["ldoc.prettify"] = "ldoc/prettify.lua", + ["ldoc.doc"] = "ldoc/doc.lua", + ["ldoc.html.ldoc_css"] = "ldoc/html/ldoc_css.lua", + ["ldoc.html.ldoc_ltp"] = "ldoc/html/ldoc_ltp.lua", + ["ldoc.html.ldoc_one_css"] = "ldoc/html/ldoc_one_css.lua", + ["ldoc.builtin.globals"] = "ldoc/builtin/globals.lua", + ["ldoc.builtin.coroutine"] = "ldoc/builtin/coroutine.lua", + ["ldoc.builtin.global"] = "ldoc/builtin/global.lua", + ["ldoc.builtin.debug"] = "ldoc/builtin/debug.lua", + ["ldoc.builtin.io"] = "ldoc/builtin/io.lua", + ["ldoc.builtin.lfs"] = "ldoc/builtin/lfs.lua", + ["ldoc.builtin.lpeg"] = "ldoc/builtin/lpeg.lua", + ["ldoc.builtin.math"] = "ldoc/builtin/math.lua", + ["ldoc.builtin.os"] = "ldoc/builtin/os.lua", + ["ldoc.builtin.package"] = "ldoc/builtin/package.lua", + ["ldoc.builtin.string"] = "ldoc/builtin/string.lua", + ["ldoc.builtin.table"] = "ldoc/builtin/table.lua", + }, + install = { + bin = { + ldoc = "ldoc.lua" + } + } +} + + diff --git a/ldoc-1.3.8-2.rockspec b/ldoc-1.3.8-2.rockspec new file mode 100644 index 00000000..096a86ee --- /dev/null +++ b/ldoc-1.3.8-2.rockspec @@ -0,0 +1,61 @@ +package = "ldoc" +version = "1.3.8-2" + +source = { + dir="ldoc", + url = "http://stevedonovan.github.com/files/ldoc-1.3.8.zip" +} + +description = { + summary = "A Lua Documentation Tool", + detailed = [[ + LDoc is a LuaDoc-compatible documentation generator which can also + process C extension source. Markdown may be optionally used to + render comments, as well as integrated readme documentation and + pretty-printed example files + ]], + homepage='http://stevedonovan.github.com/ldoc', + maintainer='steve.j.donovan@gmail.com', + license = "MIT/X11", +} + + +dependencies = { + "penlight","markdown" +} + +build = { + type = "builtin", + modules = { + ["ldoc.tools"] = "ldoc/tools.lua", + ["ldoc.lang"] = "ldoc/lang.lua", + ["ldoc.parse"] = "ldoc/parse.lua", + ["ldoc.html"] = "ldoc/html.lua", + ["ldoc.lexer"] = "ldoc/lexer.lua", + ["ldoc.markup"] = "ldoc/markup.lua", + ["ldoc.prettify"] = "ldoc/prettify.lua", + ["ldoc.doc"] = "ldoc/doc.lua", + ["ldoc.html.ldoc_css"] = "ldoc/html/ldoc_css.lua", + ["ldoc.html.ldoc_ltp"] = "ldoc/html/ldoc_ltp.lua", + ["ldoc.html.ldoc_one_css"] = "ldoc/html/ldoc_one_css.lua", + ["ldoc.builtin.globals"] = "ldoc/builtin/globals.lua", + ["ldoc.builtin.coroutine"] = "ldoc/builtin/coroutine.lua", + ["ldoc.builtin.global"] = "ldoc/builtin/global.lua", + ["ldoc.builtin.debug"] = "ldoc/builtin/debug.lua", + ["ldoc.builtin.io"] = "ldoc/builtin/io.lua", + ["ldoc.builtin.lfs"] = "ldoc/builtin/lfs.lua", + ["ldoc.builtin.lpeg"] = "ldoc/builtin/lpeg.lua", + ["ldoc.builtin.math"] = "ldoc/builtin/math.lua", + ["ldoc.builtin.os"] = "ldoc/builtin/os.lua", + ["ldoc.builtin.package"] = "ldoc/builtin/package.lua", + ["ldoc.builtin.string"] = "ldoc/builtin/string.lua", + ["ldoc.builtin.table"] = "ldoc/builtin/table.lua", + }, + install = { + bin = { + "ldoc.lua" + } + } +} + + diff --git a/ldoc-scm-2.rockspec b/ldoc-scm-2.rockspec new file mode 100644 index 00000000..fb86bae0 --- /dev/null +++ b/ldoc-scm-2.rockspec @@ -0,0 +1,62 @@ +package = "ldoc" +version = "scm-2" + +source = { + dir="LDoc", + url = "git://github.com/stevedonovan/LDoc.git" +} + +description = { + summary = "A Lua Documentation Tool", + detailed = [[ + LDoc is a LuaDoc-compatible documentation generator which can also + process C extension source. Markdown may be optionally used to + render comments, as well as integrated readme documentation and + pretty-printed example files + ]], + homepage='http://stevedonovan.github.com/ldoc', + maintainer='steve.j.donovan@gmail.com', + license = "MIT/X11", +} + +dependencies = { + "penlight","markdown" +} + +build = { + type = "builtin", + modules = { + ["ldoc.tools"] = "ldoc/tools.lua", + ["ldoc.lang"] = "ldoc/lang.lua", + ["ldoc.parse"] = "ldoc/parse.lua", + ["ldoc.html"] = "ldoc/html.lua", + ["ldoc.lexer"] = "ldoc/lexer.lua", + ["ldoc.markup"] = "ldoc/markup.lua", + ["ldoc.prettify"] = "ldoc/prettify.lua", + ["ldoc.markdown"] = "ldoc/markdown.lua", + ["ldoc.doc"] = "ldoc/doc.lua", + ["ldoc.html.ldoc_css"] = "ldoc/html/ldoc_css.lua", + ["ldoc.html.ldoc_ltp"] = "ldoc/html/ldoc_ltp.lua", + ["ldoc.html.ldoc_one_css"] = "ldoc/html/ldoc_one_css.lua", + ["ldoc.html.ldoc_pale_css"] = "ldoc/html/ldoc_pale_css.lua", + ["ldoc.builtin.globals"] = "ldoc/builtin/globals.lua", + ["ldoc.builtin.coroutine"] = "ldoc/builtin/coroutine.lua", + ["ldoc.builtin.global"] = "ldoc/builtin/global.lua", + ["ldoc.builtin.debug"] = "ldoc/builtin/debug.lua", + ["ldoc.builtin.io"] = "ldoc/builtin/io.lua", + ["ldoc.builtin.lfs"] = "ldoc/builtin/lfs.lua", + ["ldoc.builtin.lpeg"] = "ldoc/builtin/lpeg.lua", + ["ldoc.builtin.math"] = "ldoc/builtin/math.lua", + ["ldoc.builtin.os"] = "ldoc/builtin/os.lua", + ["ldoc.builtin.package"] = "ldoc/builtin/package.lua", + ["ldoc.builtin.string"] = "ldoc/builtin/string.lua", + ["ldoc.builtin.table"] = "ldoc/builtin/table.lua", + }, + copy_directories = {'doc','tests'}, + install = { + bin = { + ldoc = "ldoc.lua" + } + } +} + diff --git a/ldoc.lua b/ldoc.lua index fb209abf..e51f8633 100644 --- a/ldoc.lua +++ b/ldoc.lua @@ -7,7 +7,7 @@ -- -- C/C++ support for Lua extensions is provided. -- --- Available from LuaRocks as 'ldoc' and as a [Zip file](http://stevedonovan.github.com/files/ldoc-1.3.0.zip) +-- Available from LuaRocks as 'ldoc' and as a [Zip file](http://stevedonovan.github.com/files/ldoc-1.4.2.zip) -- -- [Github Page](https://github.com/stevedonovan/ldoc) -- @@ -25,6 +25,8 @@ local List = require 'pl.List' local stringx = require 'pl.stringx' local tablex = require 'pl.tablex' +-- Penlight compatibility +utils.unpack = utils.unpack or unpack or table.unpack local append = table.insert @@ -35,8 +37,8 @@ app.require_here() --- @usage local usage = [[ -ldoc, a documentation generator for Lua, vs 1.3.1 - -d,--dir (default docs) output directory +ldoc, a documentation generator for Lua, vs 1.4.2 + -d,--dir (default doc) output directory -o,--output (default 'index') output name -v,--verbose verbose -a,--all show local functions, etc, in docs @@ -44,25 +46,29 @@ ldoc, a documentation generator for Lua, vs 1.3.1 -m,--module module docs as text -s,--style (default !) directory for style sheet (ldoc.css) -l,--template (default !) directory for template (ldoc.ltp) - -1,--one use one-column output layout -p,--project (default ldoc) project name -t,--title (default Reference) page title -f,--format (default plain) formatting - can be markdown, discount or plain -b,--package (default .) top-level package basename (needed for module(...)) -x,--ext (default html) output file extension -c,--config (default config.ld) configuration name + -u,--unqualified don't show package name in sidebar links -i,--ignore ignore any 'no doc comment or no module' warnings + -X,--not_luadoc break LuaDoc compatibility. Descriptions may continue after tags. -D,--define (default none) set a flag to be used in config.ld -C,--colon use colon style -B,--boilerplate ignore first comment in source files -M,--merge allow module merging + -S,--simple no return or params, no summary + -O,--one one-column output layout --dump debug output dump --filter (default none) filter output as Lua data (e.g pl.pretty.dump) --tags (default none) show all references to given tags, comma-separated (string) source file or directory containing source `ldoc .` reads options from an `config.ld` file in same directory; - `ldoc -c path/to/myconfig.ld .` reads options from `path/to/myconfig.ld` + `ldoc -c path/to/myconfig.ld ` reads options from `path/to/myconfig.ld` + and processes if 'file' was not defined in the ld file. ]] local args = lapp(usage) local lfs = require 'lfs' @@ -77,22 +83,15 @@ local Item,File,Module = doc.Item,doc.File,doc.Module local quit = utils.quit -class.ModuleMap(KindMap) -local ModuleMap = ModuleMap +local ModuleMap = class(KindMap) +doc.ModuleMap = ModuleMap function ModuleMap:_init () self.klass = ModuleMap self.fieldname = 'section' end -ModuleMap:add_kind('function','Functions','Parameters') -ModuleMap:add_kind('table','Tables','Fields') -ModuleMap:add_kind('field','Fields') -ModuleMap:add_kind('lfunction','Local Functions','Parameters') -ModuleMap:add_kind('annotation','Issues') - - -class.ProjectMap(KindMap) +local ProjectMap = class(KindMap) ProjectMap.project_level = true function ProjectMap:_init () @@ -100,10 +99,7 @@ function ProjectMap:_init () self.fieldname = 'type' end -ProjectMap:add_kind('module','Modules') -ProjectMap:add_kind('script','Scripts') -ProjectMap:add_kind('topic','Topics') -ProjectMap:add_kind('example','Examples') + local lua, cc = lang.lua, lang.cc @@ -115,17 +111,61 @@ local file_types = { ['.cpp'] = cc, ['.cxx'] = cc, ['.C'] = cc, - ['.mm'] = cc + ['.mm'] = cc, + ['.moon'] = lang.moon, } - ------- ldoc external API ------------ -- the ldoc table represents the API available in `config.ld`. -local ldoc = {} +local ldoc = { charset = 'UTF-8' } + +local known_types, kind_names = {} + +local function lookup (itype,igroup,isubgroup) + local kn = kind_names[itype] + known_types[itype] = true + if kn then + if type(kn) == 'string' then + igroup = kn + else + igroup = kn[1] + isubgroup = kn[2] + end + end + return itype, igroup, isubgroup +end + +local function setup_kinds () + kind_names = ldoc.kind_names or {} + + ModuleMap:add_kind(lookup('function','Functions','Parameters')) + ModuleMap:add_kind(lookup('table','Tables','Fields')) + ModuleMap:add_kind(lookup('field','Fields')) + ModuleMap:add_kind(lookup('lfunction','Local Functions','Parameters')) + ModuleMap:add_kind(lookup('annotation','Issues')) + + ProjectMap:add_kind(lookup('module','Modules')) + ProjectMap:add_kind(lookup('script','Scripts')) + ProjectMap:add_kind(lookup('classmod','Classes')) + ProjectMap:add_kind(lookup('topic','Topics')) + ProjectMap:add_kind(lookup('example','Examples')) + + for k in pairs(kind_names) do + if not known_types[k] then + quit("unknown item type "..tools.quote(k).." in kind_names") + end + end +end + + local add_language_extension +-- hacky way for doc module to be passed options... +doc.ldoc = ldoc -local function override (field) - if ldoc[field] ~= nil then args[field] = ldoc[field] end +-- if the corresponding argument was the default, then any ldoc field overrides +local function override (field,defval) + defval = defval or false + if args[field] == defval and ldoc[field] ~= nil then args[field] = ldoc[field] end end -- aliases to existing tags can be defined. E.g. just 'p' for 'param' @@ -144,6 +184,8 @@ function ldoc.tparam_alias (name,type) ldoc.alias(name,{'param',modifiers={type=type}}) end +ldoc.alias ('error',doc.error_macro) + ldoc.tparam_alias 'string' ldoc.tparam_alias 'number' ldoc.tparam_alias 'int' @@ -173,19 +215,22 @@ function ldoc.new_type (tag, header, project_level,subfield) end function ldoc.manual_url (url) - global.set_manual_url(url) + global.set_manual_url(url) end function ldoc.custom_see_handler(pat, handler) - doc.add_custom_see_handler(pat, handler) + doc.add_custom_see_handler(pat, handler) end local ldoc_contents = { - 'alias','add_language_extension','new_type','add_section', 'tparam_alias', + 'alias','add_language_extension','custom_tags','new_type','add_section', 'tparam_alias', 'file','project','title','package','format','output','dir','ext', 'topics', - 'one','style','template','description','examples', 'pretty', - 'readme','all','manual_url', 'ignore', 'colon','boilerplate','merge', 'wrap', + 'one','style','template','description','examples', 'pretty', 'charset', 'plain', + 'readme','all','manual_url', 'ignore', 'colon', 'sort', 'module_file','vars', + 'boilerplate','merge', 'wrap', 'not_luadoc', 'template_escape','merge_error_groups', 'no_return_or_parms','no_summary','full_description','backtick_references', 'custom_see_handler', + 'no_space_before_args','parse_extra','no_lua_ref','sort_modules','use_markdown_titles', + 'unqualified', 'custom_display_name_handler', 'kind_names', 'custom_references', } ldoc_contents = tablex.makeset(ldoc_contents) @@ -277,15 +322,31 @@ if args.file == '.' then elseif type(args.file) == 'table' then for i,f in ipairs(args.file) do args.file[i] = abspath(f) - print(args.file[i]) end else args.file = abspath(args.file) end else + -- user-provided config file + if args.config ~= 'config.ld' then + local err + config_dir,err = read_ldoc_config(args.config) + if err then quit("no "..quote(args.config).." found") end + end + -- with user-provided file args.file = abspath(args.file) end +if type(ldoc.custom_tags) == 'table' then -- custom tags + for i, custom in ipairs(ldoc.custom_tags) do + if type(custom) == 'string' then + custom = {custom} + ldoc.custom_tags[i] = custom + end + doc.add_tag(custom[1], 'ML') + end +end -- custom tags + local source_dir = args.file if type(source_dir) == 'table' then source_dir = source_dir[1] @@ -293,6 +354,7 @@ end if type(source_dir) == 'string' and path.isfile(source_dir) then source_dir = path.splitpath(source_dir) end +source_dir = source_dir:gsub('[/\\]%.$','') ---------- specifying the package for inferring module names -------- -- If you use module(...), or forget to explicitly use @module, then @@ -304,6 +366,8 @@ end -- * 'NAME' explicitly give the base module package name -- +override ('package','.') + local function setup_package_base() if ldoc.package then args.package = ldoc.package end if args.package == '.' then @@ -334,7 +398,8 @@ local function process_file (f, flist) local ext = path.extension(f) local ftype = file_types[ext] if ftype then - if args.verbose then print(path.basename(f)) end + if args.verbose then print(f) end + ftype.extra = ldoc.parse_extra or {} local F,err = parse.file(f,ftype,args) if err then if F then @@ -352,9 +417,39 @@ setup_package_base() override 'colon' override 'merge' +override 'not_luadoc' +override 'module_file' +override 'boilerplate' + +setup_kinds() + +-- LDoc is doing plain ole C, don't want random Lua references! +if ldoc.parse_extra and ldoc.parse_extra.C then + ldoc.no_lua_ref = true +end + +if ldoc.merge_error_groups == nil then + ldoc.merge_error_groups = 'Error Message' +end + +-- ldoc.module_file establishes a partial ordering where the +-- master module files are processed first. +local function reorder_module_file () + if args.module_file then + local mf = {} + for mname, f in pairs(args.module_file) do + local fullpath = abspath(f) + mf[fullpath] = true + end + return function(x,y) + return mf[x] and not mf[y] + end + end +end if type(args.file) == 'table' then -- this can only be set from config file so we can assume it's already read + args.file.sortfn = reorder_module_file() process_file_list(args.file,'*.*',process_file, file_list) if #file_list == 0 then quit "no source files specified" end elseif path.isdir(args.file) then @@ -371,9 +466,13 @@ elseif path.isdir(args.file) then end end end + -- process files, optionally in order that respects master module files + local sortfn = reorder_module_file() + if sortfn then files:sort(sortfn) end for f in files:iter() do process_file(f, file_list) end + if #file_list == 0 then quit(quote(args.file).." contained no source files") end @@ -393,9 +492,10 @@ else quit ("file or directory does not exist: "..quote(args.file)) end + -- create the function that renders text (descriptions and summaries) -- (this also will initialize the code prettifier used) -override 'format' +override ('format','plain') override 'pretty' ldoc.markup = markup.create(ldoc, args.format,args.pretty) @@ -433,10 +533,16 @@ if type(ldoc.examples) == 'table' then }) -- wrap prettify for this example so it knows which file to blame -- if there's a problem - item.postprocess = function(code) return prettify.lua(f,code,0,true) end + local ext = path.extension(f):sub(2) + item.postprocess = function(code) return prettify.lua(ext,f,code,0,true) end end) end +if args.simple then + ldoc.no_return_or_parms=true + ldoc.no_summary=true +end + ldoc.readme = ldoc.readme or ldoc.topics if type(ldoc.readme) == 'string' then ldoc.readme = {ldoc.readme} @@ -450,6 +556,9 @@ if type(ldoc.readme) == 'table' then -- headers in the readme, which are attached to the File. So -- we pass the File to the postprocesser, which will insert the section markers -- and resolve inline @ references. + if ldoc.use_markdown_titles then + item.display_name = F.display_name + end item.postprocess = function(txt) return ldoc.markup(txt,F) end end) end @@ -479,19 +588,17 @@ for mod in module_list:iter() do project:add(mod,module_list) end --- the default is not to show local functions in the documentation. -if not args.all and not ldoc.all then - for mod in module_list:iter() do - mod:mask_locals() - end -end +override 'all' -table.sort(module_list,function(m1,m2) - return m1.name < m2.name -end) +if ldoc.sort_modules then + table.sort(module_list,function(m1,m2) + return m1.name < m2.name + end) +end ldoc.single = modcount == 1 and first_module or nil +--do return end -------- three ways to dump the object graph after processing ----- @@ -502,8 +609,12 @@ if args.module then if args.module == true then file_list[1]:dump(args.verbose) else - local fun = module_list[1].items.by_name[args.module] - if not fun then quit(quote(args.module).." is not part of "..quote(args.file)) end + local M,name = module_list[1], args.module + local fun = M.items.by_name[name] + if not fun then + fun = M.items.by_name[M.mod_name..':'..name] + end + if not fun then quit(quote(name).." is not part of "..quote(args.file)) end fun:dump(true) end return @@ -534,15 +645,46 @@ if args.filter ~= 'none' then os.exit() end +-- can specify format, output, dir and ext in config.ld +override ('output','index') +override ('dir','doc') +override ('ext','html') +override 'one' + +-- handling styling and templates -- ldoc.css, ldoc.templ = 'ldoc.css','ldoc.ltp' +-- special case: user wants to generate a .md file from a .lua file +if args.ext == 'md' then + if #module_list ~= 1 then + quit("can currently only generate Markdown output from one module only") + end + if ldoc.template == '!' then + ldoc.template = '!md' + end + args.output = module_list[1].name + args.dir = '.' + ldoc.template_escape = '>' + ldoc.style = false + args.ext = '.md' +end + +local function match_bang (s) + if type(s) ~= 'string' then return end + return s:match '^!(.*)' +end + local function style_dir (sname) local style = ldoc[sname] local dir + if style==false and sname == 'style' then + args.style = false + ldoc.css = false + end if style then if style == true then dir = config_dir - elseif type(style) == 'string' and path.isdir(style) then + elseif type(style) == 'string' and (path.isdir(style) or match_bang(style)) then dir = style else quit(quote(tostring(style)).." is not a directory") @@ -551,7 +693,6 @@ local function style_dir (sname) end end - -- the directories for template and stylesheet can be specified -- either by command-line '--template','--style' arguments or by 'template and -- 'style' fields in config.ld. @@ -562,36 +703,42 @@ end style_dir 'style' style_dir 'template' --- can specify format, output, dir and ext in config.ld -override 'output' -override 'dir' -override 'ext' -override 'one' -override 'boilerplate' - if not args.ext:find '^%.' then args.ext = '.'..args.ext end if args.one then - ldoc.css = 'ldoc_one.css' + ldoc.style = '!one' end -if args.style == '!' or args.template == '!' then +local builtin_style, builtin_template = match_bang(args.style),match_bang(args.template) +if builtin_style or builtin_template then -- '!' here means 'use built-in templates' local tmpdir = path.join(path.is_windows and os.getenv('TMP') or '/tmp','ldoc') if not path.isdir(tmpdir) then lfs.mkdir(tmpdir) end local function tmpwrite (name) - utils.writefile(path.join(tmpdir,name),require('ldoc.html.'..name:gsub('%.','_'))) + local ok,text = pcall(require,'ldoc.html.'..name:gsub('%.','_')) + if not ok then + quit("cannot find builtin template "..name..": "..text) + end + if not utils.writefile(path.join(tmpdir,name),text) then + quit("cannot write to temp directory "..tmpdir) + end end - if args.style == '!' then - tmpwrite(ldoc.templ) + if builtin_style then + if builtin_style ~= '' then + ldoc.css = 'ldoc_'..builtin_style..'.css' + end + tmpwrite(ldoc.css) args.style = tmpdir end - if args.template == '!' then - tmpwrite(ldoc.css) + if builtin_template then + if builtin_template ~= '' then + ldoc.templ = 'ldoc_'..builtin_template..'.ltp' + end + tmpwrite(ldoc.templ) args.template = tmpdir end end diff --git a/ldoc/builtin/coroutine.lua b/ldoc/builtin/coroutine.lua index 6992e20f..b6ea4630 100644 --- a/ldoc/builtin/coroutine.lua +++ b/ldoc/builtin/coroutine.lua @@ -1,6 +1,7 @@ --- creating and controlling coroutines. +-- @module coroutine -module 'coroutine' +local coroutine = {} --- -- Creates a new coroutine, with body `f`. `f` must be a Lua @@ -46,3 +47,4 @@ function coroutine.wrap(f) end -- `yield` are passed as extra results to `resume`. function coroutine.yield(...) end +return coroutine diff --git a/ldoc/builtin/debug.lua b/ldoc/builtin/debug.lua index a5dd0be7..981a638f 100644 --- a/ldoc/builtin/debug.lua +++ b/ldoc/builtin/debug.lua @@ -1,7 +1,7 @@ --- getting runtime debug information. +-- @module debug -module 'debug' - +local debug = {} --- -- Enters an interactive mode with the user, running each string that -- the user enters. Using simple commands and other debug facilities, @@ -121,3 +121,4 @@ function debug.setmetatable(object, table) end -- with the given index. Otherwise, it returns the name of the upvalue. function debug.setupvalue(func, up, value) end +return debug diff --git a/ldoc/builtin/globals.lua b/ldoc/builtin/globals.lua index 06dbdd5e..df6a903a 100644 --- a/ldoc/builtin/globals.lua +++ b/ldoc/builtin/globals.lua @@ -73,29 +73,66 @@ else globals.set_manual_url 'http://www.lua.org/manual/5.1/manual.html' end +-- external libs tracked by LDoc using LDoc style +local xlibs = { + lfs='lfs.html', lpeg='lpeg.html', +} +local xlib_url = 'http://stevedonovan.github.io/lua-stdlibs/modules/' + local tables = globals.tables -local function function_ref (name) - return {href = fun_ref..name, label = name} +local function function_ref (name,tbl) + local href + if not tbl then -- can only be a standard Lua global function + if globals.functions[name] then + return {href = fun_ref..name, label = name} + else + return nil + end + end + if tables[tbl] then -- function inside standard Lua table + local t = rawget(_G,tbl) -- do a quick sanity check + if not rawget(t,name) then + return nil + end + name = tbl..'.'..name + href = fun_ref..name + elseif xlibs[tbl] then -- in external libs, use LDoc style + local t = require('ldoc.builtin.'..tbl) + if not rawget(t,name) then + return nil + end + href = xlib_url..xlibs[tbl]..'#'..name + name = tbl..'.'..name + else + return nil + end + return {href = href, label = name} end -local function module_ref (name) - return {href = manual..tables[name], label = name} +local function module_ref (tbl) + local href + if tables[tbl] ~= nil then -- standard Lua table + href = manual..tables[tbl] + elseif xlibs[tbl] then -- external lib + href = xlib_url..xlibs[tbl] + else + return nil + end + return {href = href, label = tbl} end function globals.lua_manual_ref (name) local tbl,fname = tools.split_dotted_name(name) + local ref if not tbl then -- plain symbol - if functions[name] then - return function_ref(name) - end - if tables[name] then - return module_ref(name) - end + ref = function_ref(name) + if ref then return ref end + ref = module_ref(name) + if ref then return ref end else - if tables[tbl] then - return function_ref(name) - end + ref = function_ref(fname,tbl) + if ref then return ref end end return nil end diff --git a/ldoc/builtin/io.lua b/ldoc/builtin/io.lua index f8145a11..28253dfa 100644 --- a/ldoc/builtin/io.lua +++ b/ldoc/builtin/io.lua @@ -1,6 +1,7 @@ --- Reading and Writing Files. +-- @module io -module 'io' +local io = {} --- -- Equivalent to `file:close()`. Without a `file`, closes the default @@ -155,3 +156,4 @@ function file:setvbuf(mode , size) end -- `string.format` before `write`. function file:write(...) end +return io diff --git a/ldoc/builtin/lfs.lua b/ldoc/builtin/lfs.lua index 772fbb04..ed37bc1d 100644 --- a/ldoc/builtin/lfs.lua +++ b/ldoc/builtin/lfs.lua @@ -1,6 +1,7 @@ --- File and Directory manipulation +-- @module lfs -module 'lfs' +local lfs = {} --- -- Returns a table with the file attributes corresponding to filepath (or nil @@ -73,7 +74,7 @@ function lfs.dir(path) end -- and its length; both should be numbers. -- Returns true if the operation was successful; in case of error, it returns -- nil plus an error string. -function lfs.lock(filehandle, mode, start, length) +function lfs.lock(filehandle, mode, start, length) end --- -- Creates a new directory. The argument is the name of the new directory. @@ -120,3 +121,5 @@ function lfs.touch(filepath , atime , mtime) end -- Returns true if the operation was successful; in case of error, it returns -- nil plus an error string. function lfs.unlock(filehandle, start, length) end + +return lfs diff --git a/ldoc/builtin/lpeg.lua b/ldoc/builtin/lpeg.lua index 97053a74..b2c020e0 100644 --- a/ldoc/builtin/lpeg.lua +++ b/ldoc/builtin/lpeg.lua @@ -1,6 +1,7 @@ --- LPeg PEG pattern matching. +-- @module lpeg -module 'lpeg' +local lpeg = {} --- -- The matching function. It attempts to match the given pattern against the @@ -209,3 +210,5 @@ function lpeg.Ct(patt) end -- Any extra values returned by the function become the values produced by the -- capture. function lpeg.Cmt(patt, function) end + +return lpeg diff --git a/ldoc/builtin/math.lua b/ldoc/builtin/math.lua index d3e9e79e..9e97544c 100644 --- a/ldoc/builtin/math.lua +++ b/ldoc/builtin/math.lua @@ -1,6 +1,7 @@ --- standard mathematical functions. +-- @module math -module 'math' +local math = {} --- -- Returns the absolute value of `x`. @@ -140,3 +141,4 @@ function math.tan(x) end -- Returns the hyperbolic tangent of `x`. function math.tanh(x) end +return math diff --git a/ldoc/builtin/os.lua b/ldoc/builtin/os.lua index 87a4a3fe..a8b36656 100644 --- a/ldoc/builtin/os.lua +++ b/ldoc/builtin/os.lua @@ -1,6 +1,7 @@ --- Operating System facilities like date, time and program execution. +-- @module os -module 'os' +local os = {} --- -- Returns an approximation of the amount in seconds of CPU time used by @@ -108,3 +109,4 @@ function os.time(table) end -- removes the file when the program ends. function os.tmpname() end +return os diff --git a/ldoc/builtin/package.lua b/ldoc/builtin/package.lua index 45e9b5da..61a60c59 100644 --- a/ldoc/builtin/package.lua +++ b/ldoc/builtin/package.lua @@ -1,6 +1,7 @@ --- controlling how `require` finds packages. +-- @module package -module 'package' +local package = {} --- -- The path used by `require` to search for a C loader. @@ -93,3 +94,4 @@ function package.loadlib(libname, funcname) end -- environment. To be used as an option to function `module`. function package.seeall(module) end +return package diff --git a/ldoc/builtin/string.lua b/ldoc/builtin/string.lua index e69ccb1f..af7448f6 100644 --- a/ldoc/builtin/string.lua +++ b/ldoc/builtin/string.lua @@ -1,6 +1,7 @@ --- string operations like searching and matching. +-- @module string -module 'string' +local string = {} --- -- Returns the internal numerical codes of the characters `s[i]`, `s[i+1]`, @@ -170,3 +171,5 @@ function string.sub(s, i , j) end -- definition of what a lowercase letter is depends on the current locale. function string.upper(s) end +return string + diff --git a/ldoc/builtin/table.lua b/ldoc/builtin/table.lua index a386a302..7221535a 100644 --- a/ldoc/builtin/table.lua +++ b/ldoc/builtin/table.lua @@ -1,6 +1,7 @@ --- manipulating Lua tables. +-- @module table -module 'table' +local table = {} --- -- Given an array where all elements are strings or numbers, returns @@ -39,3 +40,5 @@ function table.remove(table , pos) end -- (so that `not comp(a[i+1],a[i])` will be true after the sort). If `comp` -- is not given, then the '<' operator will be used. function table.sort(table , comp) end + +return table diff --git a/ldoc/doc.lua b/ldoc/doc.lua index c2f239b4..ea25ad0d 100644 --- a/ldoc/doc.lua +++ b/ldoc/doc.lua @@ -21,13 +21,13 @@ local TAG_MULTI,TAG_ID,TAG_SINGLE,TAG_TYPE,TAG_FLAG,TAG_MULTI_LINE = 'M','id','S -- - 'N' tags which have no associated value, like 'local` (TAG_FLAG) -- - 'T' tags which represent a type, like 'function' (TAG_TYPE) local known_tags = { - param = 'M', see = 'M', usage = 'ML', ['return'] = 'M', field = 'M', author='M'; + param = 'M', see = 'M', comment = 'M', usage = 'ML', ['return'] = 'M', field = 'M', author='M',set='M'; class = 'id', name = 'id', pragma = 'id', alias = 'id', within = 'id', copyright = 'S', summary = 'S', description = 'S', release = 'S', license = 'S', - fixme = 'S', todo = 'S', warning = 'S', raise = 'S', + fixme = 'S', todo = 'S', warning = 'S', raise = 'S', charset = 'S', ['local'] = 'N', export = 'N', private = 'N', constructor = 'N', static = 'N'; -- project-level - module = 'T', script = 'T', example = 'T', topic = 'T', submodule='T', + module = 'T', script = 'T', example = 'T', topic = 'T', submodule='T', classmod='T', -- module-level ['function'] = 'T', lfunction = 'T', table = 'T', section = 'T', type = 'T', annotation = 'T', factory = 'T'; @@ -39,12 +39,18 @@ known_tags._project_level = { script = true, example = true, topic = true, - submodule = true; + submodule = true, + classmod = true, } known_tags._code_types = { module = true, - script = true + script = true, + classmod = true, +} + +known_tags._presentation_names = { + classmod = 'Class', } known_tags._module_info = { @@ -99,12 +105,23 @@ function doc.class_tag (tag) return tag == 'type' or tag == 'factory' end +-- how the type wants to be formally presented; e.g. 'module' becomes 'Module' +-- but 'classmod' will become 'Class' +function doc.presentation_name (tag) + local name = known_tags._presentation_names[tag] + if not name then + name = tag:gsub('(%a)(%a*)',function(f,r) + return f:upper()..r + end) + end + return name +end + function doc.module_info_tags () return List.iter(known_tags._module_info) end - --- annotation tags can appear anywhere in the code and may contain of these tags: +-- annotation tags can appear anywhere in the code and may contain any of these tags: known_tags._annotation_tags = { fixme = true, todo = true, warning = true } @@ -112,17 +129,20 @@ known_tags._annotation_tags = { local acount = 1 function doc.expand_annotation_item (tags, last_item) - if tags.summary ~= '' then return false end + if tags.summary ~= '' or last_item == nil then return false end + local item_name = last_item.tags.name for tag, value in pairs(tags) do if known_tags._annotation_tags[tag] then - tags.class = 'annotation' - tags.summary = value - local item_name = last_item and last_item.tags.name or '?' - tags.name = item_name..'-'..tag..acount + tags:add('class','annotation') + tags:add('summary',value) + tags:add('name',item_name..'-'..tag..acount) acount = acount + 1 return true + elseif tag == 'return' then + last_item:set_tag(tag,value) end end + return false end -- we process each file, resulting in a File object, which has a list of Item objects. @@ -156,6 +176,7 @@ function File:export_item (name) for item in self.items:iter() do local tags = item.tags if tags.name == name then + tags.export = true if tags['local'] then tags['local'] = nil end @@ -177,7 +198,7 @@ local function mod_section_type (this_mod) return this_mod and this_mod.section and this_mod.section.type end -local function find_module_in_files (name) +function File:find_module_in_files (name) for f in File.list:iter() do for m in f.modules:iter() do if m.name == name then @@ -187,17 +208,18 @@ local function find_module_in_files (name) end end +local function init_within_section (mod,name) + mod.kinds:add_kind(name, name) + mod.enclosing_section = mod.section + mod.section = nil + return name +end + function File:finish() local this_mod local items = self.items local tagged_inside - local function add_section (item, display_name) - display_name = display_name or item.display_name - this_mod.section = item - this_mod.kinds:add_kind(display_name,display_name..' ',nil,item) - this_mod.sections:append(item) - this_mod.sections.by_name[display_name:gsub('%A','_')] = item - end + self.args = self.args or {} for item in items:iter() do if mod_section_type(this_mod) == 'factory' and item.tags then local klass = '@{'..this_mod.section.name..'}' @@ -209,24 +231,31 @@ function File:finish() end end item:finish() - if doc.project_level(item.type) then + -- the default is not to show local functions in the documentation. + if not self.args.all and (item.type=='lfunction' or (item.tags and item.tags['local'])) then + -- don't add to the module -- + elseif doc.project_level(item.type) then this_mod = item local package,mname,submodule if item.type == 'module' then -- if name is 'package.mod', then mod_name is 'mod' package,mname = split_dotted_name(this_mod.name) if self.args.merge then - local mod,mf = find_module_in_files(item.name) + local mod,mf = self:find_module_in_files(item.name) if mod then print('found master module',mf) this_mod = mod + if this_mod.section then + print '***closing section from master module***' + this_mod.section = nil + end submodule = true end end elseif item.type == 'submodule' then local mf submodule = true - this_mod,mf = find_module_in_files(item.name) + this_mod,mf = self:find_module_in_files(item.name) if this_mod == nil then self:error("'"..item.name.."' not found for submodule") end @@ -240,7 +269,7 @@ function File:finish() if not submodule then this_mod.package = package this_mod.mod_name = mname - this_mod.kinds = ModuleMap() -- the iterator over the module contents + this_mod.kinds = doc.ModuleMap() -- the iterator over the module contents self.modules:append(this_mod) end elseif doc.section_tag(item.type) then @@ -249,21 +278,30 @@ function File:finish() this_mod.section = nil else local summary = item.summary:gsub('%.$','') + local lookup_name if doc.class_tag(item.type) then display_name = 'Class '..item.name + lookup_name = item.name item.module = this_mod this_mod.items.by_name[item.name] = item else display_name = summary + lookup_name = summary end item.display_name = display_name - add_section(item) + this_mod.section = item + this_mod.kinds:add_kind(display_name,display_name..' ',nil,item) + this_mod.sections:append(item) + this_mod.sections.by_name[lookup_name:gsub('%A','_')] = item end else local to_be_removed -- add the item to the module's item list if this_mod then -- new-style modules will have qualified names like 'mod.foo' + if item.name == nil then + self:error("item's name is nil") + end local mod,fname = split_dotted_name(item.name) -- warning for inferred unqualified names in new style modules -- (retired until we handle methods like Set:unset() properly) @@ -280,30 +318,44 @@ function File:finish() item.name = fname end - local enclosing_section if tagged_inside then item.tags.within = tagged_inside end if item.tags.within then - local name = item.tags.within - this_mod.kinds:add_kind(name, name) - enclosing_section = this_mod.section - this_mod.section = nil + init_within_section(this_mod,item.tags.within) end -- right, this item was within a section or a 'class' local section_description - if this_mod.section then + local classmod = this_mod.type == 'classmod' + if this_mod.section or classmod then + local stype local this_section = this_mod.section - item.section = this_section.display_name + if this_section then + item.section = this_section.display_name + stype = this_section.type + end -- if it was a class, then if the name is unqualified then it becomes -- 'Class:foo' (unless flagged as being a constructor, static or not a function) - local stype = this_section.type - if doc.class_tag(stype) then - if not item.name:match '[:%.]' then -- not qualified - local class = this_section.name + if doc.class_tag(stype) or classmod then + if not item.name:match '[:%.]' then -- not qualified name! + -- a class is either a @type section or a @classmod module. Is this a _method_? + local class = classmod and this_mod.name or this_section.name local static = item.tags.constructor or item.tags.static or item.type ~= 'function' - item.name = class..(not static and ':' or '.')..item.name + -- methods and metamethods go into their own special sections... + if classmod and item.type == 'function' then + local inferred_section + if item.name:match '^__' then + inferred_section = 'Metamethods' + elseif not static then + inferred_section = 'Methods' + end + if inferred_section then + item.tags.within = init_within_section(this_mod,inferred_section) + end + end + -- Whether to use '.' or the language's version of ':' (e.g. \ for Moonscript) + item.name = class..(not static and this_mod.file.lang.method_call or '.')..item.name end if stype == 'factory' then if item.tags.private then to_be_removed = true @@ -315,12 +367,23 @@ function File:finish() end end end - section_description = this_section.summary..' '..this_section.description - elseif item.tags.within then + if this_section then + section_description = this_section.summary..' '..(this_section.description or '') + this_section.summary = '' + elseif item.tags.within then + section_description = item.tags.within + item.section = section_description + else + if item.type == 'function' or item.type == 'lfunction' then + section_description = "Methods" + end + item.section = item.type + end + elseif item.tags.within then -- ad-hoc section... section_description = item.tags.within item.section = section_description else -- otherwise, just goes into the default sections (Functions,Tables,etc) - item.section = item.type + item.section = item.type; end item.module = this_mod @@ -332,7 +395,10 @@ function File:finish() end -- restore current section after a 'within' - if enclosing_section then this_mod.section = enclosing_section end + if this_mod.enclosing_section then + this_mod.section = this_mod.enclosing_section + this_mod.enclosing_section = nil + end else -- must be a free-standing function (sometimes a problem...) @@ -346,7 +412,7 @@ end -- is not empty. function File:add_document_section(title) - local section = title:gsub('%A','_') + local section = title:gsub('%W','_') self:new_item { name = section, class = 'section', @@ -369,10 +435,7 @@ function Item:_init(tags,file,line) self.tags = {} self.formal_args = tags.formal_args tags.formal_args = nil - local iter = tags.iter - if not iter then - iter = Map.iter - end + local iter = tags.iter or Map.iter for tag in iter(tags) do self:set_tag(tag,tags[tag]) end @@ -384,14 +447,36 @@ function Item:add_to_description (rest) end end +function Item:trailing_warning (kind,tag,rest) + if type(rest)=='string' and #rest > 0 then + Item.warning(self,kind.." tag: '"..tag..'" has trailing text ; use not_luadoc=true if you want description to continue between tags\n"'..rest..'"') + end +end + +local function is_list (l) + return getmetatable(l) == List +end + function Item:set_tag (tag,value) local ttype = known_tags[tag] + local args = self.file.args if ttype == TAG_MULTI or ttype == TAG_MULTI_LINE then -- value is always a List! - if getmetatable(value) ~= List then + local ovalue = self.tags[tag] + if ovalue then -- already defined, must be a list + --print(tag,ovalue,value) + if is_list(value) then + ovalue:extend(value) + else + ovalue:append(value) + end + value = ovalue + end + -- these multiple values are always represented as lists + if not is_list(value) then value = List{value} end - if ttype ~= TAG_MULTI_LINE then + if ttype ~= TAG_MULTI_LINE and args and args.not_luadoc then local last = value[#value] if type(last) == 'string' and last:match '\n' then local line,rest = last:match('([^\n]+)(.*)') @@ -405,19 +490,32 @@ function Item:set_tag (tag,value) if type(value) == 'table' then if value.append then -- it was a List! -- such tags are _not_ multiple, e.g. name - self:error("'"..tag.."' cannot have multiple values") + if tag == 'class' and value:contains 'example' then + self:error("cannot use 'example' tag for functions or tables. Use 'usage'") + else + self:error("'"..tag.."' cannot have multiple values; "..tostring(value)) + end end value = value[1] modifiers = value.modifiers end + if value == nil then self:error("Tag without value: "..tag) end local id, rest = tools.extract_identifier(value) self.tags[tag] = id - self:add_to_description(rest) + if args and args.not_luadoc then + self:add_to_description(rest) + else + self:trailing_warning('id',tag,rest) + end elseif ttype == TAG_SINGLE then self.tags[tag] = value elseif ttype == TAG_FLAG then self.tags[tag] = true - self:add_to_description(value) + if args.not_luadoc then + self:add_to_description(value) + else + self:trailing_warning('flag',tag,value) + end else Item.warning(self,"unknown tag: '"..tag.."' "..tostring(ttype)) end @@ -430,7 +528,7 @@ function Item.check_tag(tags,tag, value, modifiers) if alias then if type(alias) == 'string' then tag = alias - else + elseif type(alias) == 'table' then --{ tag, value=, modifiers = } local avalue,amod tag, avalue, amod = alias[1],alias.value,alias.modifiers if avalue then value = avalue..' '..value end @@ -444,6 +542,8 @@ function Item.check_tag(tags,tag, value, modifiers) modifiers[m] = v end end + else -- has to be a function that at least returns tag, value + return alias(tags,value,modifiers) end end local ttype = known_tags[tag] @@ -495,6 +595,13 @@ end local build_arg_list, split_iden -- forward declaration +function Item:split_param (line) + local name, comment = line:match('%s*([%w_%.:]+)(.*)') + if not name then + self:error("bad param name format '"..line.."'. Are you missing a parameter name?") + end + return name, comment +end function Item:finish() local tags = self.tags @@ -503,9 +610,9 @@ function Item:finish() self.type = read_del(tags,'class') self.modifiers = extract_tag_modifiers(tags) self.usage = read_del(tags,'usage') - -- see tags are multiple, but they may also be comma-separated + tags.see = read_del(tags,'see') if tags.see then - tags.see = tools.expand_comma_list(read_del(tags,'see')) + tags.see = tools.identifier_list(tags.see) end if doc.project_level(self.type) then -- we are a module, so become one! @@ -543,10 +650,7 @@ function Item:finish() local param_names, comments = List(), List() if params then for line in params:iter() do - local name, comment = line:match('%s*([%w_%.:]+)(.*)') - if not name then - self:error("bad param name format '"..line.."'. Are you missing a parameter name?") - end + local name, comment = self:split_param(line) param_names:append(name) comments:append(comment) end @@ -562,18 +666,19 @@ function Item:finish() if fargs then if #param_names == 0 then --docs may be embedded in argument comments; in either case, use formal arg names - formal = List() - if fargs.return_comment then - local retc = self:parse_argument_comment(fargs.return_comment,'return') - self.ret = List{retc} - end - for i, name in ipairs(fargs) do - formal:append(name) - comments:append(self:parse_argument_comment(fargs.comments[name],self.parameter)) - end - elseif #fargs > 0 then + local ret + formal,comments,ret = self:parse_formal_arguments(fargs) + if ret and not self.ret then self.ret = ret end + elseif #fargs > 0 then -- consistency check! local varargs = fargs[#fargs] == '...' if varargs then table.remove(fargs) end + if tags.export then + if fargs[1] == 'self' then + table.remove(fargs,1) + else + tags.static = true + end + end local k = 0 for _,pname in ipairs(param_names) do local _,field = split_iden(pname) @@ -589,14 +694,27 @@ function Item:finish() end end if k < #fargs then - for i = k+1,#fargs do - if fargs[i] ~= '...' then - self:warning("undocumented formal argument: "..quote(fargs[i])) - end + for i = k+1,#fargs do if fargs[i] ~= '...' then + self:warning("undocumented formal argument: "..quote(fargs[i])) + end end + end + end -- #fargs > 0 + -- formal arguments may come with types, inferred by the + -- appropriate code in ldoc.lang + if fargs.types then + self.modifiers[field] = List() + for t in fargs.types:iter() do + self:add_type(field,t) + end + if fargs.return_type then + if not self.ret then -- type, but no comment; no worries + self.ret = List{''} end + self.modifiers['return'] = List() + self:add_type('return',fargs.return_type) end end - end + end -- fargs -- the comments are associated with each parameter by -- adding name-value pairs to the params list (this is @@ -608,6 +726,9 @@ function Item:finish() local names = List() self.subparams = {} for i,name in ipairs(original_names) do + if type(name) ~= 'string' then + self:error("declared table cannot have array entries") + end local pname,field = split_iden(name) if field then if not fields then @@ -629,6 +750,13 @@ function Item:finish() self.params = params self.args = build_arg_list (names,pmods) end + if self.ret then + self:build_return_groups() + end +end + +function Item:add_type(field,type) + self.modifiers[field]:append {type = type} end -- ldoc allows comments in the formal arg list to be used, if they aren't specified with @param @@ -639,13 +767,26 @@ function Item:parse_argument_comment (comment,field) comment = comment:gsub('^%-+%s*','') local type,rest = comment:match '([^:]+):(.*)' if type then - self.modifiers[field]:append {type = type} + self:add_type(field,type) comment = rest end end return comment or '' end +function Item:parse_formal_arguments (fargs) + local formal, comments, ret = List(), List() + if fargs.return_comment then + local retc = self:parse_argument_comment(fargs.return_comment,'return') + ret = List{retc} + end + for i, name in ipairs(fargs) do + formal:append(name) + comments:append(self:parse_argument_comment(fargs.comments[name],self.parameter)) + end + return formal, comments, ret +end + function split_iden (name) if name == '...' then return name end local pname,field = name:match('(.-)%.(.+)') @@ -685,7 +826,7 @@ function build_arg_list (names,pmods) end opt = m.optchain or m.opt if opt then - acc(' [') + acc('[') npending=npending+1 end end @@ -697,16 +838,32 @@ function build_arg_list (names,pmods) return '('..table.concat(buffer)..')' end -function Item:type_of_param(p) +------ retrieving information about parameters ----- +-- The template leans on these guys heavily.... + +function Item:param_modifiers (p) local mods = self.modifiers[self.parameter] if not mods then return '' end - local mparam = rawget(mods,p) + return rawget(mods,p) +end + +function Item:type_of_param(p) + local mparam = self:param_modifiers(p) return mparam and mparam.type or '' end -function Item:type_of_ret(idx) - local rparam = self.modifiers['return'][idx] - return rparam and rparam.type or '' +function Item:default_of_param(p) + local m = self:param_modifiers(p) + if not m then return nil end + local opt = m.optchain or m.opt + if opt == true then return nil end + return opt +end + +function Item:readonly(p) + local m = self:param_modifiers(p) + if not m then return nil end + return m.readonly end function Item:subparam(p) @@ -727,6 +884,130 @@ function Item:display_name_of(p) end end +-------- return values and types ------- + +function Item:type_of_ret(idx) + local rparam = self.modifiers['return'][idx] + return rparam and rparam.type or '' +end + +local function integer_keys(t) + if type(t) ~= 'table' then return 0 end + for k in pairs(t) do + local num = tonumber(k) + if num then return num end + end + return 0 +end + +function Item:return_type(r) + if not r.type then return '' end + return r.type, r.ctypes +end + +local struct_return_type = '*' + +function Item:build_return_groups() + local modifiers = self.modifiers + local retmod = modifiers['return'] + local groups = List() + local lastg, group + for i,ret in ipairs(self.ret) do + local mods = retmod[i] + local g = integer_keys(mods) + if g ~= lastg then + group = List() + group.g = g + groups:append(group) + lastg = g + end + --require 'pl.pretty'.dump(ret) + if not mods then + self:error(quote(self.name)..' had no return?') + end + group:append({text=ret, type = mods and (mods.type or '') or '',mods = mods}) + end + -- order by groups to force error groups to the end + table.sort(groups,function(g1,g2) return g1.g < g2.g end) + self.retgroups = groups + --require 'pl.pretty'.dump(groups) + -- cool, now see if there are any treturns that have tfields to associate with + local fields = self.tags.field + if fields then + local fcomments = List() + for i,f in ipairs(fields) do + local name, comment = self:split_param(f) + fields[i] = name + fcomments[i] = comment + end + local fmods = modifiers.field + for group in groups:iter() do for r in group:iter() do + if r.mods and r.mods.type then + local ctypes, T = List(), r.mods.type + for i,f in ipairs(fields) do if fmods[i][T] then + ctypes:append {name=f,type=fmods[i].type,comment=fcomments[i]} + end end + r.ctypes = ctypes + --require 'pl.pretty'.dump(ctypes) + end + end end + end +end + +local ecount = 0 + +-- this alias macro implements @error. +-- Alias macros need to return the same results as Item:check_tags... +function doc.error_macro(tags,value,modifiers) + local merge_groups = doc.ldoc.merge_error_groups + local g = '2' -- our default group id + -- Were we given an explicit group modifier? + local key = integer_keys(modifiers) + if key > 0 then + g = tostring(key) + else + local l = tags:get 'return' + if l then -- there were returns already...... + -- maximum group of _existing_ error return + local grp, lastr = 0 + for r in l:iter() do if type(r) == 'table' then + local rg = r.modifiers._err + if rg then + lastr = r + grp = math.max(grp,rg) + end + end end + if grp > 0 then -- cool, create new group + if not merge_groups then + g = tostring(grp+1) + else + local mods, text, T = lastr.modifiers + local new = function(text) + return mods._collected..' '..text,{type='string',[T]=true} + end + if not mods._collected then + text = lastr[1] + lastr[1] = merge_groups + T = '@'..ecount + mods.type = T + mods._collected = 1 + ecount = ecount + 1 + tags:add('field',new(text)) + else + T = mods.type + end + mods._collected = mods._collected + 1 + return 'field',new(value) + end + end + end + end + tags:add('return','',{[g]=true,type='nil'}) + -- note that this 'return' is tagged with _err! + return 'return', value, {[g]=true,_err=tonumber(g),type='string'} +end + +---------- bothering the user -------------------- function Item:warning(msg) local file = self.file and self.file.filename @@ -743,7 +1024,6 @@ end Module.warning, Module.error = Item.warning, Item.error - -------- Resolving References ----------------- function Module:hunt_for_reference (packmod, modules) @@ -761,37 +1041,72 @@ end local err = io.stderr local function custom_see_references (s) - for pat, action in pairs(see_reference_handlers) do - if s:match(pat) then - local label, href = action(s:match(pat)) - if not label then print('custom rule failed',s,pat,href) end - return {href = href, label = label} - end - end + for pat, action in pairs(see_reference_handlers) do + if s:match(pat) then + local label, href = action(s:match(pat)) + if not label then print('custom rule failed',s,pat,href) end + return {href = href, label = label} + end + end end local function reference (s, mod_ref, item_ref) local name = item_ref and item_ref.name or '' -- this is deeply hacky; classes have 'Class ' prepended. - if item_ref and doc.class_tag(item_ref.type) then - name = 'Class_'..name - end +--~ if item_ref and doc.class_tag(item_ref.type) then +--~ name = 'Class_'..name +--~ end return {mod = mod_ref, name = name, label=s} end -function Module:process_see_reference (s,modules) +function Module:lookup_class_item (packmod, s) + local klass = packmod --"Class_"..packmod + local qs = klass..':'..s + local klass_section = self.sections.by_name[klass] + if not klass_section then return nil end -- no such class + for item in self.items:iter() do + --print('item',qs,item.name) + if s == item.name or qs == item.name then + return reference(s,self,item) + end + end + return nil +end + +function Module:process_see_reference (s,modules,istype) + if s == nil then return nil end local mod_ref,fun_ref,name,packmod local ref = custom_see_references(s) if ref then return ref end if not s:match '^[%w_%.%:%-]+$' or not s:match '[%w_]$' then return nil, "malformed see reference: '"..s..'"' end + + -- `istype` means that we are looking up strictly in a _type_ context, so then only + -- allow `classmod` module references. + local function ismod(item) + if item == nil then return false end + if not istype then return true + else + return item.type == 'classmod' + end + end + + -- it is _entirely_ possible that someone does not want auto references for standard Lua libraries! + local lua_manual_ref + local ldoc = tools.item_ldoc(self) + if ldoc and ldoc.no_lua_ref then + lua_manual_ref = function(s) return false end + else + lua_manual_ref = global.lua_manual_ref + end + -- is this a fully qualified module name? local mod_ref = modules.by_name[s] - if mod_ref then return reference(s, mod_ref,nil) end + if ismod(mod_ref) then return reference(s, mod_ref,nil) end -- module reference? mod_ref = self:hunt_for_reference(s, modules) - if mod_ref then return mod_ref end + if ismod(mod_ref) then return mod_ref end -- method reference? (These are of form CLASS.NAME) fun_ref = self.items.by_name[s] if fun_ref then return reference(s,self,fun_ref) end @@ -802,12 +1117,20 @@ function Module:process_see_reference (s,modules) if not mod_ref then mod_ref = self:hunt_for_reference(packmod, modules) if not mod_ref then - local ref = global.lua_manual_ref(s) + local ref = self:lookup_class_item(packmod,s) + if ref then return ref end + local mod, klass = split_dotted_name(packmod) + mod_ref = modules.by_name[mod] + if mod_ref then + ref = mod_ref:lookup_class_item(klass,name) + if ref then return ref end + end + ref = lua_manual_ref(s) if ref then return ref end return nil,"module not found: "..packmod end end - fun_ref = mod_ref.items.by_name[name] + fun_ref = mod_ref:get_fun_ref(name) if fun_ref then return reference(s,mod_ref,fun_ref) else @@ -820,17 +1143,33 @@ function Module:process_see_reference (s,modules) end else -- plain jane name; module in this package, function in this module mod_ref = modules.by_name[self.package..'.'..s] - if mod_ref then return reference(s, mod_ref,nil) end - fun_ref = self.items.by_name[s] - if fun_ref then return reference(s, self,fun_ref) + if ismod(mod_ref) then return reference(s, mod_ref,nil) end + fun_ref = self:get_fun_ref(s) + if fun_ref then return reference(s,self,fun_ref) else - local ref = global.lua_manual_ref (s) + local ref = lua_manual_ref (s) if ref then return ref end return nil, "function not found: "..s.." in this module" end end end +function Module:get_fun_ref(s) + local fun_ref = self.items.by_name[s] + -- did not get an exact match, so try to match by the unqualified fun name + if not fun_ref then + local patt = '[.:]'..s..'$' + for qname,ref in pairs(self.items.by_name) do + if qname:match(patt) then + fun_ref = ref + break + end + end + end + return fun_ref +end + + -- resolving @see references. A word may be either a function in this module, -- or a module in this package. A MOD.NAME reference is within this package. -- Otherwise, the full qualified name must be used. @@ -862,14 +1201,6 @@ function Module:resolve_references(modules) end end --- suppress the display of local functions and annotations. --- This is just a placeholder hack until we have a more general scheme --- for indicating 'private' content of a module. -function Module:mask_locals () - self.kinds['Local Functions'] = nil - self.kinds['Annotations'] = nil -end - function Item:dump_tags (taglist) for tag, value in pairs(self.tags) do if not taglist or taglist[tag] then @@ -897,7 +1228,7 @@ local function dump_tags (tags) end function Module:dump(verbose) - if self.type ~= 'module' then return end + if not doc.project_level(self.type) then return end print '----' print(self.type..':',self.name,self.summary) if self.description then print(self.description) end diff --git a/ldoc/html.lua b/ldoc/html.lua index f584f807..f138da81 100644 --- a/ldoc/html.lua +++ b/ldoc/html.lua @@ -18,10 +18,13 @@ local utils = require 'pl.utils' local path = require 'pl.path' local stringx = require 'pl.stringx' local template = require 'pl.template' +local tablex = require 'pl.tablex' +local OrderedMap = require 'pl.OrderedMap' local tools = require 'ldoc.tools' local markup = require 'ldoc.markup' local prettify = require 'ldoc.prettify' local doc = require 'ldoc.doc' +local unpack = utils.unpack local html = {} @@ -37,16 +40,16 @@ local function cleanup_whitespaces(text) end local function get_module_info(m) - local info = {} + local info = OrderedMap() for tag in doc.module_info_tags() do local val = m.tags[tag] if type(val)=='table' then val = table.concat(val,',') end tag = stringx.title(tag) - info[tag] = val + info:set(tag,val) end - if next(info) then + if #info:keys() > 0 then return info end end @@ -55,6 +58,30 @@ local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">" function html.generate_output(ldoc, args, project) local check_directory, check_file, writefile = tools.check_directory, tools.check_file, tools.writefile + local original_ldoc + + local function save_and_set_ldoc (set) + if not set then return end + if not original_ldoc then + original_ldoc = tablex.copy(ldoc) + end + for s in set:iter() do + local var,val = s:match('([^=]+)=(.+)') + local num = tonumber(val) + if num then val = num + elseif val == 'true' then val = true + elseif val == 'false' then val = false + end + print('setting',var,val) + ldoc[var] = val + end + end + + local function restore_ldoc () + if original_ldoc then + ldoc = original_ldoc + end + end function ldoc.escape(str) return (str:gsub("['&<>\"]", escape_table)) @@ -69,6 +96,20 @@ function html.generate_output(ldoc, args, project) return (item.summary or '?')..' '..(item.description or '') end + function ldoc.module_name (mod) + local name = mod.name + if args.unqualified and (mod.type == 'module' or mod.type == 'classmod') then -- leave out package + name = name:gsub('^.-%.','') + elseif mod.type == 'topic' then + if mod.display_name then + name = mod.display_name + else -- leave out md extension + name = name:gsub('%..*$','') + end + end + return name + end + -- this generates the internal module/function references function ldoc.href(see) if see.href then -- explict reference, e.g. to Lua manual @@ -116,18 +157,33 @@ function html.generate_output(ldoc, args, project) if #ls > 1 then return '
  • ','
  • ' else return '','' end end - function ldoc.display_name(item) + function ldoc.default_display_name(item) local name = item.display_name or item.name - if item.type == 'function' or item.type == 'lfunction' then return name..' '..item.args - else return name end + if item.type == 'function' or item.type == 'lfunction' then + if not ldoc.no_space_before_args then + name = name..' ' + end + return name..item.args + else + return name + end end - function ldoc.no_spaces(s) return (s:gsub('%A','_')) end + function ldoc.display_name(item) + if ldoc.custom_display_name_handler then + return ldoc.custom_display_name_handler(item, ldoc.default_display_name) + else + return ldoc.default_display_name(item) + end + end + + function ldoc.no_spaces(s) + s = s:gsub('%s*$','') + return (s:gsub('%W','_')) + end - function ldoc.titlecase(s) - return (s:gsub('(%a)(%a*)',function(f,r) - return f:upper()..r - end)) + function ldoc.module_typename(m) + return doc.presentation_name(m.type) end function ldoc.is_list (t) @@ -135,7 +191,7 @@ function html.generate_output(ldoc, args, project) end function ldoc.typename (tp) - if not tp or tp == '' then return '' end + if not tp or tp == '' or tp:match '^@' then return '' end local optional -- ? is short for ?nil| if tp:match("^%?") and not tp:match '|' then @@ -146,11 +202,16 @@ function html.generate_output(ldoc, args, project) optional = true tp = tp2 end + local types = {} for name in tp:gmatch("[^|]+") do - local ref,err = markup.process_reference(name) + local sym = name:match '([%w%.%:]+)' + local ref,err = markup.process_reference(sym,true) if ref then - types[#types+1] = ('%s'):format(ldoc.href(ref),ref.label or name) + if ref.label and sym == name then + name = ref.label + end + types[#types+1] = ('%s'):format(ldoc.href(ref),name) else types[#types+1] = ''..name..'' end @@ -167,6 +228,11 @@ function html.generate_output(ldoc, args, project) return names end + local function set_charset (ldoc,m) + m = m or ldoc.module + ldoc.doc_charset = (m and m.tags.charset) or ldoc.charset + end + local module_template,err = utils.readfile (path.join(args.template,ldoc.templ)) if not module_template then quit("template not found at '"..args.template.."' Use -l to specify directory containing ldoc.ltp") @@ -178,35 +244,44 @@ function html.generate_output(ldoc, args, project) ldoc.pairs = pairs ldoc.print = print + -- Bang out the index. -- in single mode there is one module and the 'index' is the -- documentation for that module. ldoc.module = ldoc.single if ldoc.single and args.one then ldoc.kinds_allowed = {module = true, topic = true} + ldoc.one = true end ldoc.root = true if ldoc.module then ldoc.module.info = get_module_info(ldoc.module) + ldoc.module.ldoc = ldoc + save_and_set_ldoc(ldoc.module.tags.set) end + set_charset(ldoc) local out,err = template.substitute(module_template,{ ldoc = ldoc, module = ldoc.module, + _escape = ldoc.template_escape }) ldoc.root = false if not out then quit("template failed: "..err) end + restore_ldoc() check_directory(args.dir) -- make sure output directory is ok args.dir = args.dir .. path.sep - check_file(args.dir..css, path.join(args.style,css)) -- has CSS been copied? + if css then -- has CSS been copied? + check_file(args.dir..css, path.join(args.style,css)) + end -- write out the module index out = cleanup_whitespaces(out) writefile(args.dir..args.output..args.ext,out) -- in single mode, we exclude any modules since the module has been done; - -- this step is then only for putting out any examples or topics + -- ext step is then only for putting out any examples or topics local mods = List() for kind, modules in project() do local lkind = kind:lower() @@ -218,7 +293,9 @@ function html.generate_output(ldoc, args, project) -- write out the per-module documentation -- note that we reset the internal ordering of the 'kinds' so that -- e.g. when reading a topic the other Topics will be listed first. - ldoc.css = '../'..css + if css then + ldoc.css = '../'..css + end for m in mods:iter() do local kind, lkind, modules = unpack(m) check_directory(args.dir..lkind) @@ -226,13 +303,19 @@ function html.generate_output(ldoc, args, project) for m in modules() do ldoc.module = m ldoc.body = m.body + m.ldoc = ldoc + if m.tags.set then + save_and_set_ldoc(m.tags.set) + end + set_charset(ldoc) m.info = get_module_info(m) if ldoc.body and m.postprocess then ldoc.body = m.postprocess(ldoc.body) end out,err = template.substitute(module_template,{ module=m, - ldoc = ldoc + ldoc = ldoc, + _escape = ldoc.template_escape }) if not out then quit('template failed for '..m.name..': '..err) @@ -240,6 +323,7 @@ function html.generate_output(ldoc, args, project) out = cleanup_whitespaces(out) writefile(args.dir..lkind..'/'..m.name..args.ext,out) end + restore_ldoc() end end if not args.quiet then print('output written to '..tools.abspath(args.dir)) end diff --git a/ldoc/html/ldoc_css.lua b/ldoc/html/ldoc_css.lua index 44700453..1724909a 100644 --- a/ldoc/html/ldoc_css.lua +++ b/ldoc/html/ldoc_css.lua @@ -29,7 +29,7 @@ del,ins { text-decoration: none; } li { - list-style: bullet; + list-style: disc; margin-left: 20px; } caption,th { @@ -253,7 +253,7 @@ table.module_list td { border-style: solid; border-color: #cccccc; } -table.module_list td.name { background-color: #f0f0f0; ; min-width: 200px; } +table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } table.module_list td.summary { width: 100%; } @@ -269,9 +269,14 @@ table.function_list td { border-style: solid; border-color: #cccccc; } -table.function_list td.name { background-color: #f0f0f0; ; min-width: 200px; } +table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } table.function_list td.summary { width: 100%; } +ul.nowrap { + overflow:auto; + white-space:nowrap; +} + dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} dl.table h3, dl.function h3 {font-size: .95em;} diff --git a/ldoc/html/ldoc_ltp.lua b/ldoc/html/ldoc_ltp.lua index 60f2d729..b7de5f6c 100644 --- a/ldoc/html/ldoc_ltp.lua +++ b/ldoc/html/ldoc_ltp.lua @@ -2,7 +2,7 @@ return [==[ - + $(ldoc.title) @@ -24,7 +24,7 @@ return [==[ # local use_li = ldoc.use_li # local display_name = ldoc.display_name # local iter = ldoc.modules.iter -# local M = ldoc.markup +# local function M(txt,item) return ldoc.markup(txt,item,ldoc.plain) end # local nowrap = ldoc.wrap and '' or 'nowrap' @@ -50,10 +50,10 @@ return [==[ # end -# if ldoc.no_summary and module then -- bang out the functions on the side +# if ldoc.no_summary and module and not ldoc.one then -- bang out the functions on the side # for kind, items in module.kinds() do

    $(kind)

    -
      +
        # for item in items() do
      • $(display_name(item))
      • # end @@ -61,21 +61,19 @@ return [==[ # end # end # -------- contents of project ---------- -# -- if not ldoc.no_summary then # local this_mod = module and module.name # for kind, mods, type in ldoc.kinds() do # if not ldoc.kinds_allowed or ldoc.kinds_allowed[type] then

        $(kind)

        -
          -# for mod in mods() do -# if mod.name == this_mod then -- highlight current module, link to others -
        • $(mod.name)
        • +
            +# for mod in mods() do local name = ldoc.module_name(mod) +# if mod.name == this_mod then +
          • $(name)
          • # else -
          • $(mod.name)
          • +
          • $(name)
          • # end # end # end -# -- end
          # end @@ -83,13 +81,10 @@ return [==[
          -#if module then -

          $(ldoc.titlecase(module.type)) $(module.name)

          -# end - # if ldoc.body then -- verbatim HTML as contents; 'non-code' entries $(ldoc.body) # elseif module then -- module documentation +

          $(ldoc.module_typename(module)) $(module.name)

          $(M(module.summary,module))

          $(M(module.description,module))

          # if module.usage then @@ -104,8 +99,8 @@ return [==[ # if module.info then

          Info:

            -# for tag, value in ldoc.pairs(module.info) do -
          • $(tag): $(value)
          • +# for tag, value in module.info:iter() do +
          • $(tag): $(M(value,module))
          • # end
          # end -- if module.info @@ -154,8 +149,26 @@ return [==[
          $(M(ldoc.descript(item),item)) +# if ldoc.custom_tags then +# for custom in iter(ldoc.custom_tags) do +# local tag = item.tags[custom[1]] +# if tag and not custom.hidden then +# local li,il = use_li(tag) +

          $(custom.title or custom[1]):

          +
            +# for value in iter(tag) do + $(li)$(custom.format and custom.format(value) or M(value))$(il) +# end -- for +# end -- if tag +
          +# end -- iter tags +# end + # if show_parms and item.params and #item.params > 0 then -

          $(module.kinds:type_of(item).subnames):

          +# local subnames = module.kinds:type_of(item).subnames +# if subnames then +

          $(subnames):

          +# end
            # for parm in iter(item.params) do # local param,sublist = item:subparam(parm) @@ -164,12 +177,19 @@ return [==[
              # end # for p in iter(param) do -# local name,tp = item:display_name_of(p), ldoc.typename(item:type_of_param(p)) +# local name,tp,def = item:display_name_of(p), ldoc.typename(item:type_of_param(p)), item:default_of_param(p)
            • $(name) # if tp ~= '' then $(tp) -# end - $(M(item.params[p],item))
            • +# end + $(M(item.params[p],item)) +# if def then + (default $(def)) +# end +# if item:readonly(p) then + readonly +# end + # end # if sublist then
            @@ -178,19 +198,31 @@ return [==[
          # end -- if params -# if show_return and item.ret then -# local li,il = use_li(item.ret) +# if show_return and item.retgroups then local groups = item.retgroups

          Returns:

          +# for i,group in ldoc.ipairs(groups) do local li,il = use_li(group)
            -# for i,r in ldoc.ipairs(item.ret) do +# for r in group:iter() do local type, ctypes = item:return_type(r); local rt = ldoc.typename(type) $(li) -# local tp = ldoc.typename(item:type_of_ret(i)) -# if tp ~= '' then - $(tp) -# end - $(M(r,item))$(il) -# end -- for +# if rt ~= '' then + $(rt) +# end + $(M(r.text,item))$(il) +# if ctypes then +
              +# for c in ctypes:iter() do +
            • $(c.name) + $(ldoc.typename(c.type)) + $(M(c.comment,item))
            • +# end +
            +# end -- if ctypes +# end -- for r
          +# if i < #groups then +

          Or

          +# end +# end -- for group # end -- if returns # if show_return and item.raise then @@ -200,7 +232,7 @@ return [==[ # if item.see then # local li,il = use_li(item.see) -

          see also:

          +

          See also:

            # for see in iter(item.see) do $(li)$(see.label)$(il) @@ -249,7 +281,7 @@ return [==[
          -generated by LDoc 1.3 +generated by LDoc 1.4.2
          diff --git a/ldoc/html/ldoc_md_ltp.lua b/ldoc/html/ldoc_md_ltp.lua new file mode 100644 index 00000000..ca39766e --- /dev/null +++ b/ldoc/html/ldoc_md_ltp.lua @@ -0,0 +1,17 @@ +return [[ +> local lev = ldoc.level or 2 +> local lev1,lev2 = ('#'):rep(lev),('#'):rep(lev+1) +> for kind, items in module.kinds() do +> local kitem = module.kinds:get_item(kind) +> if kitem then +$(lev1) $(ldoc.descript(kitem)) + +> end +> for item in items() do +$(lev2) $(ldoc.display_name(item)) + +$(ldoc.descript(item)) + +> end +> end +]] diff --git a/ldoc/html/ldoc_pale_css.lua b/ldoc/html/ldoc_pale_css.lua new file mode 100644 index 00000000..75abb320 --- /dev/null +++ b/ldoc/html/ldoc_pale_css.lua @@ -0,0 +1,302 @@ +return [[/* BEGIN RESET + +Copyright (c) 2010, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.8.2r1 +*/ +html { + color: #000; + background: #FFF; +} +body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { + margin: 0; + padding: 0; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +fieldset,img { + border: 0; +} +address,caption,cite,code,dfn,em,strong,th,var,optgroup { + font-style: inherit; + font-weight: inherit; +} +del,ins { + text-decoration: none; +} +li { + list-style: bullet; + margin-left: 20px; +} +caption,th { + text-align: left; +} +h1,h2,h3,h4,h5,h6 { + font-size: 100%; + font-weight: bold; +} +q:before,q:after { + content: ''; +} +abbr,acronym { + border: 0; + font-variant: normal; +} +sup { + vertical-align: baseline; +} +sub { + vertical-align: baseline; +} +legend { + color: #000; +} +input,button,textarea,select,optgroup,option { + font-family: inherit; + font-size: inherit; + font-style: inherit; + font-weight: inherit; +} +input,button,textarea,select {*font-size:100%; +} +/* END RESET */ + +body { + margin-left: 1em; + margin-right: 1em; + font-family: arial, helvetica, geneva, sans-serif; + background-color: #ffffff; margin: 0px; +} + +code, tt { font-family: monospace; } +span.parameter { font-family:monospace; } +span.parameter:after { content:":"; } +span.types:before { content:"("; } +span.types:after { content:")"; } +.type { font-weight: bold; font-style:italic } + +body, p, td, th { font-size: .95em; line-height: 1.2em;} + +p, ul { margin: 10px 0 0 0px;} + +strong { font-weight: bold;} + +em { font-style: italic;} + +h1 { + font-size: 1.5em; + margin: 0 0 20px 0; +} +h2, h3, h4 { margin: 15px 0 10px 0; } +h2 { font-size: 1.25em; } +h3 { font-size: 1.15em; } +h4 { font-size: 1.06em; } + +a:link { font-weight: bold; color: #004080; text-decoration: none; } +a:visited { font-weight: bold; color: #006699; text-decoration: none; } +a:link:hover { text-decoration: underline; } + +hr { + color:#cccccc; + background: #00007f; + height: 1px; +} + +blockquote { margin-left: 3em; } + +ul { list-style-type: disc; } + +p.name { + font-family: "Andale Mono", monospace; + padding-top: 1em; +} + +pre.example { + background-color: rgb(245, 245, 245); + border: 1px solid silver; + padding: 10px; + margin: 10px 0 10px 0; + font-family: "Andale Mono", monospace; + font-size: .85em; +} + +pre { + background-color: rgb(245,245,255); // rgb(245, 245, 245); + border: 1px solid #cccccc; //silver; + padding: 10px; + margin: 10px 0 10px 0; + overflow: auto; + font-family: "Andale Mono", monospace; +} + + +table.index { border: 1px #00007f; } +table.index td { text-align: left; vertical-align: top; } + +#container { + margin-left: 1em; + margin-right: 1em; + background-color: #f0f0f0; +} + +#product { + text-align: center; + border-bottom: 1px solid #cccccc; + background-color: #ffffff; +} + +#product big { + font-size: 2em; +} + +#main { + background-color:#FFFFFF; // #f0f0f0; + //border-left: 2px solid #cccccc; +} + +#navigation { + float: left; + width: 14em; + vertical-align: top; + background-color:#FFFFFF; // #f0f0f0; + overflow: visible; +} + +#navigation h2 { + background-color:#FFFFFF;//:#e7e7e7; + font-size:1.1em; + color:#000000; + text-align: left; + padding:0.2em; + //border-top:1px solid #dddddd; + border-bottom:1px solid #dddddd; +} + +#navigation ul +{ + font-size:1em; + list-style-type: none; + margin: 1px 1px 10px 1px; +} + +#navigation li { + text-indent: -1em; + display: block; + margin: 3px 0px 0px 22px; +} + +#navigation li li a { + margin: 0px 3px 0px -1em; +} + +#content { + margin-left: 14em; + padding: 1em; + width: 700px; + border-left: 2px solid #cccccc; + // border-right: 2px solid #cccccc; + background-color: #ffffff; +} + +#about { + clear: both; + padding: 5px; + border-top: 2px solid #cccccc; + background-color: #ffffff; +} + +@media print { + body { + font: 12pt "Times New Roman", "TimeNR", Times, serif; + } + a { font-weight: bold; color: #004080; text-decoration: underline; } + + #main { + background-color: #ffffff; + border-left: 0px; + } + + #container { + margin-left: 2%; + margin-right: 2%; + background-color: #ffffff; + } + + #content { + padding: 1em; + background-color: #ffffff; + } + + #navigation { + display: none; + } + pre.example { + font-family: "Andale Mono", monospace; + font-size: 10pt; + page-break-inside: avoid; + } +} + +table.module_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.module_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.module_list td.name { background-color: #f0f0f0; ; min-width: 200px; } +table.module_list td.summary { width: 100%; } + +table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.function_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.function_list td.name { background-color: #f6f6ff; ; min-width: 200px; } +table.function_list td.summary { width: 100%; } + +dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} +dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} +dl.table h3, dl.function h3 {font-size: .95em;} + +ul.nowrap { + overflow:auto; + whitespace:nowrap; +} + +/* stop sublists from having initial vertical space */ +ul ul { margin-top: 0px; } +ol ul { margin-top: 0px; } +ol ol { margin-top: 0px; } +ul ol { margin-top: 0px; } + +/* styles for prettification of source */ +pre .comment { color: #558817; } +pre .constant { color: #a8660d; } +pre .escape { color: #844631; } +pre .keyword { color: #2239a8; font-weight: bold; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } +pre .string { color: #a8660d; } +pre .number { color: #f8660d; } +pre .operator { color: #2239a8; font-weight: bold; } +pre .preprocessor, pre .prepro { color: #a33243; } +pre .global { color: #800080; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } +]] diff --git a/ldoc/lang.lua b/ldoc/lang.lua index e02d02ae..888dc352 100644 --- a/ldoc/lang.lua +++ b/ldoc/lang.lua @@ -5,6 +5,7 @@ local class = require 'pl.class' local utils = require 'pl.utils' +local List = require 'pl.List' local tools = require 'ldoc.tools' local lexer = require 'ldoc.lexer' local quit = utils.quit @@ -73,7 +74,8 @@ function Lua:_init() self.line_comment = '^%-%-+' -- used for stripping self.start_comment_ = '^%-%-%-+' -- used for doc comment line start self.block_comment = '^%-%-%[=*%[%-+' -- used for block doc comments - self.end_comment_ = '[^%-]%-%-+\n$' ---- exclude --- this kind of comment --- + self.end_comment_ = '[^%-]%-%-+[^-]*\n$' ---- exclude --- this kind of comment --- + self.method_call = ':' self:finalize() end @@ -85,6 +87,7 @@ end function Lua:grab_block_comment(v,tok) local equals = v:match('^%-%-%[(=*)%[') + if not equals then return v end v = v:gsub(self.block_comment,'') return tools.grab_block_comment(v,tok,'%]'..equals..'%]') end @@ -239,7 +242,8 @@ end -- note a difference here: we scan C/C++ code in full-text mode, not line by line. --- This is because we can't detect multiline comments in line mode +-- This is because we can't detect multiline comments in line mode. +-- Note: this applies to C/C++ code used to generate _Lua_ documentation! local CC = class(Lang) @@ -247,6 +251,7 @@ function CC:_init() self.line_comment = '^//+' self.start_comment_ = '^///+' self.block_comment = '^/%*%*+' + self.method_call = ':' self:finalize() end @@ -258,8 +263,101 @@ function CC.lexer(f) end function CC:grab_block_comment(v,tok) - v = v:gsub(self.block_comment,'') + v = v:gsub(self.block_comment,''):gsub('\n%s*%*','\n') return 'comment',v:sub(1,-3) end -return { lua = Lua(), cc = CC() } +--- here the argument name is always last, and the type is composed of any tokens before +function CC:extract_arg (tl,idx) + idx = idx or 1 + local res = List() + for i = idx,#tl-1 do + res:append(tl[i][2]) + end + local type = res:join ' ' + return tl[#tl][2], type +end + +function CC:item_follows (t,v,tok) + if not self.extra.C then + return false + end + if t == 'iden' or t == 'keyword' then -- + if v == self.extra.export then -- this is not part of the return type! + t,v = tnext(tok) + end + -- types may have multiple tokens: example, const char *bonzo(...) + local return_type, name = v + t,v = tnext(tok) + name = v + t,v = tnext(tok) + while t ~= '(' do + return_type = return_type .. ' ' .. name + name = v + t,v = tnext(tok) + end + --print ('got',name,t,v,return_type) + return function(tags,tok) + if not tags.name then + tags:add('name',name) + end + tags:add('class','function') + if t == '(' then + tags.formal_args,t,v = tools.get_parameters(tok,')',',',self) + if return_type ~= 'void' then + tags.formal_args.return_type = return_type + end + end + end + end + return false +end + +local Moon = class(Lua) + +function Moon:_init() + self.line_comment = '^%-%-+' -- used for stripping + self.start_comment_ = '^%s*%-%-%-+' -- used for doc comment line start + self.block_comment = '^%-%-%[=*%[%-+' -- used for block doc comments + self.end_comment_ = '[^%-]%-%-+\n$' ---- exclude --- this kind of comment --- + self.method_call = '.' + self:finalize() +end + +function Moon:item_follows (t,v,tok) + if t == '.' then -- enclosed in with statement + t,v = tnext(tok) + end + if t == 'iden' then + local name,t,v = tools.get_fun_name(tok,v,'') + if name == 'class' then + name,t,v = tools.get_fun_name(tok,v,'') + -- class! + return function(tags,tok) + tags:add('class','type') + tags:add('name',name) + end + elseif t == '=' or t == ':' then -- function/method + t,v = tnext(tok) + return function(tags,tok) + if not tags.name then + tags:add('name',name) + end + if t == '(' then + tags.formal_args,t,v = tools.get_parameters(tok) + else + tags.formal_args = List() + end + tags:add('class','function') + if t == '=' then + tags.formal_args:insert(1,'self') + tags.formal_args.comments = {self=''} + end + end + else + return nil + end + end +end + +return { lua = Lua(), cc = CC(), moon = Moon() } diff --git a/ldoc/lexer.lua b/ldoc/lexer.lua index 8f6bbe09..fa50ac87 100644 --- a/ldoc/lexer.lua +++ b/ldoc/lexer.lua @@ -17,16 +17,12 @@ -- iden n -- keyword do -- --- See the Guide for further discussion
          --- @class module --- @name pl.lexer +-- +-- Based on pl.lexer from Penlight local strfind = string.find local strsub = string.sub local append = table.insert ---[[ -module ('pl.lexer',utils._module) -]] local function assert_arg(idx,val,tp) if type(val) ~= tp then @@ -70,6 +66,14 @@ local function sdump(tok,options) return "string",tok end +-- strings enclosed in back ticks +local function bdump(tok,options) + if options and options.string then + tok = tok:sub(2,-2) + end + return "backtick",tok +end + -- long Lua strings need extra work to get rid of the quotes local function sdump_l(tok,options) if options and options.string then @@ -172,6 +176,9 @@ function lexer.scan (s,matches,filter,options) if file then s = file:read() if not s then return nil end -- empty file + if s:match '^\239\187' then -- UTF-8 BOM Abomination + s = s:sub(4) + end s = s ..'\n' end local sz = #s @@ -295,6 +302,7 @@ function lexer.lua(s,filter,options) {STRING3,sdump}, {STRING1,sdump}, {STRING2,sdump}, + {'^`[^`]+`',bdump}, {'^%-%-%[(=*)%[.-%]%1%]',cdump}, {'^%-%-.-\n',cdump}, {'^%[(=*)%[.-%]%1%]',sdump_l}, @@ -363,6 +371,7 @@ function lexer.cpp(s,filter,options) {'^|=',tdump}, {'^%^=',tdump}, {'^::',tdump}, + {'^%.%.%.',tdump}, {'^.',tdump} } end diff --git a/ldoc/markdown.lua b/ldoc/markdown.lua index 5181dd8f..96d9b31a 100644 --- a/ldoc/markdown.lua +++ b/ldoc/markdown.lua @@ -1,1359 +1,1365 @@ -#!/usr/bin/env lua - ---[[ -# markdown.lua -- version 0.32 - - - -**Author:** Niklas Frykholm, -**Date:** 31 May 2008 - -This is an implementation of the popular text markup language Markdown in pure Lua. -Markdown can convert documents written in a simple and easy to read text format -to well-formatted HTML. For a more thourough description of Markdown and the Markdown -syntax, see . - -The original Markdown source is written in Perl and makes heavy use of advanced -regular expression techniques (such as negative look-ahead, etc) which are not available -in Lua's simple regex engine. Therefore this Lua port has been rewritten from the ground -up. It is probably not completely bug free. If you notice any bugs, please report them to -me. A unit test that exposes the error is helpful. - -## Usage - - require "markdown" - markdown(source) - -``markdown.lua`` exposes a single global function named ``markdown(s)`` which applies the -Markdown transformation to the specified string. - -``markdown.lua`` can also be used directly from the command line: - - lua markdown.lua test.md - -Creates a file ``test.html`` with the converted content of ``test.md``. Run: - - lua markdown.lua -h - -For a description of the command-line options. - -``markdown.lua`` uses the same license as Lua, the MIT license. - -## License - -Copyright © 2008 Niklas Frykholm. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this -software and associated documentation files (the "Software"), to deal in the Software -without restriction, including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons -to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies -or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -## Version history - -- **0.32** -- 31 May 2008 - - Fix for links containing brackets -- **0.31** -- 1 Mar 2008 - - Fix for link definitions followed by spaces -- **0.30** -- 25 Feb 2008 - - Consistent behavior with Markdown when the same link reference is reused -- **0.29** -- 24 Feb 2008 - - Fix for
           blocks with spaces in them
          --	**0.28** -- 18 Feb 2008
          -	-	Fix for link encoding
          --	**0.27** -- 14 Feb 2008
          -	-	Fix for link database links with ()
          --	**0.26** -- 06 Feb 2008
          -	-	Fix for nested italic and bold markers
          --	**0.25** -- 24 Jan 2008
          -	-	Fix for encoding of naked <
          --	**0.24** -- 21 Jan 2008
          -	-	Fix for link behavior.
          --	**0.23** -- 10 Jan 2008
          -	-	Fix for a regression bug in longer expressions in italic or bold.
          --	**0.22** -- 27 Dec 2007
          -	-	Fix for crash when processing blocks with a percent sign in them.
          --	**0.21** -- 27 Dec 2007
          -	- 	Fix for combined strong and emphasis tags
          --	**0.20** -- 13 Oct 2007
          -	-	Fix for < as well in image titles, now matches Dingus behavior
          --	**0.19** -- 28 Sep 2007
          -	-	Fix for quotation marks " and ampersands & in link and image titles.
          --	**0.18** -- 28 Jul 2007
          -	-	Does not crash on unmatched tags (behaves like standard markdown)
          --	**0.17** -- 12 Apr 2007
          -	-	Fix for links with %20 in them.
          --	**0.16** -- 12 Apr 2007
          -	-	Do not require arg global to exist.
          --	**0.15** -- 28 Aug 2006
          -	-	Better handling of links with underscores in them.
          --	**0.14** -- 22 Aug 2006
          -	-	Bug for *`foo()`*
          --	**0.13** -- 12 Aug 2006
          -	-	Added -l option for including stylesheet inline in document.
          -	-	Fixed bug in -s flag.
          -	-	Fixed emphasis bug.
          --	**0.12** -- 15 May 2006
          -	-	Fixed several bugs to comply with MarkdownTest 1.0 
          --	**0.11** -- 12 May 2006
          -	-	Fixed bug for escaping `*` and `_` inside code spans.
          -	-	Added license terms.
          -	-	Changed join() to table.concat().
          --	**0.10** -- 3 May 2006
          -	-	Initial public release.
          -
          -// Niklas
          -]]
          -
          -
          --- Set up a table for holding local functions to avoid polluting the global namespace
          -local M = {}
          -local MT = {__index = _G}
          -setmetatable(M, MT)
          -setfenv(1, M)
          -
          -----------------------------------------------------------------------
          --- Utility functions
          -----------------------------------------------------------------------
          -
          --- Locks table t from changes, writes an error if someone attempts to change the table.
          --- This is useful for detecting variables that have "accidently" been made global. Something
          --- I tend to do all too much.
          -function lock(t)
          -	function lock_new_index(t, k, v)
          -		error("module has been locked -- " .. k .. " must be declared local", 2)
          -	end
          -
          -	local mt = {__newindex = lock_new_index}
          -	if getmetatable(t) then mt.__index = getmetatable(t).__index end
          -	setmetatable(t, mt)
          -end
          -
          --- Returns the result of mapping the values in table t through the function f
          -function map(t, f)
          -	local out = {}
          -	for k,v in pairs(t) do out[k] = f(v,k) end
          -	return out
          -end
          -
          --- The identity function, useful as a placeholder.
          -function identity(text) return text end
          -
          --- Functional style if statement. (NOTE: no short circuit evaluation)
          -function iff(t, a, b) if t then return a else return b end end
          -
          --- Splits the text into an array of separate lines.
          -function split(text, sep)
          -	sep = sep or "\n"
          -	local lines = {}
          -	local pos = 1
          -	while true do
          -		local b,e = text:find(sep, pos)
          -		if not b then table.insert(lines, text:sub(pos)) break end
          -		table.insert(lines, text:sub(pos, b-1))
          -		pos = e + 1
          -	end
          -	return lines
          -end
          -
          --- Converts tabs to spaces
          -function detab(text)
          -	local tab_width = 4
          -	local function rep(match)
          -		local spaces = -match:len()
          -		while spaces<1 do spaces = spaces + tab_width end
          -		return match .. string.rep(" ", spaces)
          -	end
          -	text = text:gsub("([^\n]-)\t", rep)
          -	return text
          -end
          -
          --- Applies string.find for every pattern in the list and returns the first match
          -function find_first(s, patterns, index)
          -	local res = {}
          -	for _,p in ipairs(patterns) do
          -		local match = {s:find(p, index)}
          -		if #match>0 and (#res==0 or match[1] < res[1]) then res = match end
          -	end
          -	return unpack(res)
          -end
          -
          --- If a replacement array is specified, the range [start, stop] in the array is replaced
          --- with the replacement array and the resulting array is returned. Without a replacement
          --- array the section of the array between start and stop is returned.
          -function splice(array, start, stop, replacement)
          -	if replacement then
          -		local n = stop - start + 1
          -		while n > 0 do
          -			table.remove(array, start)
          -			n = n - 1
          -		end
          -		for i,v in ipairs(replacement) do
          -			table.insert(array, start, v)
          -		end
          -		return array
          -	else
          -		local res = {}
          -		for i = start,stop do
          -			table.insert(res, array[i])
          -		end
          -		return res
          -	end
          -end
          -
          --- Outdents the text one step.
          -function outdent(text)
          -	text = "\n" .. text
          -	text = text:gsub("\n  ? ? ?", "\n")
          -	text = text:sub(2)
          -	return text
          -end
          -
          --- Indents the text one step.
          -function indent(text)
          -	text = text:gsub("\n", "\n    ")
          -	return text
          -end
          -
          --- Does a simple tokenization of html data. Returns the data as a list of tokens. 
          --- Each token is a table with a type field (which is either "tag" or "text") and
          --- a text field (which contains the original token data).
          -function tokenize_html(html)
          -	local tokens = {}
          -	local pos = 1
          -	while true do
          -		local start = find_first(html, {"", start)
          -		elseif html:match("^<%?", start) then
          -			_,stop = html:find("?>", start)
          -		else
          -			_,stop = html:find("%b<>", start)
          -		end
          -		if not stop then
          -			-- error("Could not match html tag " .. html:sub(start,start+30)) 
          -		 	table.insert(tokens, {type="text", text=html:sub(start, start)})
          -			pos = start + 1
          -		else
          -			table.insert(tokens, {type="tag", text=html:sub(start, stop)})
          -			pos = stop + 1
          -		end
          -	end
          -	return tokens
          -end
          -
          -----------------------------------------------------------------------
          --- Hash
          -----------------------------------------------------------------------
          -
          --- This is used to "hash" data into alphanumeric strings that are unique
          --- in the document. (Note that this is not cryptographic hash, the hash
          --- function is not one-way.) The hash procedure is used to protect parts
          --- of the document from further processing.
          -
          -local HASH = {
          -	-- Has the hash been inited.
          -	inited = false,
          -	
          -	-- The unique string prepended to all hash values. This is to ensure
          -	-- that hash values do not accidently coincide with an actual existing
          -	-- string in the document.
          -	identifier = "",
          -	
          -	-- Counter that counts up for each new hash instance.
          -	counter = 0,
          -	
          -	-- Hash table.
          -	table = {}
          -}
          -
          --- Inits hashing. Creates a hash_identifier that doesn't occur anywhere
          --- in the text.
          -function init_hash(text)
          -	HASH.inited = true
          -	HASH.identifier = ""
          -	HASH.counter = 0
          -	HASH.table = {}
          -	
          -	local s = "HASH"
          -	local counter = 0
          -	local id
          -	while true do
          -		id  = s .. counter
          -		if not text:find(id, 1, true) then break end
          -		counter = counter + 1
          -	end
          -	HASH.identifier = id
          -end
          -
          --- Returns the hashed value for s.
          -function hash(s)
          -	assert(HASH.inited)
          -	if not HASH.table[s] then
          -		HASH.counter = HASH.counter + 1
          -		local id = HASH.identifier .. HASH.counter .. "X"
          -		HASH.table[s] = id
          -	end
          -	return HASH.table[s]
          -end
          -
          -----------------------------------------------------------------------
          --- Protection
          -----------------------------------------------------------------------
          -
          --- The protection module is used to "protect" parts of a document
          --- so that they are not modified by subsequent processing steps. 
          --- Protected parts are saved in a table for later unprotection
          -
          --- Protection data
          -local PD = {
          -	-- Saved blocks that have been converted
          -	blocks = {},
          -
          -	-- Block level tags that will be protected
          -	tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote",
          -	"pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset",
          -	"iframe", "math", "ins", "del"}
          -}
          -
          --- Pattern for matching a block tag that begins and ends in the leftmost
          --- column and may contain indented subtags, i.e.
          --- 
          --- A nested block. ---
          --- Nested data. ---
          ---
          -function block_pattern(tag) - return "\n<" .. tag .. ".-\n[ \t]*\n" -end - --- Pattern for matching a block tag that begins and ends with a newline -function line_pattern(tag) - return "\n<" .. tag .. ".-[ \t]*\n" -end - --- Protects the range of characters from start to stop in the text and --- returns the protected string. -function protect_range(text, start, stop) - local s = text:sub(start, stop) - local h = hash(s) - PD.blocks[h] = s - text = text:sub(1,start) .. h .. text:sub(stop) - return text -end - --- Protect every part of the text that matches any of the patterns. The first --- matching pattern is protected first, etc. -function protect_matches(text, patterns) - while true do - local start, stop = find_first(text, patterns) - if not start then break end - text = protect_range(text, start, stop) - end - return text -end - --- Protects blocklevel tags in the specified text -function protect(text) - -- First protect potentially nested block tags - text = protect_matches(text, map(PD.tags, block_pattern)) - -- Then protect block tags at the line level. - text = protect_matches(text, map(PD.tags, line_pattern)) - -- Protect
          and comment tags - text = protect_matches(text, {"\n]->[ \t]*\n"}) - text = protect_matches(text, {"\n[ \t]*\n"}) - return text -end - --- Returns true if the string s is a hash resulting from protection -function is_protected(s) - return PD.blocks[s] -end - --- Unprotects the specified text by expanding all the nonces -function unprotect(text) - for k,v in pairs(PD.blocks) do - v = v:gsub("%%", "%%%%") - text = text:gsub(k, v) - end - return text -end - - ----------------------------------------------------------------------- --- Block transform ----------------------------------------------------------------------- - --- The block transform functions transform the text on the block level. --- They work with the text as an array of lines rather than as individual --- characters. - --- Returns true if the line is a ruler of (char) characters. --- The line must contain at least three char characters and contain only spaces and --- char characters. -function is_ruler_of(line, char) - if not line:match("^[ %" .. char .. "]*$") then return false end - if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end - return true -end - --- Identifies the block level formatting present in the line -function classify(line) - local info = {line = line, text = line} - - if line:match("^ ") then - info.type = "indented" - info.outdented = line:sub(5) - return info - end - - for _,c in ipairs({'*', '-', '_', '='}) do - if is_ruler_of(line, c) then - info.type = "ruler" - info.ruler_char = c - return info - end - end - - if line == "" then - info.type = "blank" - return info - end - - if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then - local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") - info.type = "header" - info.level = m1:len() - info.text = m2 - return info - end - - if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then - local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") - info.type = "list_item" - info.list_type = "numeric" - info.number = 0 + number - info.text = text - return info - end - - if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then - local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") - info.type = "list_item" - info.list_type = "bullet" - info.bullet = bullet - info.text= text - return info - end - - if line:match("^>[ \t]?(.*)") then - info.type = "blockquote" - info.text = line:match("^>[ \t]?(.*)") - return info - end - - if is_protected(line) then - info.type = "raw" - info.html = unprotect(line) - return info - end - - info.type = "normal" - return info -end - --- Find headers constisting of a normal line followed by a ruler and converts them to --- header entries. -function headers(array) - local i = 1 - while i <= #array - 1 do - if array[i].type == "normal" and array[i+1].type == "ruler" and - (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then - local info = {line = array[i].line} - info.text = info.line - info.type = "header" - info.level = iff(array[i+1].ruler_char == "=", 1, 2) - table.remove(array, i+1) - array[i] = info - end - i = i + 1 - end - return array -end - --- Find list blocks and convert them to protected data blocks -function lists(array, sublist) - local function process_list(arr) - local function any_blanks(arr) - for i = 1, #arr do - if arr[i].type == "blank" then return true end - end - return false - end - - local function split_list_items(arr) - local acc = {arr[1]} - local res = {} - for i=2,#arr do - if arr[i].type == "list_item" then - table.insert(res, acc) - acc = {arr[i]} - else - table.insert(acc, arr[i]) - end - end - table.insert(res, acc) - return res - end - - local function process_list_item(lines, block) - while lines[#lines].type == "blank" do - table.remove(lines) - end - - local itemtext = lines[1].text - for i=2,#lines do - itemtext = itemtext .. "\n" .. outdent(lines[i].line) - end - if block then - itemtext = block_transform(itemtext, true) - if not itemtext:find("
          ") then itemtext = indent(itemtext) end
          -				return "    
        • " .. itemtext .. "
        • " - else - local lines = split(itemtext) - lines = map(lines, classify) - lines = lists(lines, true) - lines = blocks_to_html(lines, true) - itemtext = table.concat(lines, "\n") - if not itemtext:find("
          ") then itemtext = indent(itemtext) end
          -				return "    
        • " .. itemtext .. "
        • " - end - end - - local block_list = any_blanks(arr) - local items = split_list_items(arr) - local out = "" - for _, item in ipairs(items) do - out = out .. process_list_item(item, block_list) .. "\n" - end - if arr[1].list_type == "numeric" then - return "
            \n" .. out .. "
          " - else - return "
            \n" .. out .. "
          " - end - end - - -- Finds the range of lines composing the first list in the array. A list - -- starts with (^ list_item) or (blank list_item) and ends with - -- (blank* $) or (blank normal). - -- - -- A sublist can start with just (list_item) does not need a blank... - local function find_list(array, sublist) - local function find_list_start(array, sublist) - if array[1].type == "list_item" then return 1 end - if sublist then - for i = 1,#array do - if array[i].type == "list_item" then return i end - end - else - for i = 1, #array-1 do - if array[i].type == "blank" and array[i+1].type == "list_item" then - return i+1 - end - end - end - return nil - end - local function find_list_end(array, start) - local pos = #array - for i = start, #array-1 do - if array[i].type == "blank" and array[i+1].type ~= "list_item" - and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then - pos = i-1 - break - end - end - while pos > start and array[pos].type == "blank" do - pos = pos - 1 - end - return pos - end - - local start = find_list_start(array, sublist) - if not start then return nil end - return start, find_list_end(array, start) - end - - while true do - local start, stop = find_list(array, sublist) - if not start then break end - local text = process_list(splice(array, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - array = splice(array, start, stop, {info}) - end - - -- Convert any remaining list items to normal - for _,line in ipairs(array) do - if line.type == "list_item" then line.type = "normal" end - end - - return array -end - --- Find and convert blockquote markers. -function blockquotes(lines) - local function find_blockquote(lines) - local start - for i,line in ipairs(lines) do - if line.type == "blockquote" then - start = i - break - end - end - if not start then return nil end - - local stop = #lines - for i = start+1, #lines do - if lines[i].type == "blank" or lines[i].type == "blockquote" then - elseif lines[i].type == "normal" then - if lines[i-1].type == "blank" then stop = i-1 break end - else - stop = i-1 break - end - end - while lines[stop].type == "blank" do stop = stop - 1 end - return start, stop - end - - local function process_blockquote(lines) - local raw = lines[1].text - for i = 2,#lines do - raw = raw .. "\n" .. lines[i].text - end - local bt = block_transform(raw) - if not bt:find("
          ") then bt = indent(bt) end
          -		return "
          \n " .. bt .. - "\n
          " - end - - while true do - local start, stop = find_blockquote(lines) - if not start then break end - local text = process_blockquote(splice(lines, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - lines = splice(lines, start, stop, {info}) - end - return lines -end - --- Find and convert codeblocks. -function codeblocks(lines) - local function find_codeblock(lines) - local start - for i,line in ipairs(lines) do - if line.type == "indented" then start = i break end - end - if not start then return nil end - - local stop = #lines - for i = start+1, #lines do - if lines[i].type ~= "indented" and lines[i].type ~= "blank" then - stop = i-1 - break - end - end - while lines[stop].type == "blank" do stop = stop - 1 end - return start, stop - end - - local function process_codeblock(lines) - local raw = detab(encode_code(outdent(lines[1].line))) - for i = 2,#lines do - raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) - end - return "
          " .. raw .. "\n
          " - end - - while true do - local start, stop = find_codeblock(lines) - if not start then break end - local text = process_codeblock(splice(lines, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - lines = splice(lines, start, stop, {info}) - end - return lines -end - --- Convert lines to html code -function blocks_to_html(lines, no_paragraphs) - local out = {} - local i = 1 - while i <= #lines do - local line = lines[i] - if line.type == "ruler" then - table.insert(out, "
          ") - elseif line.type == "raw" then - table.insert(out, line.html) - elseif line.type == "normal" then - local s = line.line - - while i+1 <= #lines and lines[i+1].type == "normal" do - i = i + 1 - s = s .. "\n" .. lines[i].line - end - - if no_paragraphs then - table.insert(out, span_transform(s)) - else - table.insert(out, "

          " .. span_transform(s) .. "

          ") - end - elseif line.type == "header" then - local s = "" .. span_transform(line.text) .. "" - table.insert(out, s) - else - table.insert(out, line.line) - end - i = i + 1 - end - return out -end - --- Perform all the block level transforms -function block_transform(text, sublist) - local lines = split(text) - lines = map(lines, classify) - lines = headers(lines) - lines = lists(lines, sublist) - lines = codeblocks(lines) - lines = blockquotes(lines) - lines = blocks_to_html(lines) - local text = table.concat(lines, "\n") - return text -end - --- Debug function for printing a line array to see the result --- of partial transforms. -function print_lines(lines) - for i, line in ipairs(lines) do - print(i, line.type, line.text or line.line) - end -end - ----------------------------------------------------------------------- --- Span transform ----------------------------------------------------------------------- - --- Functions for transforming the text at the span level. - --- These characters may need to be escaped because they have a special --- meaning in markdown. -escape_chars = "'\\`*_{}[]()>#+-.!'" -escape_table = {} - -function init_escape_table() - escape_table = {} - for i = 1,#escape_chars do - local c = escape_chars:sub(i,i) - escape_table[c] = hash(c) - end -end - --- Adds a new escape to the escape table. -function add_escape(text) - if not escape_table[text] then - escape_table[text] = hash(text) - end - return escape_table[text] -end - --- Escape characters that should not be disturbed by markdown. -function escape_special_chars(text) - local tokens = tokenize_html(text) - - local out = "" - for _, token in ipairs(tokens) do - local t = token.text - if token.type == "tag" then - -- In tags, encode * and _ so they don't conflict with their use in markdown. - t = t:gsub("%*", escape_table["*"]) - t = t:gsub("%_", escape_table["_"]) - else - t = encode_backslash_escapes(t) - end - out = out .. t - end - return out -end - --- Encode backspace-escaped characters in the markdown source. -function encode_backslash_escapes(t) - for i=1,escape_chars:len() do - local c = escape_chars:sub(i,i) - t = t:gsub("\\%" .. c, escape_table[c]) - end - return t -end - --- Unescape characters that have been encoded. -function unescape_special_chars(t) - local tin = t - for k,v in pairs(escape_table) do - k = k:gsub("%%", "%%%%") - t = t:gsub(v,k) - end - if t ~= tin then t = unescape_special_chars(t) end - return t -end - --- Encode/escape certain characters inside Markdown code runs. --- The point is that in code, these characters are literals, --- and lose their special Markdown meanings. -function encode_code(s) - s = s:gsub("%&", "&") - s = s:gsub("<", "<") - s = s:gsub(">", ">") - for k,v in pairs(escape_table) do - s = s:gsub("%"..k, v) - end - return s -end - --- Handle backtick blocks. -function code_spans(s) - s = s:gsub("\\\\", escape_table["\\"]) - s = s:gsub("\\`", escape_table["`"]) - - local pos = 1 - while true do - local start, stop = s:find("`+", pos) - if not start then return s end - local count = stop - start + 1 - -- Find a matching numbert of backticks - local estart, estop = s:find(string.rep("`", count), stop+1) - local brstart = s:find("\n", stop+1) - if estart and (not brstart or estart < brstart) then - local code = s:sub(stop+1, estart-1) - code = code:gsub("^[ \t]+", "") - code = code:gsub("[ \t]+$", "") - code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) - code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) - code = "" .. encode_code(code) .. "" - code = add_escape(code) - s = s:sub(1, start-1) .. code .. s:sub(estop+1) - pos = start + code:len() - else - pos = stop + 1 - end - end - return s -end - --- Encode alt text... enodes &, and ". -function encode_alt(s) - if not s then return s end - s = s:gsub('&', '&') - s = s:gsub('"', '"') - s = s:gsub('<', '<') - return s -end - --- Handle image references -function images(text) - local function reference_link(alt, id) - alt = encode_alt(alt:match("%b[]"):sub(2,-2)) - id = id:match("%[(.*)%]"):lower() - if id == "" then id = text:lower() end - link_database[id] = link_database[id] or {} - if not link_database[id].url then return nil end - local url = link_database[id].url or id - url = encode_alt(url) - local title = encode_alt(link_database[id].title) - if title then title = " title=\"" .. title .. "\"" else title = "" end - return add_escape ('' .. alt .. '") - end - - local function inline_link(alt, link) - alt = encode_alt(alt:match("%b[]"):sub(2,-2)) - local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") - url = url or link:match("%(?%)") - url = encode_alt(url) - title = encode_alt(title) - if title then - return add_escape('' .. alt .. '') - else - return add_escape('' .. alt .. '') - end - end - - text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) - text = text:gsub("!(%b[])(%b())", inline_link) - return text -end - --- Handle anchor references -function anchors(text) - local function reference_link(text, id) - text = text:match("%b[]"):sub(2,-2) - id = id:match("%b[]"):sub(2,-2):lower() - if id == "" then id = text:lower() end - link_database[id] = link_database[id] or {} - if not link_database[id].url then return nil end - local url = link_database[id].url or id - url = encode_alt(url) - local title = encode_alt(link_database[id].title) - if title then title = " title=\"" .. title .. "\"" else title = "" end - return add_escape("") .. text .. add_escape("") - end - - local function inline_link(text, link) - text = text:match("%b[]"):sub(2,-2) - local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") - title = encode_alt(title) - url = url or link:match("%(?%)") or "" - url = encode_alt(url) - if title then - return add_escape("") .. text .. "" - else - return add_escape("") .. text .. add_escape("") - end - end - - text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) - text = text:gsub("(%b[])(%b())", inline_link) - return text -end - --- Handle auto links, i.e. . -function auto_links(text) - local function link(s) - return add_escape("") .. s .. "" - end - -- Encode chars as a mix of dec and hex entitites to (perhaps) fool - -- spambots. - local function encode_email_address(s) - -- Use a deterministic encoding to make unit testing possible. - -- Code 45% hex, 45% dec, 10% plain. - local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} - local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} - local plain = {code = function(c) return c end, count = 0, rate = 0.1} - local codes = {hex, dec, plain} - local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end - - local out = "" - for i = 1,s:len() do - for _,code in ipairs(codes) do code.count = code.count + code.rate end - if codes[1].count < codes[2].count then swap(codes,1,2) end - if codes[2].count < codes[3].count then swap(codes,2,3) end - if codes[1].count < codes[2].count then swap(codes,1,2) end - - local code = codes[1] - local c = s:sub(i,i) - -- Force encoding of "@" to make email address more invisible. - if c == "@" and code == plain then code = codes[2] end - out = out .. code.code(c) - code.count = code.count - 1 - end - return out - end - local function mail(s) - s = unescape_special_chars(s) - local address = encode_email_address("mailto:" .. s) - local text = encode_email_address(s) - return add_escape("") .. text .. "" - end - -- links - text = text:gsub("<(https?:[^'\">%s]+)>", link) - text = text:gsub("<(ftp:[^'\">%s]+)>", link) - - -- mail - text = text:gsub("%s]+)>", mail) - text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) - return text -end - --- Encode free standing amps (&) and angles (<)... note that this does not --- encode free >. -function amps_and_angles(s) - -- encode amps not part of &..; expression - local pos = 1 - while true do - local amp = s:find("&", pos) - if not amp then break end - local semi = s:find(";", amp+1) - local stop = s:find("[ \t\n&]", amp+1) - if not semi or (stop and stop < semi) or (semi - amp) > 15 then - s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) - pos = amp+1 - else - pos = amp+1 - end - end - - -- encode naked <'s - s = s:gsub("<([^a-zA-Z/?$!])", "<%1") - s = s:gsub("<$", "<") - - -- what about >, nothing done in the original markdown source to handle them - return s -end - --- Handles emphasis markers (* and _) in the text. -function emphasis(text) - for _, s in ipairs {"%*%*", "%_%_"} do - text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "%1") - text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "%1") - end - for _, s in ipairs {"%*", "%_"} do - text = text:gsub(s .. "([^%s_])" .. s, "%1") - text = text:gsub(s .. "([^%s_])" .. s, "%1") - text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "%1") - text = text:gsub(s .. "([^<>_]-[^<>_]-[^<>_]-)" .. s, "%1") - end - return text -end - --- Handles line break markers in the text. -function line_breaks(text) - return text:gsub(" +\n", "
          \n") -end - --- Perform all span level transforms. -function span_transform(text) - text = code_spans(text) - text = escape_special_chars(text) - text = images(text) - text = anchors(text) - text = auto_links(text) - text = amps_and_angles(text) - text = emphasis(text) - text = line_breaks(text) - return text -end - ----------------------------------------------------------------------- --- Markdown ----------------------------------------------------------------------- - --- Cleanup the text by normalizing some possible variations to make further --- processing easier. -function cleanup(text) - -- Standardize line endings - text = text:gsub("\r\n", "\n") -- DOS to UNIX - text = text:gsub("\r", "\n") -- Mac to UNIX - - -- Convert all tabs to spaces - text = detab(text) - - -- Strip lines with only spaces and tabs - while true do - local subs - text, subs = text:gsub("\n[ \t]+\n", "\n\n") - if subs == 0 then break end - end - - return "\n" .. text .. "\n" -end - --- Strips link definitions from the text and stores the data in a lookup table. -function strip_link_definitions(text) - local linkdb = {} - - local function link_def(id, url, title) - id = id:match("%[(.+)%]"):lower() - linkdb[id] = linkdb[id] or {} - linkdb[id].url = url or linkdb[id].url - linkdb[id].title = title or linkdb[id].title - return "" - end - - local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*]+)>?[ \t]*" - local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" - local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" - local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" - - text = text:gsub(def_title1, link_def) - text = text:gsub(def_title2, link_def) - text = text:gsub(def_title3, link_def) - text = text:gsub(def_no_title, link_def) - return text, linkdb -end - -link_database = {} - --- Main markdown processing function -function markdown(text) - init_hash(text) - init_escape_table() - - text = cleanup(text) - text = protect(text) - text, link_database = strip_link_definitions(text) - text = block_transform(text) - text = unescape_special_chars(text) - return text -end - ----------------------------------------------------------------------- --- End of module ----------------------------------------------------------------------- - -setfenv(1, _G) -M.lock(M) - --- Expose markdown function to the world -markdown = M.markdown - --- Class for parsing command-line options -local OptionParser = {} -OptionParser.__index = OptionParser - --- Creates a new option parser -function OptionParser:new() - local o = {short = {}, long = {}} - setmetatable(o, self) - return o -end - --- Calls f() whenever a flag with specified short and long name is encountered -function OptionParser:flag(short, long, f) - local info = {type = "flag", f = f} - if short then self.short[short] = info end - if long then self.long[long] = info end -end - --- Calls f(param) whenever a parameter flag with specified short and long name is encountered -function OptionParser:param(short, long, f) - local info = {type = "param", f = f} - if short then self.short[short] = info end - if long then self.long[long] = info end -end - --- Calls f(v) for each non-flag argument -function OptionParser:arg(f) - self.arg = f -end - --- Runs the option parser for the specified set of arguments. Returns true if all arguments --- where successfully parsed and false otherwise. -function OptionParser:run(args) - local pos = 1 - while pos <= #args do - local arg = args[pos] - if arg == "--" then - for i=pos+1,#args do - if self.arg then self.arg(args[i]) end - return true - end - end - if arg:match("^%-%-") then - local info = self.long[arg:sub(3)] - if not info then print("Unknown flag: " .. arg) return false end - if info.type == "flag" then - info.f() - pos = pos + 1 - else - param = args[pos+1] - if not param then print("No parameter for flag: " .. arg) return false end - info.f(param) - pos = pos+2 - end - elseif arg:match("^%-") then - for i=2,arg:len() do - local c = arg:sub(i,i) - local info = self.short[c] - if not info then print("Unknown flag: -" .. c) return false end - if info.type == "flag" then - info.f() - else - if i == arg:len() then - param = args[pos+1] - if not param then print("No parameter for flag: -" .. c) return false end - info.f(param) - pos = pos + 1 - else - param = arg:sub(i+1) - info.f(param) - end - break - end - end - pos = pos + 1 - else - if self.arg then self.arg(arg) end - pos = pos + 1 - end - end - return true -end - --- Handles the case when markdown is run from the command line -local function run_command_line(arg) - -- Generate output for input s given options - local function run(s, options) - s = markdown(s) - if not options.wrap_header then return s end - local header = "" - if options.header then - local f = io.open(options.header) or error("Could not open file: " .. options.header) - header = f:read("*a") - f:close() - else - header = [[ - - - - - TITLE - - - -]] - local title = options.title or s:match("

          (.-)

          ") or s:match("

          (.-)

          ") or - s:match("

          (.-)

          ") or "Untitled" - header = header:gsub("TITLE", title) - if options.inline_style then - local style = "" - local f = io.open(options.stylesheet) - if f then - style = f:read("*a") f:close() - else - error("Could not include style sheet " .. options.stylesheet .. ": File not found") - end - header = header:gsub('', - "") - else - header = header:gsub("STYLESHEET", options.stylesheet) - end - header = header:gsub("CHARSET", options.charset) - end - local footer = "" - if options.footer then - local f = io.open(options.footer) or error("Could not open file: " .. options.footer) - footer = f:read("*a") - f:close() - end - return header .. s .. footer - end - - -- Generate output path name from input path name given options. - local function outpath(path, options) - if options.append then return path .. ".html" end - local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end - m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end - return path .. ".html" - end - - -- Default commandline options - local options = { - wrap_header = true, - header = nil, - footer = nil, - charset = "utf-8", - title = nil, - stylesheet = "default.css", - inline_style = false - } - local help = [[ -Usage: markdown.lua [OPTION] [FILE] -Runs the markdown text markup to HTML converter on each file specified on the -command line. If no files are specified, runs on standard input. - -No header: - -n, --no-wrap Don't wrap the output in ... tags. -Custom header: - -e, --header FILE Use content of FILE for header. - -f, --footer FILE Use content of FILE for footer. -Generated header: - -c, --charset SET Specifies charset (default utf-8). - -i, --title TITLE Specifies title (default from first

          tag). - -s, --style STYLE Specifies style sheet file (default default.css). - -l, --inline-style Include the style sheet file inline in the header. -Generated files: - -a, --append Append .html extension (instead of replacing). -Other options: - -h, --help Print this help text. - -t, --test Run the unit tests. -]] - - local run_stdin = true - local op = OptionParser:new() - op:flag("n", "no-wrap", function () options.wrap_header = false end) - op:param("e", "header", function (x) options.header = x end) - op:param("f", "footer", function (x) options.footer = x end) - op:param("c", "charset", function (x) options.charset = x end) - op:param("i", "title", function(x) options.title = x end) - op:param("s", "style", function(x) options.stylesheet = x end) - op:flag("l", "inline-style", function(x) options.inline_style = true end) - op:flag("a", "append", function() options.append = true end) - op:flag("t", "test", function() - local n = arg[0]:gsub("markdown.lua", "markdown-tests.lua") - local f = io.open(n) - if f then - f:close() dofile(n) - else - error("Cannot find markdown-tests.lua") - end - run_stdin = false - end) - op:flag("h", "help", function() print(help) run_stdin = false end) - op:arg(function(path) - local file = io.open(path) or error("Could not open file: " .. path) - local s = file:read("*a") - file:close() - s = run(s, options) - file = io.open(outpath(path, options), "w") or error("Could not open output file: " .. outpath(path, options)) - file:write(s) - file:close() - run_stdin = false - end - ) - - if not op:run(arg) then - print(help) - run_stdin = false - end - - if run_stdin then - local s = io.read("*a") - s = run(s, options) - io.write(s) - end -end - --- If we are being run from the command-line, act accordingly -if arg and arg[0]:find("markdown%.lua$") then - run_command_line(arg) -else - return markdown -end \ No newline at end of file +#!/usr/bin/env lua + +--[[ +# markdown.lua -- version 0.32 + + + +**Author:** Niklas Frykholm, +**Date:** 31 May 2008 + +This is an implementation of the popular text markup language Markdown in pure Lua. +Markdown can convert documents written in a simple and easy to read text format +to well-formatted HTML. For a more thourough description of Markdown and the Markdown +syntax, see . + +The original Markdown source is written in Perl and makes heavy use of advanced +regular expression techniques (such as negative look-ahead, etc) which are not available +in Lua's simple regex engine. Therefore this Lua port has been rewritten from the ground +up. It is probably not completely bug free. If you notice any bugs, please report them to +me. A unit test that exposes the error is helpful. + +## Usage + + require "markdown" + markdown(source) + +``markdown.lua`` exposes a single global function named ``markdown(s)`` which applies the +Markdown transformation to the specified string. + +``markdown.lua`` can also be used directly from the command line: + + lua markdown.lua test.md + +Creates a file ``test.html`` with the converted content of ``test.md``. Run: + + lua markdown.lua -h + +For a description of the command-line options. + +``markdown.lua`` uses the same license as Lua, the MIT license. + +## License + +Copyright © 2008 Niklas Frykholm. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Version history + +- **0.32** -- 31 May 2008 + - Fix for links containing brackets +- **0.31** -- 1 Mar 2008 + - Fix for link definitions followed by spaces +- **0.30** -- 25 Feb 2008 + - Consistent behavior with Markdown when the same link reference is reused +- **0.29** -- 24 Feb 2008 + - Fix for
           blocks with spaces in them
          +-	**0.28** -- 18 Feb 2008
          +	-	Fix for link encoding
          +-	**0.27** -- 14 Feb 2008
          +	-	Fix for link database links with ()
          +-	**0.26** -- 06 Feb 2008
          +	-	Fix for nested italic and bold markers
          +-	**0.25** -- 24 Jan 2008
          +	-	Fix for encoding of naked <
          +-	**0.24** -- 21 Jan 2008
          +	-	Fix for link behavior.
          +-	**0.23** -- 10 Jan 2008
          +	-	Fix for a regression bug in longer expressions in italic or bold.
          +-	**0.22** -- 27 Dec 2007
          +	-	Fix for crash when processing blocks with a percent sign in them.
          +-	**0.21** -- 27 Dec 2007
          +	- 	Fix for combined strong and emphasis tags
          +-	**0.20** -- 13 Oct 2007
          +	-	Fix for < as well in image titles, now matches Dingus behavior
          +-	**0.19** -- 28 Sep 2007
          +	-	Fix for quotation marks " and ampersands & in link and image titles.
          +-	**0.18** -- 28 Jul 2007
          +	-	Does not crash on unmatched tags (behaves like standard markdown)
          +-	**0.17** -- 12 Apr 2007
          +	-	Fix for links with %20 in them.
          +-	**0.16** -- 12 Apr 2007
          +	-	Do not require arg global to exist.
          +-	**0.15** -- 28 Aug 2006
          +	-	Better handling of links with underscores in them.
          +-	**0.14** -- 22 Aug 2006
          +	-	Bug for *`foo()`*
          +-	**0.13** -- 12 Aug 2006
          +	-	Added -l option for including stylesheet inline in document.
          +	-	Fixed bug in -s flag.
          +	-	Fixed emphasis bug.
          +-	**0.12** -- 15 May 2006
          +	-	Fixed several bugs to comply with MarkdownTest 1.0 
          +-	**0.11** -- 12 May 2006
          +	-	Fixed bug for escaping `*` and `_` inside code spans.
          +	-	Added license terms.
          +	-	Changed join() to table.concat().
          +-	**0.10** -- 3 May 2006
          +	-	Initial public release.
          +
          +// Niklas
          +]]
          +
          +
          +-- Set up a table for holding local functions to avoid polluting the global namespace
          +local M = {}
          +local unpack = unpack or table.unpack
          +local MT = {__index = _G}
          +setmetatable(M, MT)
          +
          +----------------------------------------------------------------------
          +-- Utility functions
          +----------------------------------------------------------------------
          +
          +-- Locks table t from changes, writes an error if someone attempts to change the table.
          +-- This is useful for detecting variables that have "accidently" been made global. Something
          +-- I tend to do all too much.
          +function M.lock(t)
          +	local function lock_new_index(t, k, v)
          +		error("module has been locked -- " .. k .. " must be declared local", 2)
          +	end
          +
          +	local mt = {__newindex = lock_new_index}
          +	if getmetatable(t) then
          +      mt.__index = getmetatable(t).__index
          +   end
          +	setmetatable(t, mt)
          +end
          +
          +-- Returns the result of mapping the values in table t through the function f
          +local function map(t, f)
          +	local out = {}
          +	for k,v in pairs(t) do out[k] = f(v,k) end
          +	return out
          +end
          +
          +-- The identity function, useful as a placeholder.
          +local function identity(text) return text end
          +
          +-- Functional style if statement. (NOTE: no short circuit evaluation)
          +local function iff(t, a, b) if t then return a else return b end end
          +
          +-- Splits the text into an array of separate lines.
          +local function split(text, sep)
          +	sep = sep or "\n"
          +	local lines = {}
          +	local pos = 1
          +	while true do
          +		local b,e = text:find(sep, pos)
          +		if not b then table.insert(lines, text:sub(pos)) break end
          +		table.insert(lines, text:sub(pos, b-1))
          +		pos = e + 1
          +	end
          +	return lines
          +end
          +
          +-- Converts tabs to spaces
          +local function detab(text)
          +	local tab_width = 4
          +	local function rep(match)
          +		local spaces = -match:len()
          +		while spaces<1 do spaces = spaces + tab_width end
          +		return match .. string.rep(" ", spaces)
          +	end
          +	text = text:gsub("([^\n]-)\t", rep)
          +	return text
          +end
          +
          +-- Applies string.find for every pattern in the list and returns the first match
          +local function find_first(s, patterns, index)
          +	local res = {}
          +	for _,p in ipairs(patterns) do
          +		local match = {s:find(p, index)}
          +		if #match>0 and (#res==0 or match[1] < res[1]) then res = match end
          +	end
          +	return unpack(res)
          +end
          +
          +-- If a replacement array is specified, the range [start, stop] in the array is replaced
          +-- with the replacement array and the resulting array is returned. Without a replacement
          +-- array the section of the array between start and stop is returned.
          +local function splice(array, start, stop, replacement)
          +	if replacement then
          +		local n = stop - start + 1
          +		while n > 0 do
          +			table.remove(array, start)
          +			n = n - 1
          +		end
          +		for i,v in ipairs(replacement) do
          +			table.insert(array, start, v)
          +		end
          +		return array
          +	else
          +		local res = {}
          +		for i = start,stop do
          +			table.insert(res, array[i])
          +		end
          +		return res
          +	end
          +end
          +
          +-- Outdents the text one step.
          +local function outdent(text)
          +	text = "\n" .. text
          +	text = text:gsub("\n  ? ? ?", "\n")
          +	text = text:sub(2)
          +	return text
          +end
          +
          +-- Indents the text one step.
          +local function indent(text)
          +	text = text:gsub("\n", "\n    ")
          +	return text
          +end
          +
          +-- Does a simple tokenization of html data. Returns the data as a list of tokens.
          +-- Each token is a table with a type field (which is either "tag" or "text") and
          +-- a text field (which contains the original token data).
          +local function tokenize_html(html)
          +	local tokens = {}
          +	local pos = 1
          +	while true do
          +		local start = find_first(html, {"", start)
          +		elseif html:match("^<%?", start) then
          +			_,stop = html:find("?>", start)
          +		else
          +			_,stop = html:find("%b<>", start)
          +		end
          +		if not stop then
          +			-- error("Could not match html tag " .. html:sub(start,start+30))
          +		 	table.insert(tokens, {type="text", text=html:sub(start, start)})
          +			pos = start + 1
          +		else
          +			table.insert(tokens, {type="tag", text=html:sub(start, stop)})
          +			pos = stop + 1
          +		end
          +	end
          +	return tokens
          +end
          +
          +----------------------------------------------------------------------
          +-- Hash
          +----------------------------------------------------------------------
          +
          +-- This is used to "hash" data into alphanumeric strings that are unique
          +-- in the document. (Note that this is not cryptographic hash, the hash
          +-- function is not one-way.) The hash procedure is used to protect parts
          +-- of the document from further processing.
          +
          +local HASH = {
          +	-- Has the hash been inited.
          +	inited = false,
          +
          +	-- The unique string prepended to all hash values. This is to ensure
          +	-- that hash values do not accidently coincide with an actual existing
          +	-- string in the document.
          +	identifier = "",
          +
          +	-- Counter that counts up for each new hash instance.
          +	counter = 0,
          +
          +	-- Hash table.
          +	table = {}
          +}
          +
          +-- Inits hashing. Creates a hash_identifier that doesn't occur anywhere
          +-- in the text.
          +local function init_hash(text)
          +	HASH.inited = true
          +	HASH.identifier = ""
          +	HASH.counter = 0
          +	HASH.table = {}
          +
          +	local s = "HASH"
          +	local counter = 0
          +	local id
          +	while true do
          +		id  = s .. counter
          +		if not text:find(id, 1, true) then break end
          +		counter = counter + 1
          +	end
          +	HASH.identifier = id
          +end
          +
          +-- Returns the hashed value for s.
          +local function hash(s)
          +	assert(HASH.inited)
          +	if not HASH.table[s] then
          +		HASH.counter = HASH.counter + 1
          +		local id = HASH.identifier .. HASH.counter .. "X"
          +		HASH.table[s] = id
          +	end
          +	return HASH.table[s]
          +end
          +
          +----------------------------------------------------------------------
          +-- Protection
          +----------------------------------------------------------------------
          +
          +-- The protection module is used to "protect" parts of a document
          +-- so that they are not modified by subsequent processing steps.
          +-- Protected parts are saved in a table for later unprotection
          +
          +-- Protection data
          +local PD = {
          +	-- Saved blocks that have been converted
          +	blocks = {},
          +
          +	-- Block level tags that will be protected
          +	tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote",
          +	"pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset",
          +	"iframe", "math", "ins", "del"}
          +}
          +
          +-- Pattern for matching a block tag that begins and ends in the leftmost
          +-- column and may contain indented subtags, i.e.
          +-- 
          +-- A nested block. +--
          +-- Nested data. +--
          +--
          +local function block_pattern(tag) + return "\n<" .. tag .. ".-\n[ \t]*\n" +end + +-- Pattern for matching a block tag that begins and ends with a newline +local function line_pattern(tag) + return "\n<" .. tag .. ".-[ \t]*\n" +end + +-- Protects the range of characters from start to stop in the text and +-- returns the protected string. +local function protect_range(text, start, stop) + local s = text:sub(start, stop) + local h = hash(s) + PD.blocks[h] = s + text = text:sub(1,start) .. h .. text:sub(stop) + return text +end + +-- Protect every part of the text that matches any of the patterns. The first +-- matching pattern is protected first, etc. +local function protect_matches(text, patterns) + while true do + local start, stop = find_first(text, patterns) + if not start then break end + text = protect_range(text, start, stop) + end + return text +end + +-- Protects blocklevel tags in the specified text +local function protect(text) + -- First protect potentially nested block tags + text = protect_matches(text, map(PD.tags, block_pattern)) + -- Then protect block tags at the line level. + text = protect_matches(text, map(PD.tags, line_pattern)) + -- Protect
          and comment tags + text = protect_matches(text, {"\n]->[ \t]*\n"}) + text = protect_matches(text, {"\n[ \t]*\n"}) + return text +end + +-- Returns true if the string s is a hash resulting from protection +local function is_protected(s) + return PD.blocks[s] +end + +-- Unprotects the specified text by expanding all the nonces +local function unprotect(text) + for k,v in pairs(PD.blocks) do + v = v:gsub("%%", "%%%%") + text = text:gsub(k, v) + end + return text +end + + +---------------------------------------------------------------------- +-- Block transform +---------------------------------------------------------------------- + +-- The block transform functions transform the text on the block level. +-- They work with the text as an array of lines rather than as individual +-- characters. + +-- Returns true if the line is a ruler of (char) characters. +-- The line must contain at least three char characters and contain only spaces and +-- char characters. +local function is_ruler_of(line, char) + if not line:match("^[ %" .. char .. "]*$") then return false end + if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end + return true +end + +-- Identifies the block level formatting present in the line +local function classify(line) + local info = {line = line, text = line} + + if line:match("^ ") then + info.type = "indented" + info.outdented = line:sub(5) + return info + end + + for _,c in ipairs({'*', '-', '_', '='}) do + if is_ruler_of(line, c) then + info.type = "ruler" + info.ruler_char = c + return info + end + end + + if line == "" then + info.type = "blank" + return info + end + + if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then + local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") + info.type = "header" + info.level = m1:len() + info.text = m2 + return info + end + + if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then + local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") + info.type = "list_item" + info.list_type = "numeric" + info.number = 0 + number + info.text = text + return info + end + + if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then + local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") + info.type = "list_item" + info.list_type = "bullet" + info.bullet = bullet + info.text= text + return info + end + + if line:match("^>[ \t]?(.*)") then + info.type = "blockquote" + info.text = line:match("^>[ \t]?(.*)") + return info + end + + if is_protected(line) then + info.type = "raw" + info.html = unprotect(line) + return info + end + + info.type = "normal" + return info +end + +-- Find headers constisting of a normal line followed by a ruler and converts them to +-- header entries. +local function headers(array) + local i = 1 + while i <= #array - 1 do + if array[i].type == "normal" and array[i+1].type == "ruler" and + (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then + local info = {line = array[i].line} + info.text = info.line + info.type = "header" + info.level = iff(array[i+1].ruler_char == "=", 1, 2) + table.remove(array, i+1) + array[i] = info + end + i = i + 1 + end + return array +end + +local block_transform, blocks_to_html, encode_code, span_transform, encode_backslash_escapes + +-- Find list blocks and convert them to protected data blocks +local function lists(array, sublist) + local function process_list(arr) + local function any_blanks(arr) + for i = 1, #arr do + if arr[i].type == "blank" then return true end + end + return false + end + + local function split_list_items(arr) + local acc = {arr[1]} + local res = {} + for i=2,#arr do + if arr[i].type == "list_item" then + table.insert(res, acc) + acc = {arr[i]} + else + table.insert(acc, arr[i]) + end + end + table.insert(res, acc) + return res + end + + local function process_list_item(lines, block) + while lines[#lines].type == "blank" do + table.remove(lines) + end + + local itemtext = lines[1].text + for i=2,#lines do + itemtext = itemtext .. "\n" .. outdent(lines[i].line) + end + if block then + itemtext = block_transform(itemtext, true) + if not itemtext:find("
          ") then itemtext = indent(itemtext) end
          +				return "    
        • " .. itemtext .. "
        • " + else + local lines = split(itemtext) + lines = map(lines, classify) + lines = lists(lines, true) + lines = blocks_to_html(lines, true) + itemtext = table.concat(lines, "\n") + if not itemtext:find("
          ") then itemtext = indent(itemtext) end
          +				return "    
        • " .. itemtext .. "
        • " + end + end + + local block_list = any_blanks(arr) + local items = split_list_items(arr) + local out = "" + for _, item in ipairs(items) do + out = out .. process_list_item(item, block_list) .. "\n" + end + if arr[1].list_type == "numeric" then + return "
            \n" .. out .. "
          " + else + return "
            \n" .. out .. "
          " + end + end + + -- Finds the range of lines composing the first list in the array. A list + -- starts with (^ list_item) or (blank list_item) and ends with + -- (blank* $) or (blank normal). + -- + -- A sublist can start with just (list_item) does not need a blank... + local function find_list(array, sublist) + local function find_list_start(array, sublist) + if array[1].type == "list_item" then return 1 end + if sublist then + for i = 1,#array do + if array[i].type == "list_item" then return i end + end + else + for i = 1, #array-1 do + if array[i].type == "blank" and array[i+1].type == "list_item" then + return i+1 + end + end + end + return nil + end + local function find_list_end(array, start) + local pos = #array + for i = start, #array-1 do + if array[i].type == "blank" and array[i+1].type ~= "list_item" + and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then + pos = i-1 + break + end + end + while pos > start and array[pos].type == "blank" do + pos = pos - 1 + end + return pos + end + + local start = find_list_start(array, sublist) + if not start then return nil end + return start, find_list_end(array, start) + end + + while true do + local start, stop = find_list(array, sublist) + if not start then break end + local text = process_list(splice(array, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + array = splice(array, start, stop, {info}) + end + + -- Convert any remaining list items to normal + for _,line in ipairs(array) do + if line.type == "list_item" then line.type = "normal" end + end + + return array +end + +-- Find and convert blockquote markers. +local function blockquotes(lines) + local function find_blockquote(lines) + local start + for i,line in ipairs(lines) do + if line.type == "blockquote" then + start = i + break + end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type == "blank" or lines[i].type == "blockquote" then + elseif lines[i].type == "normal" then + if lines[i-1].type == "blank" then stop = i-1 break end + else + stop = i-1 break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_blockquote(lines) + local raw = lines[1].text + for i = 2,#lines do + raw = raw .. "\n" .. lines[i].text + end + local bt = block_transform(raw) + if not bt:find("
          ") then bt = indent(bt) end
          +		return "
          \n " .. bt .. + "\n
          " + end + + while true do + local start, stop = find_blockquote(lines) + if not start then break end + local text = process_blockquote(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Find and convert codeblocks. +local function codeblocks(lines) + local function find_codeblock(lines) + local start + for i,line in ipairs(lines) do + if line.type == "indented" then start = i break end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type ~= "indented" and lines[i].type ~= "blank" then + stop = i-1 + break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_codeblock(lines) + local raw = detab(encode_code(outdent(lines[1].line))) + for i = 2,#lines do + raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) + end + return "
          " .. raw .. "\n
          " + end + + while true do + local start, stop = find_codeblock(lines) + if not start then break end + local text = process_codeblock(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Convert lines to html code +function blocks_to_html(lines, no_paragraphs) + local out = {} + local i = 1 + while i <= #lines do + local line = lines[i] + if line.type == "ruler" then + table.insert(out, "
          ") + elseif line.type == "raw" then + table.insert(out, line.html) + elseif line.type == "normal" then + local s = line.line + + while i+1 <= #lines and lines[i+1].type == "normal" do + i = i + 1 + s = s .. "\n" .. lines[i].line + end + + if no_paragraphs then + table.insert(out, span_transform(s)) + else + table.insert(out, "

          " .. span_transform(s) .. "

          ") + end + elseif line.type == "header" then + local s = "" .. span_transform(line.text) .. "" + table.insert(out, s) + else + table.insert(out, line.line) + end + i = i + 1 + end + return out +end + +-- Perform all the block level transforms +function block_transform(text, sublist) + local lines = split(text) + lines = map(lines, classify) + lines = headers(lines) + lines = lists(lines, sublist) + lines = codeblocks(lines) + lines = blockquotes(lines) + lines = blocks_to_html(lines) + local text = table.concat(lines, "\n") + return text +end + +-- Debug function for printing a line array to see the result +-- of partial transforms. +local function print_lines(lines) + for i, line in ipairs(lines) do + print(i, line.type, line.text or line.line) + end +end + +---------------------------------------------------------------------- +-- Span transform +---------------------------------------------------------------------- + +-- Functions for transforming the text at the span level. + +-- These characters may need to be escaped because they have a special +-- meaning in markdown. +local escape_chars = "'\\`*_{}[]()>#+-.!'" +local escape_table = {} + +local function init_escape_table() + escape_table = {} + for i = 1,#escape_chars do + local c = escape_chars:sub(i,i) + escape_table[c] = hash(c) + end +end + +-- Adds a new escape to the escape table. +local function add_escape(text) + if not escape_table[text] then + escape_table[text] = hash(text) + end + return escape_table[text] +end + +-- Escape characters that should not be disturbed by markdown. +local function escape_special_chars(text) + local tokens = tokenize_html(text) + + local out = "" + for _, token in ipairs(tokens) do + local t = token.text + if token.type == "tag" then + -- In tags, encode * and _ so they don't conflict with their use in markdown. + t = t:gsub("%*", escape_table["*"]) + t = t:gsub("%_", escape_table["_"]) + else + t = encode_backslash_escapes(t) + end + out = out .. t + end + return out +end + +-- Encode backspace-escaped characters in the markdown source. +function encode_backslash_escapes(t) + for i=1,escape_chars:len() do + local c = escape_chars:sub(i,i) + t = t:gsub("\\%" .. c, escape_table[c]) + end + return t +end + +-- Unescape characters that have been encoded. +local function unescape_special_chars(t) + local tin = t + for k,v in pairs(escape_table) do + k = k:gsub("%%", "%%%%") + t = t:gsub(v,k) + end + if t ~= tin then t = unescape_special_chars(t) end + return t +end + +-- Encode/escape certain characters inside Markdown code runs. +-- The point is that in code, these characters are literals, +-- and lose their special Markdown meanings. +function encode_code(s) + s = s:gsub("%&", "&") + s = s:gsub("<", "<") + s = s:gsub(">", ">") + for k,v in pairs(escape_table) do + s = s:gsub("%"..k, v) + end + return s +end + +-- Handle backtick blocks. +local function code_spans(s) + s = s:gsub("\\\\", escape_table["\\"]) + s = s:gsub("\\`", escape_table["`"]) + + local pos = 1 + while true do + local start, stop = s:find("`+", pos) + if not start then return s end + local count = stop - start + 1 + -- Find a matching numbert of backticks + local estart, estop = s:find(string.rep("`", count), stop+1) + local brstart = s:find("\n", stop+1) + if estart and (not brstart or estart < brstart) then + local code = s:sub(stop+1, estart-1) + code = code:gsub("^[ \t]+", "") + code = code:gsub("[ \t]+$", "") + code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) + code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) + code = "" .. encode_code(code) .. "" + code = add_escape(code) + s = s:sub(1, start-1) .. code .. s:sub(estop+1) + pos = start + code:len() + else + pos = stop + 1 + end + end + return s +end + +-- Encode alt text... enodes &, and ". +local function encode_alt(s) + if not s then return s end + s = s:gsub('&', '&') + s = s:gsub('"', '"') + s = s:gsub('<', '<') + return s +end + +local link_database + +-- Handle image references +local function images(text) + local function reference_link(alt, id) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + id = id:match("%[(.*)%]"):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape ('' .. alt .. '") + end + + local function inline_link(alt, link) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + url = url or link:match("%(?%)") + url = encode_alt(url) + title = encode_alt(title) + if title then + return add_escape('' .. alt .. '') + else + return add_escape('' .. alt .. '') + end + end + + text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("!(%b[])(%b())", inline_link) + return text +end + +-- Handle anchor references +local function anchors(text) + local function reference_link(text, id) + text = text:match("%b[]"):sub(2,-2) + id = id:match("%b[]"):sub(2,-2):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape("") .. text .. add_escape("") + end + + local function inline_link(text, link) + text = text:match("%b[]"):sub(2,-2) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + title = encode_alt(title) + url = url or link:match("%(?%)") or "" + url = encode_alt(url) + if title then + return add_escape("") .. text .. "" + else + return add_escape("") .. text .. add_escape("") + end + end + + text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("(%b[])(%b())", inline_link) + return text +end + +-- Handle auto links, i.e. . +local function auto_links(text) + local function link(s) + return add_escape("") .. s .. "" + end + -- Encode chars as a mix of dec and hex entitites to (perhaps) fool + -- spambots. + local function encode_email_address(s) + -- Use a deterministic encoding to make unit testing possible. + -- Code 45% hex, 45% dec, 10% plain. + local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} + local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} + local plain = {code = function(c) return c end, count = 0, rate = 0.1} + local codes = {hex, dec, plain} + local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end + + local out = "" + for i = 1,s:len() do + for _,code in ipairs(codes) do code.count = code.count + code.rate end + if codes[1].count < codes[2].count then swap(codes,1,2) end + if codes[2].count < codes[3].count then swap(codes,2,3) end + if codes[1].count < codes[2].count then swap(codes,1,2) end + + local code = codes[1] + local c = s:sub(i,i) + -- Force encoding of "@" to make email address more invisible. + if c == "@" and code == plain then code = codes[2] end + out = out .. code.code(c) + code.count = code.count - 1 + end + return out + end + local function mail(s) + s = unescape_special_chars(s) + local address = encode_email_address("mailto:" .. s) + local text = encode_email_address(s) + return add_escape("") .. text .. "" + end + -- links + text = text:gsub("<(https?:[^'\">%s]+)>", link) + text = text:gsub("<(ftp:[^'\">%s]+)>", link) + + -- mail + text = text:gsub("%s]+)>", mail) + text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) + return text +end + +-- Encode free standing amps (&) and angles (<)... note that this does not +-- encode free >. +local function amps_and_angles(s) + -- encode amps not part of &..; expression + local pos = 1 + while true do + local amp = s:find("&", pos) + if not amp then break end + local semi = s:find(";", amp+1) + local stop = s:find("[ \t\n&]", amp+1) + if not semi or (stop and stop < semi) or (semi - amp) > 15 then + s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) + pos = amp+1 + else + pos = amp+1 + end + end + + -- encode naked <'s + s = s:gsub("<([^a-zA-Z/?$!])", "<%1") + s = s:gsub("<$", "<") + + -- what about >, nothing done in the original markdown source to handle them + return s +end + +-- Handles emphasis markers (* and _) in the text. +local function emphasis(text) + for _, s in ipairs {"%*%*", "%_%_"} do + text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "%1") + text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "%1") + end + for _, s in ipairs {"%*", "%_"} do + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "%1") + text = text:gsub(s .. "([^<>_]-[^<>_]-[^<>_]-)" .. s, "%1") + end + return text +end + +-- Handles line break markers in the text. +local function line_breaks(text) + return text:gsub(" +\n", "
          \n") +end + +-- Perform all span level transforms. +function span_transform(text) + text = code_spans(text) + text = escape_special_chars(text) + text = images(text) + text = anchors(text) + text = auto_links(text) + text = amps_and_angles(text) + text = emphasis(text) + text = line_breaks(text) + return text +end + +---------------------------------------------------------------------- +-- Markdown +---------------------------------------------------------------------- + +-- Cleanup the text by normalizing some possible variations to make further +-- processing easier. +local function cleanup(text) + -- Standardize line endings + text = text:gsub("\r\n", "\n") -- DOS to UNIX + text = text:gsub("\r", "\n") -- Mac to UNIX + + -- Convert all tabs to spaces + text = detab(text) + + -- Strip lines with only spaces and tabs + while true do + local subs + text, subs = text:gsub("\n[ \t]+\n", "\n\n") + if subs == 0 then break end + end + + return "\n" .. text .. "\n" +end + +-- Strips link definitions from the text and stores the data in a lookup table. +local function strip_link_definitions(text) + local linkdb = {} + + local function link_def(id, url, title) + id = id:match("%[(.+)%]"):lower() + linkdb[id] = linkdb[id] or {} + linkdb[id].url = url or linkdb[id].url + linkdb[id].title = title or linkdb[id].title + return "" + end + + local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*]+)>?[ \t]*" + local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" + + text = text:gsub(def_title1, link_def) + text = text:gsub(def_title2, link_def) + text = text:gsub(def_title3, link_def) + text = text:gsub(def_no_title, link_def) + return text, linkdb +end + +link_database = {} + +-- Main markdown processing function +local function markdown(text) + init_hash(text) + init_escape_table() + + text = cleanup(text) + text = protect(text) + text, link_database = strip_link_definitions(text) + text = block_transform(text) + text = unescape_special_chars(text) + return text +end + +---------------------------------------------------------------------- +-- End of module +---------------------------------------------------------------------- + +M.lock(M) + +-- Expose markdown function to the world +_G.markdown = M.markdown + +-- Class for parsing command-line options +local OptionParser = {} +OptionParser.__index = OptionParser + +-- Creates a new option parser +function OptionParser:new() + local o = {short = {}, long = {}} + setmetatable(o, self) + return o +end + +-- Calls f() whenever a flag with specified short and long name is encountered +function OptionParser:flag(short, long, f) + local info = {type = "flag", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(param) whenever a parameter flag with specified short and long name is encountered +function OptionParser:param(short, long, f) + local info = {type = "param", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(v) for each non-flag argument +function OptionParser:arg(f) + self.arg = f +end + +-- Runs the option parser for the specified set of arguments. Returns true if all arguments +-- where successfully parsed and false otherwise. +function OptionParser:run(args) + local pos = 1 + local param + while pos <= #args do + local arg = args[pos] + if arg == "--" then + for i=pos+1,#args do + if self.arg then self.arg(args[i]) end + return true + end + end + if arg:match("^%-%-") then + local info = self.long[arg:sub(3)] + if not info then print("Unknown flag: " .. arg) return false end + if info.type == "flag" then + info.f() + pos = pos + 1 + else + param = args[pos+1] + if not param then print("No parameter for flag: " .. arg) return false end + info.f(param) + pos = pos+2 + end + elseif arg:match("^%-") then + for i=2,arg:len() do + local c = arg:sub(i,i) + local info = self.short[c] + if not info then print("Unknown flag: -" .. c) return false end + if info.type == "flag" then + info.f() + else + if i == arg:len() then + param = args[pos+1] + if not param then print("No parameter for flag: -" .. c) return false end + info.f(param) + pos = pos + 1 + else + param = arg:sub(i+1) + info.f(param) + end + break + end + end + pos = pos + 1 + else + if self.arg then self.arg(arg) end + pos = pos + 1 + end + end + return true +end + +-- Handles the case when markdown is run from the command line +local function run_command_line(arg) + -- Generate output for input s given options + local function run(s, options) + s = markdown(s) + if not options.wrap_header then return s end + local header = "" + if options.header then + local f = io.open(options.header) or error("Could not open file: " .. options.header) + header = f:read("*a") + f:close() + else + header = [[ + + + + + TITLE + + + +]] + local title = options.title or s:match("

          (.-)

          ") or s:match("

          (.-)

          ") or + s:match("

          (.-)

          ") or "Untitled" + header = header:gsub("TITLE", title) + if options.inline_style then + local style = "" + local f = io.open(options.stylesheet) + if f then + style = f:read("*a") f:close() + else + error("Could not include style sheet " .. options.stylesheet .. ": File not found") + end + header = header:gsub('', + "") + else + header = header:gsub("STYLESHEET", options.stylesheet) + end + header = header:gsub("CHARSET", options.charset) + end + local footer = "" + if options.footer then + local f = io.open(options.footer) or error("Could not open file: " .. options.footer) + footer = f:read("*a") + f:close() + end + return header .. s .. footer + end + + -- Generate output path name from input path name given options. + local function outpath(path, options) + if options.append then return path .. ".html" end + local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end + m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end + return path .. ".html" + end + + -- Default commandline options + local options = { + wrap_header = true, + header = nil, + footer = nil, + charset = "utf-8", + title = nil, + stylesheet = "default.css", + inline_style = false + } + local help = [[ +Usage: markdown.lua [OPTION] [FILE] +Runs the markdown text markup to HTML converter on each file specified on the +command line. If no files are specified, runs on standard input. + +No header: + -n, --no-wrap Don't wrap the output in ... tags. +Custom header: + -e, --header FILE Use content of FILE for header. + -f, --footer FILE Use content of FILE for footer. +Generated header: + -c, --charset SET Specifies charset (default utf-8). + -i, --title TITLE Specifies title (default from first

          tag). + -s, --style STYLE Specifies style sheet file (default default.css). + -l, --inline-style Include the style sheet file inline in the header. +Generated files: + -a, --append Append .html extension (instead of replacing). +Other options: + -h, --help Print this help text. + -t, --test Run the unit tests. +]] + + local run_stdin = true + local op = OptionParser:new() + op:flag("n", "no-wrap", function () options.wrap_header = false end) + op:param("e", "header", function (x) options.header = x end) + op:param("f", "footer", function (x) options.footer = x end) + op:param("c", "charset", function (x) options.charset = x end) + op:param("i", "title", function(x) options.title = x end) + op:param("s", "style", function(x) options.stylesheet = x end) + op:flag("l", "inline-style", function(x) options.inline_style = true end) + op:flag("a", "append", function() options.append = true end) + op:flag("t", "test", function() + local n = arg[0]:gsub("markdown.lua", "markdown-tests.lua") + local f = io.open(n) + if f then + f:close() dofile(n) + else + error("Cannot find markdown-tests.lua") + end + run_stdin = false + end) + op:flag("h", "help", function() print(help) run_stdin = false end) + op:arg(function(path) + local file = io.open(path) or error("Could not open file: " .. path) + local s = file:read("*a") + file:close() + s = run(s, options) + file = io.open(outpath(path, options), "w") or error("Could not open output file: " .. outpath(path, options)) + file:write(s) + file:close() + run_stdin = false + end + ) + + if not op:run(arg) then + print(help) + run_stdin = false + end + + if run_stdin then + local s = io.read("*a") + s = run(s, options) + io.write(s) + end +end + +-- If we are being run from the command-line, act accordingly +if arg and arg[0]:find("markdown%.lua$") then + run_command_line(arg) +else + return markdown +end diff --git a/ldoc/markup.lua b/ldoc/markup.lua index 5f2b2988..ec4acf39 100644 --- a/ldoc/markup.lua +++ b/ldoc/markup.lua @@ -15,14 +15,25 @@ local backtick_references -- inline use same lookup as @see local function resolve_inline_references (ldoc, txt, item, plain) local res = (txt:gsub('@{([^}]-)}',function (name) + if name:match '^\\' then return '@{'..name:sub(2)..'}' end local qname,label = utils.splitv(name,'%s*|') if not qname then qname = name end - local ref,err = markup.process_reference(qname) + local ref, err + local custom_ref, refname = utils.splitv(qname,':') + if custom_ref and ldoc.custom_references then + custom_ref = ldoc.custom_references[custom_ref] + if custom_ref then + ref,err = custom_ref(refname) + end + end + if not ref then + ref,err = markup.process_reference(qname) + end if not ref then err = err .. ' ' .. qname - if item then item:warning(err) + if item and item.warning then item:warning(err) else io.stderr:write('nofile error: ',err,'\n') end @@ -43,9 +54,12 @@ local function resolve_inline_references (ldoc, txt, item, plain) res = res:gsub('`([^`]+)`',function(name) local ref,err = markup.process_reference(name) if ref then + if not plain and name then + name = name:gsub('_', '\\_') + end return ('%s '):format(ldoc.href(ref),name) else - return '`'..name..'`' + return ''..name..'' end end) end @@ -56,7 +70,8 @@ end -- they can appear in the contents list as a ToC. function markup.add_sections(F, txt) local sections, L, first = {}, 1, true - local title_pat_end, title_pat = '[^#]%s*(.+)' + local title_pat + local lstrip = stringx.lstrip for line in stringx.lines(txt) do if first then local level,header = line:match '^(#+)%s*(.+)' @@ -65,14 +80,16 @@ function markup.add_sections(F, txt) else level = '##' end - title_pat = '^'..level..title_pat_end + title_pat = '^'..level..'([^#]%s*.+)' + title_pat = lstrip(title_pat) first = false + F.display_name = header end local title = line:match (title_pat) if title then - -- Markdown does allow this pattern + -- Markdown allows trailing '#'... title = title:gsub('%s*#+$','') - sections[L] = F:add_document_section(title) + sections[L] = F:add_document_section(lstrip(title)) end L = L + 1 end @@ -86,8 +103,8 @@ local function indent_line (line) return indent,line end -local function non_blank (line) - return line:find '%S' +local function blank (line) + return not line:find '%S' end local global_context, local_context @@ -115,6 +132,8 @@ local function process_multiline_markdown(ldoc, txt, F) code = concat(code,'\n') if code ~= '' then local err + -- If we omit the following '\n', a '--' (or '//') comment on the + -- last line won't be recognized. code, err = prettify.code(lang,filename,code..'\n',L,false) append(res,'
          ')
                    append(res, code)
          @@ -152,7 +171,7 @@ local function process_multiline_markdown(ldoc, txt, F)
                 if indent >= 4 then -- indented code block
                    local code = {}
                    local plain
          -         while indent >= 4 or not non_blank(line) do
          +         while indent >= 4 or blank(line) do
                       if not start_indent then
                          start_indent = indent
                          if line:match '^%s*@plain%s*$' then
          @@ -161,7 +180,7 @@ local function process_multiline_markdown(ldoc, txt, F)
                          end
                       end
                       if not plain then
          -               append(code,line:sub(start_indent))
          +               append(code,line:sub(start_indent + 1))
                       else
                          append(res,line)
                       end
          @@ -170,7 +189,9 @@ local function process_multiline_markdown(ldoc, txt, F)
                       indent, line = indent_line(line)
                    end
                    start_indent = nil
          -         if #code > 1 then table.remove(code) end
          +         while #code > 1 and blank(code[#code]) do  -- trim blank lines.
          +           table.remove(code)
          +         end
                    pretty_code (code,'lua')
                 else
                    local section = F.sections[L]
          @@ -233,7 +254,6 @@ local function get_formatter(format)
              end
           end
           
          -
           local function text_processor(ldoc)
              return function(txt,item)
                 if txt == nil then return '' end
          @@ -243,10 +263,17 @@ local function text_processor(ldoc)
              end
           end
           
          +local plain_processor
           
           local function markdown_processor(ldoc, formatter)
          -   return function (txt,item)
          +   return function (txt,item,plain)
                 if txt == nil then return '' end
          +      if plain then
          +         if not plain_processor then
          +            plain_processor = text_processor(ldoc)
          +         end
          +         return plain_processor(txt,item)
          +      end
                 if utils.is_type(item,doc.File) then
                    txt = process_multiline_markdown(ldoc, txt, item)
                 else
          @@ -258,7 +285,6 @@ local function markdown_processor(ldoc, formatter)
              end
           end
           
          -
           local function get_processor(ldoc, format)
              if format == 'plain' then return text_processor(ldoc) end
           
          @@ -276,25 +302,29 @@ end
           function markup.create (ldoc, format, pretty)
              local processor
              markup.plain = true
          +   if format == 'backtick' then
          +      ldoc.backtick_references = true
          +      format = 'plain'
          +   end
              backtick_references = ldoc.backtick_references
              global_context = ldoc.package and ldoc.package .. '.'
              prettify.set_prettifier(pretty)
           
          -   markup.process_reference = function(name)
          +   markup.process_reference = function(name,istype)
                 if local_context == 'none.' and not name:match '%.' then
                    return nil,'not found'
                 end
                 local mod = ldoc.single or ldoc.module or ldoc.modules[1]
          -      local ref,err = mod:process_see_reference(name, ldoc.modules)
          +      local ref,err = mod:process_see_reference(name, ldoc.modules, istype)
                 if ref then return ref end
                 if global_context then
                    local qname = global_context .. name
          -         ref = mod:process_see_reference(qname, ldoc.modules)
          +         ref = mod:process_see_reference(qname, ldoc.modules, istype)
                    if ref then return ref end
                 end
                 if local_context then
                    local qname = local_context .. name
          -         ref = mod:process_see_reference(qname, ldoc.modules)
          +         ref = mod:process_see_reference(qname, ldoc.modules, istype)
                    if ref then return ref end
                 end
                 -- note that we'll return the original error!
          diff --git a/ldoc/parse.lua b/ldoc/parse.lua
          index 721e75b4..30ecdb13 100644
          --- a/ldoc/parse.lua
          +++ b/ldoc/parse.lua
          @@ -1,5 +1,6 @@
           -- parsing code for doc comments
           
          +local utils = require 'pl.utils'
           local List = require 'pl.List'
           local Map = require 'pl.Map'
           local stringio = require 'pl.stringio'
          @@ -7,6 +8,7 @@ local lexer = require 'ldoc.lexer'
           local tools = require 'ldoc.tools'
           local doc = require 'ldoc.doc'
           local Item,File = doc.Item,doc.File
          +local unpack = utils.unpack
           
           ------ Parsing the Source --------------
           -- This uses the lexer from PL, but it should be possible to use Peter Odding's
          @@ -70,18 +72,49 @@ local function parse_colon_tags (text)
              return preamble,tag_items
           end
           
          +-- Tags are stored as an ordered multi map from strings to strings
          +-- If the same key is used, then the value becomes a list
           local Tags = {}
           Tags.__index = Tags
           
          -function Tags.new (t)
          +function Tags.new (t,name)
          +   local class
          +   if name then
          +      class = t
          +      t = {}
          +   end
              t._order = List()
          -   return setmetatable(t,Tags)
          +   local tags = setmetatable(t,Tags)
          +   if name then
          +      tags:add('class',class)
          +      tags:add('name',name)
          +   end
          +   return tags
           end
           
          -function Tags:add (tag,value)
          -   self[tag] = value
          -   --print('adding',tag,value)
          -   self._order:append(tag)
          +function Tags:add (tag,value,modifiers)
          +   if modifiers then -- how modifiers are encoded
          +      value = {value,modifiers=modifiers}
          +   end
          +   local ovalue = self:get(tag)
          +   if ovalue then
          +      ovalue:append(value)
          +      value = ovalue
          +   end
          +   rawset(self,tag,value)
          +   if not ovalue then
          +      self._order:append(tag)
          +   end
          +end
          +
          +function Tags:get (tag)
          +   local ovalue = rawget(self,tag)
          +   if ovalue then -- previous value?
          +      if getmetatable(ovalue) ~= List then
          +         ovalue = List{ovalue}
          +      end
          +      return ovalue
          +   end
           end
           
           function Tags:iter ()
          @@ -119,16 +152,7 @@ local function extract_tags (s,args)
                    value = strip(value)
                 end
           
          -      if modifiers then value = { value, modifiers=modifiers } end
          -      local old_value = tags[tag]
          -
          -      if not old_value then -- first element
          -         tags:add(tag,value)
          -      elseif type(old_value)=='table' and old_value.append then -- append to existing list
          -         old_value :append (value)
          -      else -- upgrade string->list
          -         tags:add(tag,List{old_value, value})
          -      end
          +      tags:add(tag,value,modifiers)
              end
              return tags --Map(tags)
           end
          @@ -154,15 +178,15 @@ local function parse_file(fname, lang, package, args)
              local current_item, module_item
           
              F.args = args
          -
          +   F.lang = lang
              F.base = package
           
              local tok,f = lang.lexer(fname)
              if not tok then return nil end
           
          -    local function lineno ()
          +   local function lineno ()
                 return tok:lineno()
          -    end
          +   end
           
              local function filename () return fname end
           
          @@ -190,6 +214,8 @@ local function parse_file(fname, lang, package, args)
              local t,v = tnext(tok)
              -- with some coding styles first comment is standard boilerplate; option to ignore this.
              if args.boilerplate and t == 'comment' then
          +      -- hack to deal with boilerplate inside Lua block comments
          +      if v:match '%s*%-%-%[%[' then lang:grab_block_comment(v,tok) end
                 t,v = tnext(tok)
              end
              if t == '#' then -- skip Lua shebang line, if present
          @@ -211,7 +237,7 @@ local function parse_file(fname, lang, package, args)
                 else
                    mod,t,v = lang:parse_module_call(tok,t,v)
                    if mod ~= '...' then
          -            add_module({summary='(no description)'},mod,true)
          +            add_module(Tags.new{summary='(no description)'},mod,true)
                       first_comment = false
                       module_found = true
                    end
          @@ -229,6 +255,9 @@ local function parse_file(fname, lang, package, args)
           
                    if lang:empty_comment(v)  then -- ignore rest of empty start comments
                       t,v = tok()
          +            if t == 'space' and not v:match '\n' then
          +               t,v = tok()
          +            end
                    end
           
                    while t and t == 'comment' do
          @@ -245,7 +274,11 @@ local function parse_file(fname, lang, package, args)
                    local item_follows, tags, is_local, case
                    if ldoc_comment then
                       comment = table.concat(comment)
          -
          +            if comment:match '^%s*$' then
          +               ldoc_comment = nil
          +            end
          +         end
          +         if ldoc_comment then
                       if first_comment then
                          first_comment = false
                       else
          @@ -331,7 +364,18 @@ local function parse_file(fname, lang, package, args)
                          end
                       end
                       if is_local or tags['local'] then
          -               tags['local'] = true
          +               tags:add('local',true)
          +            end
          +            -- support for standalone fields/properties of classes/modules
          +            if (tags.field or tags.param) and not tags.class then
          +               -- the hack is to take a subfield and pull out its name,
          +               -- (see Tag:add above) but let the subfield itself go through
          +               -- with any modifiers.
          +               local fp = tags.field or tags.param
          +               if type(fp) == 'table' then fp = fp[1] end
          +               fp = tools.extract_identifier(fp)
          +               tags:add('name',fp)
          +               tags:add('class','field')
                       end
                       if tags.name then
                          current_item = F:new_item(tags,line)
          diff --git a/ldoc/prettify.lua b/ldoc/prettify.lua
          index 17a9edf6..c3ea3d5c 100644
          --- a/ldoc/prettify.lua
          +++ b/ldoc/prettify.lua
          @@ -4,9 +4,7 @@
           -- A module reference to an example `test-fun.lua` would look like
           -- `@{example:test-fun}`.
           local List = require 'pl.List'
          -local lexer = require 'ldoc.lexer'
           local globals = require 'ldoc.builtin.globals'
          -local tnext = lexer.skipws
           local prettify = {}
           
           local escaped_chars = {
          @@ -24,16 +22,25 @@ local function span(t,val)
              return ('%s'):format(t,val)
           end
           
          -local spans = {keyword=true,number=true,string=true,comment=true,global=true}
          +local spans = {keyword=true,number=true,string=true,comment=true,global=true,backtick=true}
          +
          +local cpp_lang = {c = true, cpp = true, cxx = true, h = true}
          +
          +function prettify.lua (lang, fname, code, initial_lineno, pre)
          +   local res, lexer, tokenizer = List(), require 'ldoc.lexer'
          +   local tnext = lexer.skipws
          +   if not cpp_lang[lang] then
          +      tokenizer = lexer.lua
          +   else
          +      tokenizer = lexer.cpp
          +   end
           
          -function prettify.lua (fname, code, initial_lineno, pre)
          -   local res = List()
              if pre then
                 res:append '
          \n'
              end
              initial_lineno = initial_lineno or 0
           
          -   local tok = lexer.lua(code,{},{})
          +   local tok = tokenizer(code,{},{})
              local error_reporter = {
                 warning = function (self,msg)
                    io.stderr:write(fname..':'..tok:lineno()+initial_lineno..': '..msg,'\n')
          @@ -47,7 +54,7 @@ function prettify.lua (fname, code, initial_lineno, pre)
                    t = 'global'
                 end
                 if spans[t] then
          -         if t == 'comment' then -- may contain @{ref}
          +         if t == 'comment' or t == 'backtick' then -- may contain @{ref} or `..`
                       val = prettify.resolve_inline_references(val,error_reporter)
                    end
                    res:append(span(t,val))
          @@ -72,7 +79,7 @@ local lxsh_highlighers = {bib=true,c=true,lua=true,sh=true}
           
           function prettify.code (lang,fname,code,initial_lineno,pre)
              if not lxsh then
          -      return prettify.lua (fname, code, initial_lineno, pre)
          +      return prettify.lua (lang,fname, code, initial_lineno, pre)
              else
                 if not lxsh_highlighers[lang] then
                    lang = 'lua'
          @@ -82,7 +89,7 @@ function prettify.code (lang,fname,code,initial_lineno,pre)
                    external = true
                 })
                 if not pre then
          -         code = code:gsub("^(.*)
          $", '%1') + code = code:gsub("^(.-)%s*
          $", '%1') end return code end diff --git a/ldoc/project.ldoc.mode b/ldoc/project.ldoc.mode new file mode 100644 index 00000000..dae53bfc --- /dev/null +++ b/ldoc/project.ldoc.mode @@ -0,0 +1,2 @@ +mode=lua +tabs=use=false,size=3 diff --git a/ldoc/tools.lua b/ldoc/tools.lua index 34d014e3..0de2672e 100644 --- a/ldoc/tools.lua +++ b/ldoc/tools.lua @@ -16,22 +16,30 @@ local lexer = require 'ldoc.lexer' local quit = utils.quit local lfs = require 'lfs' +-- at rendering time, can access the ldoc table from any module item, +-- or the item itself if it's a module +function M.item_ldoc (item) + local mod = item and (item.module or item) + return mod and mod.ldoc +end + -- this constructs an iterator over a list of objects which returns only -- those objects where a field has a certain value. It's used to iterate --- only over functions or tables, etc. +-- only over functions or tables, etc. If the list of item has a module +-- with a context, then use that to pre-sort the fltered items. -- (something rather similar exists in LuaDoc) function M.type_iterator (list,field,value) return function() - local i = 1 - return function() - local val = list[i] - while val and val[field] ~= value do - i = i + 1 - val = list[i] - end - i = i + 1 - if val then return val end + local fls = list:filter(function(item) + return item[field] == value + end) + local ldoc = M.item_ldoc(fls[1]) + if ldoc and ldoc.sort then + fls:sort(function(ia,ib) + return ia.name < ib.name + end) end + return fls:iter() end end @@ -129,7 +137,6 @@ function KindMap.add_kind (klass,tag,kind,subnames,item) end end - ----- some useful utility functions ------ function M.module_basepath() @@ -143,28 +150,19 @@ function M.module_basepath() end -- split a qualified name into the module part and the name part, --- e.g 'pl.utils.split' becomes 'pl.utils' and 'split' +-- e.g 'pl.utils.split' becomes 'pl.utils' and 'split'. Also +-- must understand colon notation! function M.split_dotted_name (s) - local s1,s2 = path.splitext(s) - if s2=='' then return nil - else return s1,s2:sub(2) - end -end - --- expand lists of possibly qualified identifiers --- given something like {'one , two.2','three.drei.drie)'} --- it will output {"one","two.2","three.drei.drie"} -function M.expand_comma_list (ls) - local new_ls = List() - for s in ls:iter() do - s = s:gsub('[^%.:%-%w_]*$','') - if s:find ',' then - new_ls:extend(List.split(s,'%s*,%s*')) - else - new_ls:append(s) - end + local s1,s2 = s:match '^(.+)[%.:](.+)$' + if s1 then -- we can split + return s1,s2 + else + return nil end - return new_ls +--~ local s1,s2 = path.splitext(s) +--~ if s2=='' then return nil +--~ else return s1,s2:sub(2) +--~ end end -- grab lines from a line iterator `iter` until the line matches the pattern. @@ -186,6 +184,19 @@ function M.extract_identifier (value) return value:match('([%.:%-_%w]+)(.*)$') end +function M.identifier_list (ls) + local ns = List() + if type(ls) == 'string' then ls = List{ns} end + for s in ls:iter() do + if s:match ',' then + ns:extend(List.split(s,'[,%s]+')) + else + ns:append(s) + end + end + return ns +end + function M.strip (s) return s:gsub('^%s+',''):gsub('%s+$','') end @@ -285,16 +296,26 @@ local function value_of (tok) return tok[2] end -- following the arguments. ldoc will use these in addition to explicit -- param tags. -function M.get_parameters (tok,endtoken,delim) +function M.get_parameters (tok,endtoken,delim,lang) tok = M.space_skip_getter(tok) local args = List() args.comments = {} - local ltl = lexer.get_separated_list(tok,endtoken,delim) + local ltl,tt = lexer.get_separated_list(tok,endtoken,delim) if not ltl or not ltl[1] or #ltl[1] == 0 then return args end -- no arguments - local function strip_comment (text) - return text:match("%s*%-%-+%s*(.*)") + local strip_comment, extract_arg + + if lang then + strip_comment = utils.bind1(lang.trim_comment,lang) + extract_arg = utils.bind1(lang.extract_arg,lang) + else + strip_comment = function(text) + return text:match("%s*%-%-+%s*(.*)") + end + extract_arg = function(tl,idx) + return value_of(tl[idx or 1]) + end end local function set_comment (idx,tok) @@ -308,6 +329,15 @@ function M.get_parameters (tok,endtoken,delim) args.comments[arg] = text end + local function add_arg (tl,idx) + local name, type = extract_arg(tl,idx) + args:append(name) + if type then + if not args.types then args.types = List() end + args.types:append(type) + end + end + for i = 1,#ltl do local tl = ltl[i] -- token list for argument if #tl > 0 then @@ -324,10 +354,10 @@ function M.get_parameters (tok,endtoken,delim) j = j + 1 end if #tl > 1 then - args:append(value_of(tl[j])) + add_arg(tl,j) end else - args:append(value_of(tl[1])) + add_arg(tl,1) end if i == #ltl and #tl > 1 then while j <= #tl and type_of(tl[j]) ~= 'comment' do @@ -344,7 +374,6 @@ function M.get_parameters (tok,endtoken,delim) end end - ----[[ -- we had argument comments -- but the last one may be outside the parens! (Geoff style) -- (only try this stunt if it's a function parameter list!) @@ -353,23 +382,25 @@ function M.get_parameters (tok,endtoken,delim) local last_arg = args[n] if not args.comments[last_arg] then while true do - local t = {tok()} - if type_of(t) == 'comment' then - set_comment(n,t) + tt = {tok()} + if type_of(tt) == 'comment' then + set_comment(n,tt) else break end end end end - --]] - return args + -- return what token we ended on as well - can be token _past_ ')' + return args,tt[1],tt[2] end --- parse a Lua identifier - contains names separated by . and :. -function M.get_fun_name (tok,first) +-- parse a Lua identifier - contains names separated by . and (optionally) :. +-- Set `colon` to be the secondary separator, '' for none. +function M.get_fun_name (tok,first,colon) local res = {} local t,name,sep + colon = colon or ':' if not first then t,name = tnext(tok) else @@ -377,7 +408,7 @@ function M.get_fun_name (tok,first) end if t ~= 'iden' then return nil end t,sep = tnext(tok) - while sep == '.' or sep == ':' do + while sep == '.' or sep == colon do append(res,name) append(res,sep) t,name = tnext(tok) @@ -433,24 +464,33 @@ end function M.process_file_list (list, mask, operation, ...) local exclude_list = list.exclude and M.files_from_list(list.exclude, mask) - local function process (f,...) + local files = List() + local function process (f) f = M.abspath(f) if not exclude_list or exclude_list and exclude_list:index(f) == nil then - operation(f, ...) + files:append(f) end end for _,f in ipairs(list) do if path.isdir(f) then - local files = List(dir.getallfiles(f,mask)) - for f in files:iter() do - process(f,...) + local dfiles = List(dir.getallfiles(f,mask)) + for f in dfiles:iter() do + process(f) end elseif path.isfile(f) then - process(f,...) + process(f) else quit("file or directory does not exist: "..M.quote(f)) end end + + if list.sortfn then + files:sort(list.sortfn) + end + + for f in files:iter() do + operation(f,...) + end end function M.files_from_list (list, mask) diff --git a/readme.md b/readme.md index 3d6c378c..35e9c2b5 100644 --- a/readme.md +++ b/readme.md @@ -11,7 +11,7 @@ with LuaDoc) and depends on Penlight itself.(This allowed me to _not_ write a lo The [API documentation](http://stevedonovan.github.com/Penlight/api/index.html) of Penlight is an example of a project using plain LuaDoc markup processed using LDoc. -LDoc is intended to be compatible with [LuaDoc](http://luadoc.luaforge.net/manual.htm) and +LDoc is intended to be compatible with [LuaDoc](http://keplerproject.github.io/luadoc/) and thus follows the pattern set by the various *Doc tools: --- Summary ends with a period. diff --git a/tests/easy/easy.lua b/tests/easy/easy.lua index 63e39687..00dca974 100644 --- a/tests/easy/easy.lua +++ b/tests/easy/easy.lua @@ -1,4 +1,5 @@ ---- simplified LDoc style +--- simplified LDoc colon style. +-- You have to use -C flag or 'colon=true' for this one! module 'easy' --- First one. diff --git a/tests/moonscript/List.moon b/tests/moonscript/List.moon new file mode 100644 index 00000000..bd1272e9 --- /dev/null +++ b/tests/moonscript/List.moon @@ -0,0 +1,65 @@ +---- +-- A list class that wraps a table +-- @classmod List +import insert,concat,remove from table + +class List + --- constructor passed a table `t`, which can be `nil`. + new: (t) => + @ls = t or {} + + --- append to list. + add: (item) => + insert @ls,item + + --- insert `item` at `idx` + insert: (idx,item) => + insert @ls,idx,item + + --- remove item at `idx` + remove: (idx) => remove @ls,idx + + --- length of list + len: => #@ls + + --- string representation + __tostring: => '['..(concat @ls,',')..']' + + --- return idx of first occurence of `item` + find: (item) => + for i = 1,#@ls + if @ls[i] == item then return i + + --- remove item by value + remove_value: (item) => + idx = self\find item + self\remove idx if idx + + --- remove a list of items + remove_values: (items) => + for item in *items do self\remove_value item + + --- create a sublist of items indexed by a table `indexes` + index_by: (indexes) => + List [@ls[idx] for idx in *indexes] + + --- make a copy of this list + copy: => List [v for v in *@ls] + + --- append items from the table or list `list` + extend: (list) => + other = if list.__class == List then list.ls else list + for v in *other do self\add v + self + + --- concatenate two lists, giving a new list + __concat: (l1,l2) -> l1\copy!\extend l2 + + --- an iterator over all items + iter: => + i,t,n = 0,@ls,#@ls + -> + i += 1 + if i <= n then t[i] + +return List diff --git a/tests/moonscript/config.ld b/tests/moonscript/config.ld new file mode 100644 index 00000000..e92d36ae --- /dev/null +++ b/tests/moonscript/config.ld @@ -0,0 +1,5 @@ +file = 'list.moon' +no_return_or_parms=true +no_summary=true +format = 'markdown' +--sort = true diff --git a/tests/styles/colon.lua b/tests/styles/colon.lua index ea72ead6..17912576 100644 --- a/tests/styles/colon.lua +++ b/tests/styles/colon.lua @@ -11,15 +11,22 @@ --- first useless function. -- Optional type specifiers are allowed in this format. --- As an extension, '?' is short for '?|'. +-- As an extension, '?T' is short for '?nil|T'. -- Note how these types are rendered! -- string: name -- int: age --- ?person2: options +-- ?person3: options -- treturn: ?table|string function one (name,age,options) end +--- implicit table can always use colon notation. +person2 = { + id=true, -- string: official ID number + sex=true, -- string: one of 'M', 'F' or 'N' + spouse=true, -- ?person3: wife or husband +} + --- explicit table in colon format. -- Note how '!' lets you use a type name directly. -- string: surname diff --git a/tests/styles/config/config.ld b/tests/styles/config/config.ld new file mode 100644 index 00000000..239ae21d --- /dev/null +++ b/tests/styles/config/config.ld @@ -0,0 +1,2 @@ +no_return_or_parms=true +no_summary=true diff --git a/tests/styles/four.lua b/tests/styles/four.lua index 02b78964..4c3f2906 100644 --- a/tests/styles/four.lua +++ b/tests/styles/four.lua @@ -1,8 +1,8 @@ ------------ -- Yet another module. --- @module four -- Description can continue after simple tags, if you --- like +-- like - but to keep backwards compatibility, say 'not_luadoc=true' +-- @module four -- @author bob, james -- @license MIT -- @copyright InfoReich 2013 @@ -11,7 +11,7 @@ -- Note the the standard tparam aliases, and how the 'opt' and 'optchain' -- modifiers may also be used. If the Lua function has varargs, then -- you may document an indefinite number of extra arguments! --- @string name person's name +-- @tparam ?string|Person name person's name -- @int age -- @string[opt='gregorian'] calender optional calendar -- @int[opt=0] offset optional offset @@ -27,7 +27,6 @@ end function two (one,two,three,four) end - --- third useless function. -- Can always put comments inline, may -- be multiple. @@ -38,6 +37,11 @@ function three ( -- person: -- not less than zero! ) +---- function with single optional arg +-- @param[opt] one +function four (one) +end + --- an implicit table. -- Again, we can use the comments person = { diff --git a/tests/styles/multiple.lua b/tests/styles/multiple.lua new file mode 100644 index 00000000..85e12b99 --- /dev/null +++ b/tests/styles/multiple.lua @@ -0,0 +1,45 @@ +------ +-- Various ways of indicating errors +-- @module multiple + +----- +-- function with return groups. +-- @treturn[1] string result +-- @return[2] nil +-- @return[2] error message +function mul1 () end + +----- +-- function with return and error tag +-- @return result +-- @error message +function mul2 () end + +----- +-- function with multiple error tags +-- @return result +-- @error not found +-- @error bad format +function mul3 () end + +---- +-- function with inline return and errors +-- @string name +function mul4 (name) + if type(name) ~= 'string' then + --- @error[1] not a string + return nil, 'not a string' + end + if #name == 0 then + --- @error[2] zero-length string + return nil, 'zero-length string' + end + --- @treturn string converted to uppercase + return name:upper() +end +----- +-- function that raises an error. +-- @string filename +-- @treturn string result +-- @raise 'file not found' +function mul5(filename) end diff --git a/tests/styles/one.lua b/tests/styles/one.lua index d3b1edac..30e3145f 100644 --- a/tests/styles/one.lua +++ b/tests/styles/one.lua @@ -2,7 +2,9 @@ A non-doc comment multi-line probably containing license information! -Doesn't use module(), but module name is inferred from file name +Doesn't use module(), but module name is inferred from file name. +If you have initial licence comments that look like doc comments, +then set `boilerplate=true` ]] ------------ -- Test module, diff --git a/tests/styles/priority_queue.lua b/tests/styles/priority_queue.lua index cd8c70b7..9dcc5ad5 100644 --- a/tests/styles/priority_queue.lua +++ b/tests/styles/priority_queue.lua @@ -1,7 +1,10 @@ -------------------------------------------------------------------------------- ---- Queue of objects sorted by priority +--- Queue of objects sorted by priority. -- @module lua-nucleo.priority_queue --- This file is a part of lua-nucleo library +-- This file is a part of lua-nucleo library. Note that if you wish to spread +-- the description after tags, then invoke with `not_luadoc=true`. +-- The flags here are `ldoc -X -f backtick priority_queue.lua`, which +-- also expands backticks. -- @copyright lua-nucleo authors (see file `COPYRIGHT` for the license) -------------------------------------------------------------------------------- @@ -34,7 +37,7 @@ do local insert = function(self, priority, value) method_arguments( - self, + s "number", priority ) assert(value ~= nil, "value can't be nil") -- value may be of any type, except nil @@ -77,7 +80,7 @@ do return front_elem[PRIORITY_KEY], front_elem[VALUE_KEY] end - --- construct a @{priority_queue}. + --- construct a `priority_queue`. -- @constructor make_priority_queue = function() --- @export diff --git a/tests/styles/struct.lua b/tests/styles/struct.lua new file mode 100644 index 00000000..c28689a1 --- /dev/null +++ b/tests/styles/struct.lua @@ -0,0 +1,13 @@ +------ +-- functions returning compound types +-- @module struct + +----- +-- returns a 'struct'. +-- @string name your name dammit +-- @tfield string arb stuff +-- @treturn st details of person +-- @tfield[st] string name of person +-- @tfield[st] int age of person +function struct(name) end + diff --git a/tests/styles/three.lua b/tests/styles/three.lua index a782358d..09c0ceb1 100644 --- a/tests/styles/three.lua +++ b/tests/styles/three.lua @@ -3,6 +3,10 @@ -- Description here ---- +--- documented, but private +local function question () +end + --- answer to everything. -- @return magic number local function answer () diff --git a/tests/styles/type.lua b/tests/styles/type.lua new file mode 100644 index 00000000..8499dc8e --- /dev/null +++ b/tests/styles/type.lua @@ -0,0 +1,45 @@ +----- +-- module containing a class +-- @module type + +---- +-- Our class. +-- @type Bonzo + +---- +-- make a new Bonzo. +-- @see Bonzo:dog +-- @string s name of Bonzo +function Bonzo.new(s) +end + +----- +-- get a string representation. +-- works with `tostring` +function Bonzo:__tostring() +end + +---- +-- Another method. +function Bonzo:dog () + +end + +---- +-- Private method. +-- You need -a flag or 'all=true' to see these +-- @local +function Bonzo:cat () + +end + + +---- +-- A subtable with fields. +-- @table Details +-- @string[readonly] name +-- @int[readonly] age + +--- +-- This is a simple field/property of the class. +-- @string[opt="Bilbo",readonly] frodo direct access to text diff --git a/tests/usage/config.ld b/tests/usage/config.ld index 85ff293b..f33e4ba2 100644 --- a/tests/usage/config.ld +++ b/tests/usage/config.ld @@ -11,3 +11,5 @@ pretty='lxsh' -- suppress @params and the summary at the top no_return_or_parms=true no_summary=true +not_luadoc=true + diff --git a/tests/usage/usage.lua b/tests/usage/usage.lua index 21f6042e..ba05136d 100644 --- a/tests/usage/usage.lua +++ b/tests/usage/usage.lua @@ -70,13 +70,12 @@ end --[[----------------- @table Vector.Opts -Options table format for `Vector.options` +Options table format for `Vector:options` - autoconvert: try to convert strings to numbers + * `autoconvert`: try to convert strings to numbers + * `adder`: function used to perform addition and subtraction + * `multiplier`: function used to perform multiplication and division - adder: function used to perform addition and subtraction - - multiplier: function used to perform multiplication and division @usage v = Vector {{1},{2}} v:options {adder = function(x,y) return {x[1]+y[1]} end}