diff --git a/.github/workflows/docmaker-ci.yml b/.github/workflows/docmaker-ci.yml
index bb2dba6..34790b6 100644
--- a/.github/workflows/docmaker-ci.yml
+++ b/.github/workflows/docmaker-ci.yml
@@ -32,6 +32,10 @@ jobs:
Xvfb :99 &
echo "DISPLAY=:99" >> $GITHUB_ENV
+ - name: Install cmark-gfm on Linux
+ if: ${{ matrix.platform == 'ubuntu-latest' }}
+ run: sudo apt-get update && sudo apt-get install -y cmark-gfm
+
- name: Set up MATLAB on the runner
uses: matlab-actions/setup-matlab@v3
with:
diff --git a/README.md b/README.md
index 1de8916..022d176 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,11 @@ Finally, configure [Markdown conversion](./tbx/docmakerdoc/conversion.md) by gen
DocMaker is developed by [David Sampson](https://www.mathworks.com/matlabcentral/profile/authors/16247) from [MathWorks](https://www.mathworks.com/services/consulting.html) on [GitHub](https://github.com/mathworks/docmaker).
-DocMaker uses the [GitHub Markdown API](https://docs.github.com/en/rest/markdown) or the [GitLab Markdown API](https://docs.gitlab.com/api/markdown/) for converting Markdown to HTML, and the [GitHub Markdown CSS](https://github.com/sindresorhus/github-markdown-css) by [Sindre Sorhus](https://github.com/sindresorhus) for HTML styling.
+DocMaker uses the [GitHub Markdown API](https://docs.github.com/en/rest/markdown),
+the [GitLab Markdown API](https://docs.gitlab.com/api/markdown/), or local
+[cmark-gfm](https://github.com/github/cmark-gfm) for converting Markdown to
+HTML. It uses the [GitHub Markdown CSS](https://github.com/sindresorhus/github-markdown-css)
+by [Sindre Sorhus](https://github.com/sindresorhus) for HTML styling.
The author thanks [Ken Deeley](https://www.mathworks.com/matlabcentral/profile/authors/2987674) from MathWorks for his support during the development of DocMaker.
diff --git a/buildfile.m b/buildfile.m
index 40119a6..19d9c93 100644
--- a/buildfile.m
+++ b/buildfile.m
@@ -83,6 +83,11 @@ function packageTask( ~ )
s.ToolboxName = v.Name;
s.ToolboxVersion = v.Version;
+% Specify the package name from R2026b
+if ~isMATLABReleaseOlderThan( "R2026b" )
+ s.PackageName = s.Name;
+end % if
+
if getenv( "GITHUB_ACTIONS" ) == "true"
% Check version and tag compatibility for release
ref = string( getenv( "GITHUB_REF" ) );
diff --git a/docmaker.json b/docmaker.json
index a40dc0a..0979880 100644
--- a/docmaker.json
+++ b/docmaker.json
@@ -1,4 +1,5 @@
{
+ "Name":"MATLAB_DocMaker",
"Identifier":"ae82d5d2-45be-4fc5-a01b-70d8bb905ca2",
"Summary":"Create MATLAB toolbox documentation from Markdown documents and MATLAB scripts",
"Description":"With DocMaker, you can:\n* write documentation in Markdown and convert to HTML for viewing in MATLAB\n* run MATLAB scripts and capture output as images for inclusion in documentation\n* create MATLAB documentation index files from a Markdown table of contents",
diff --git a/resources/project/UMvhTnfaEKsbCRaOOLl1x86F4Is/0ikZNnfq8Omj89uI9ZrkWUPmJAsd.xml b/resources/project/UMvhTnfaEKsbCRaOOLl1x86F4Is/0ikZNnfq8Omj89uI9ZrkWUPmJAsd.xml
new file mode 100644
index 0000000..99772b4
--- /dev/null
+++ b/resources/project/UMvhTnfaEKsbCRaOOLl1x86F4Is/0ikZNnfq8Omj89uI9ZrkWUPmJAsd.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/project/UMvhTnfaEKsbCRaOOLl1x86F4Is/0ikZNnfq8Omj89uI9ZrkWUPmJAsp.xml b/resources/project/UMvhTnfaEKsbCRaOOLl1x86F4Is/0ikZNnfq8Omj89uI9ZrkWUPmJAsp.xml
new file mode 100644
index 0000000..041260f
--- /dev/null
+++ b/resources/project/UMvhTnfaEKsbCRaOOLl1x86F4Is/0ikZNnfq8Omj89uI9ZrkWUPmJAsp.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/76Ysw5dsZobh4SitR55uuJz41y0d.xml b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/76Ysw5dsZobh4SitR55uuJz41y0d.xml
new file mode 100644
index 0000000..4356a6a
--- /dev/null
+++ b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/76Ysw5dsZobh4SitR55uuJz41y0d.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/76Ysw5dsZobh4SitR55uuJz41y0p.xml b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/76Ysw5dsZobh4SitR55uuJz41y0p.xml
new file mode 100644
index 0000000..01cb34e
--- /dev/null
+++ b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/76Ysw5dsZobh4SitR55uuJz41y0p.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/mHFG09DfonBnLdBP-i8fjH-c5Ucd.xml b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/mHFG09DfonBnLdBP-i8fjH-c5Ucd.xml
new file mode 100644
index 0000000..378b613
--- /dev/null
+++ b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/mHFG09DfonBnLdBP-i8fjH-c5Ucd.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/mHFG09DfonBnLdBP-i8fjH-c5Ucp.xml b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/mHFG09DfonBnLdBP-i8fjH-c5Ucp.xml
new file mode 100644
index 0000000..770714f
--- /dev/null
+++ b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/mHFG09DfonBnLdBP-i8fjH-c5Ucp.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/z1RViax6Ofaoj4VSrBPZcC0V2lgd.xml b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/z1RViax6Ofaoj4VSrBPZcC0V2lgd.xml
new file mode 100644
index 0000000..378b613
--- /dev/null
+++ b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/z1RViax6Ofaoj4VSrBPZcC0V2lgd.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/z1RViax6Ofaoj4VSrBPZcC0V2lgp.xml b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/z1RViax6Ofaoj4VSrBPZcC0V2lgp.xml
new file mode 100644
index 0000000..c879c59
--- /dev/null
+++ b/resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/z1RViax6Ofaoj4VSrBPZcC0V2lgp.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/hLMKfvmMZYFbdoE4Xco7ryXas6s/alNvJCF7XFIrHIM8Qu5C9uu3RZMd.xml b/resources/project/hLMKfvmMZYFbdoE4Xco7ryXas6s/alNvJCF7XFIrHIM8Qu5C9uu3RZMd.xml
new file mode 100644
index 0000000..4356a6a
--- /dev/null
+++ b/resources/project/hLMKfvmMZYFbdoE4Xco7ryXas6s/alNvJCF7XFIrHIM8Qu5C9uu3RZMd.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/hLMKfvmMZYFbdoE4Xco7ryXas6s/alNvJCF7XFIrHIM8Qu5C9uu3RZMp.xml b/resources/project/hLMKfvmMZYFbdoE4Xco7ryXas6s/alNvJCF7XFIrHIM8Qu5C9uu3RZMp.xml
new file mode 100644
index 0000000..cf5148d
--- /dev/null
+++ b/resources/project/hLMKfvmMZYFbdoE4Xco7ryXas6s/alNvJCF7XFIrHIM8Qu5C9uu3RZMp.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/tbx/docmaker/+docmaker/GitHub.m b/tbx/docmaker/+docmaker/GitHub.m
index 870b1c9..e7683fb 100644
--- a/tbx/docmaker/+docmaker/GitHub.m
+++ b/tbx/docmaker/+docmaker/GitHub.m
@@ -32,7 +32,7 @@
end
obj.Hostname = hostname;
- obj.Token_ = token;
+ obj.Token_ = docmaker.MarkdownConverter.normalizeToken( token );
end % constructor
@@ -95,4 +95,4 @@
end % methods
-end % classdef
\ No newline at end of file
+end % classdef
diff --git a/tbx/docmaker/+docmaker/GitLab.m b/tbx/docmaker/+docmaker/GitLab.m
index c28fd82..6b83669 100644
--- a/tbx/docmaker/+docmaker/GitLab.m
+++ b/tbx/docmaker/+docmaker/GitLab.m
@@ -32,7 +32,7 @@
end
obj.Hostname = hostname;
- obj.Token_ = token;
+ obj.Token_ = docmaker.MarkdownConverter.normalizeToken( token );
end % constructor
diff --git a/tbx/docmaker/+docmaker/LocalCmark.m b/tbx/docmaker/+docmaker/LocalCmark.m
new file mode 100644
index 0000000..92671c1
--- /dev/null
+++ b/tbx/docmaker/+docmaker/LocalCmark.m
@@ -0,0 +1,100 @@
+classdef LocalCmark < docmaker.MarkdownConverter
+ %docmaker.LocalCmark Local cmark-gfm Markdown converter
+ %
+ % docmaker.LocalCmark uses the local cmark-gfm binary to convert
+ % Markdown to HTML without making network calls.
+ %
+ % c = docmaker.LocalCmark() creates a converter using cmark-gfm on
+ % the system path.
+ %
+ % c = docmaker.LocalCmark(b) uses the binary at path b.
+
+ % Copyright 2026 The MathWorks, Inc.
+
+ properties ( SetAccess = immutable )
+ Binary(1, 1) string
+ end % properties ( SetAccess = immutable )
+
+ methods
+
+ function obj = LocalCmark( binary )
+ %LocalCmark Local cmark-gfm Markdown converter
+
+ arguments ( Input )
+ binary(1, 1) string = "cmark-gfm"
+ end % arguments ( Input )
+
+ validateBinary( binary )
+ obj.Binary = binary;
+
+ end % constructor
+
+ function doc = md2xml( obj, md )
+ %md2xml Convert Markdown to XML
+ %
+ % x = md2xml(c,md) converts the Markdown md to the XML
+ % document x using the local cmark-gfm binary.
+
+ arguments ( Input )
+ obj(1, 1) docmaker.LocalCmark
+ md(1, 1) string
+ end % arguments ( Input )
+
+ % Write Markdown content to a temporary file.
+ tmpIn = tempname() + ".md";
+ cleaner = onCleanup( @() cleanupTempFile( tmpIn ) );
+ writelines( md, tmpIn, "Encoding", "UTF-8" );
+
+ % Invoke the binary on the temporary file.
+ command = sprintf( [ ...
+ '"%s" --extension table --extension strikethrough ' ...
+ '--extension autolink --extension tasklist ' ...
+ '--extension tagfilter --to html "%s" 2>&1' ], ...
+ obj.Binary, tmpIn );
+ [status, output] = system( command );
+ xml = strip( string( output ) );
+
+ if status ~= 0
+ if xml == ""
+ xml = "No diagnostic output was returned.";
+ end
+ error( "docmaker:LocalCmark:ProcessFailed", ...
+ "Unable to run cmark-gfm binary ""%s"" " + ...
+ "(status %d): %s", ...
+ obj.Binary, status, xml )
+ end % if
+
+ % Close tags and parse.
+ xml = docmaker.closetags( xml, "img" );
+ xml = docmaker.closetags( xml, "hr" );
+ xml = docmaker.closetags( xml, "br" );
+ parser = matlab.io.xml.dom.Parser();
+ doc = parser.parseString( "
" + xml + "
" );
+ doc.XMLStandalone = true;
+
+ end % md2xml
+
+ end % methods
+
+end % classdef
+
+function validateBinary( binary )
+%validateBinary Reject binary values unsafe for shell invocation.
+
+unsafeCharacters = ["""", "$", "`", "&", "|", "<", ">", "^", ...
+ "%", "!", ";", newline, string( char( 13 ) )];
+if any( contains( binary, unsafeCharacters ) )
+ error( "docmaker:LocalCmark:InvalidBinary", ...
+ "Binary must not contain shell metacharacters." )
+end % if
+
+end % validateBinary
+
+function cleanupTempFile( filePath )
+%cleanupTempFile Delete a temporary file when it exists.
+
+if isfile( filePath )
+ delete( filePath )
+end % if
+
+end % cleanupTempFile
diff --git a/tbx/docmaker/+docmaker/MarkdownConverter.m b/tbx/docmaker/+docmaker/MarkdownConverter.m
index ef2a473..ddfa0d3 100644
--- a/tbx/docmaker/+docmaker/MarkdownConverter.m
+++ b/tbx/docmaker/+docmaker/MarkdownConverter.m
@@ -10,4 +10,21 @@
xml = md2xml( md )
end
-end % classdef
\ No newline at end of file
+ methods ( Static, Access = protected )
+
+ function token = normalizeToken( token )
+ %normalizeToken Normalize an access token
+
+ arguments
+ token(1, 1) string
+ end
+
+ if ~ismissing( token ) && strlength( token ) == 0
+ token = missing;
+ end
+
+ end % normalizeToken
+
+ end % methods ( Access = protected )
+
+end % classdef
diff --git a/tbx/docmaker/+docmaker/converter.m b/tbx/docmaker/+docmaker/converter.m
index 36b35cb..88cbc40 100644
--- a/tbx/docmaker/+docmaker/converter.m
+++ b/tbx/docmaker/+docmaker/converter.m
@@ -3,14 +3,16 @@
%
% c = docmaker.converter() returns the current Markdown converter.
%
-% The converter can be "GitHub" (default) or "GitLab" and is controlled
-% by the environment variable DOCMAKER_CONVERTER, the secret "DocMaker
-% converter" or the preference "converter".
+% The converter can be "GitHub" (default), "GitLab", or "LocalCmark" and
+% is controlled by the environment variable DOCMAKER_CONVERTER, the
+% secret "DocMaker converter" or the preference "converter".
%
% The GitHub hostname and token are stored in DOCMAKER_GITHUB_HOSTNAME
% and DOCMAKER_GITHUB_TOKEN, "DocMaker GitHub hostname" and "DocMaker
% GitHub token", or "github_hostname" and "github_token". The same naming
-% convention applies to GitLab.
+% convention applies to GitLab. The LocalCmark binary is configured by
+% DOCMAKER_LOCALCMARK_BINARY, "DocMaker LocalCmark binary", or
+% "localcmark_binary".
% Copyright 2024-2026 The MathWorks, Inc.
@@ -18,12 +20,15 @@
switch lower( n )
case "github"
h = getValue( "GitHub hostname", "api.github.com" );
- t = getValue( "GitHub token", "" );
+ t = getValue( "GitHub token", missing );
c = docmaker.GitHub( h, t );
case "gitlab"
h = getValue( "GitLab hostname", "www.gitlab.com" );
- t = getValue( "GitLab token", "" );
+ t = getValue( "GitLab token", missing );
c = docmaker.GitLab( h, t );
+ case "localcmark"
+ b = getValue( "LocalCmark binary", "cmark-gfm" );
+ c = docmaker.LocalCmark( b );
otherwise
error( "docmaker:Unhandled", "Unknown converter ""%s"".", n )
end
@@ -74,4 +79,4 @@
tf = HAS_SECRETS; % retrieve
end
-end % hasSecrets
\ No newline at end of file
+end % hasSecrets
diff --git a/tbx/docmaker/Contents.m b/tbx/docmaker/Contents.m
index 07e7c55..bf00f1b 100644
--- a/tbx/docmaker/Contents.m
+++ b/tbx/docmaker/Contents.m
@@ -1,9 +1,10 @@
% MATLAB DocMaker
-% Version 0.8.0 (R2026b) 15-Oct-2026
+% Version 0.8.1 (R2026b) 29-Oct-2026
%
-% docconvert - convert Markdown documents to HTML
-% docrun - run MATLAB code in HTML documents and insert output
-% docindex - create documentation index files
-% docdelete - delete generated artifacts
+% docconvert - convert Markdown documents to HTML
+% docrun - run MATLAB code in HTML documents and insert output
+% docindex - create documentation index files
+% docdelete - delete generated artifacts
+% DocMakerTask - generate documentation in a build file
% Copyright 2020-2026 The MathWorks, Inc.
diff --git a/tbx/docmaker/docconvert.m b/tbx/docmaker/docconvert.m
index 161f4fe..1bd4c60 100644
--- a/tbx/docmaker/docconvert.m
+++ b/tbx/docmaker/docconvert.m
@@ -288,12 +288,12 @@
end % relpath
function cleanLaTeXExpressions( fHTML )
-%CLEANLATEXEXPRESSIONS Postprocess LaTeX expressions after conversion to
-%HTML via the GitHub or GitLab APIs. We remove spurious HTML tags and add
-%delimiters to the expressions as needed.
+%CLEANLATEXEXPRESSIONS Postprocess LaTeX expressions after Markdown
+%conversion. We remove spurious HTML tags and add delimiters to the
+%expressions as needed.
%
-% For the GitHub API, we replace ... with _..._ inside $...$ or
-% $$...$$.
+% For GitHub and LocalCmark, we replace ... with _..._ inside
+% $...$ or $$...$$.
%
% Markdown italics and LaTeX subscripts both use an underscore, causing a
% conflict when Markdown is parsed with priority over LaTeX inside a LaTeX
@@ -321,6 +321,8 @@ function cleanLaTeXExpressions( fHTML )
switch converterType
case "GitHub"
removeItalicTags( fHTML )
+ case "LocalCmark"
+ removeItalicTags( fHTML )
case "GitLab"
wrapMathSpans( fHTML )
otherwise
@@ -410,4 +412,4 @@ function wrapMathSpans( fHTML )
fprintf( fileID, "%s", cleanHTML );
fclose( fileID );
-end % wrapMathSpans
\ No newline at end of file
+end % wrapMathSpans
diff --git a/tbx/docmakerdoc/about.md b/tbx/docmakerdoc/about.md
index 91b2bcc..86db396 100644
--- a/tbx/docmakerdoc/about.md
+++ b/tbx/docmakerdoc/about.md
@@ -4,7 +4,11 @@
DocMaker is developed by [David Sampson](https://www.mathworks.com/matlabcentral/profile/authors/16247) from [MathWorks](https://www.mathworks.com/services/consulting.html) on [GitHub](https://github.com/mathworks/docmaker).
-DocMaker uses the [GitHub Markdown API](https://docs.github.com/en/rest/markdown) or the [GitLab Markdown API](https://docs.gitlab.com/api/markdown/) for converting Markdown to HTML, and the [GitHub Markdown CSS](https://github.com/sindresorhus/github-markdown-css) by [Sindre Sorhus](https://github.com/sindresorhus) for HTML styling.
+DocMaker uses the [GitHub Markdown API](https://docs.github.com/en/rest/markdown),
+the [GitLab Markdown API](https://docs.gitlab.com/api/markdown/), or local
+[cmark-gfm](https://github.com/github/cmark-gfm) for converting Markdown to
+HTML. It uses the [GitHub Markdown CSS](https://github.com/sindresorhus/github-markdown-css)
+by [Sindre Sorhus](https://github.com/sindresorhus) for HTML styling.
The author thanks [Ken Deeley](https://www.mathworks.com/matlabcentral/profile/authors/2987674) from MathWorks for his support during the development of DocMaker.
@@ -18,4 +22,4 @@ DocMaker was developed to build documentation for [GUI Layout Toolbox](http://ww
___
-[home](index.md) :house: | :copyright: [MathWorks](https://www.mathworks.com/services/consulting.html) 2024-2026
\ No newline at end of file
+[home](index.md) :house: | :copyright: [MathWorks](https://www.mathworks.com/services/consulting.html) 2024-2026
diff --git a/tbx/docmakerdoc/changelog.md b/tbx/docmakerdoc/changelog.md
index 994aa74..e545060 100644
--- a/tbx/docmakerdoc/changelog.md
+++ b/tbx/docmakerdoc/changelog.md
@@ -1,5 +1,10 @@
# Release notes
+## Version 0.8.1
+
+* Added optional offline Markdown conversion with local [cmark-gfm](https://github.com/github/cmark-gfm)
+* Fixed empty GitHub and GitLab access token settings to send unauthenticated requests
+
## Version 0.8.0
* Added support for LaTeX expressions using [MathJax](https://www.mathjax.org/) via the `Interpreter="latex"` option in `docconvert`
@@ -23,4 +28,4 @@
___
-:copyright: [MathWorks](https://www.mathworks.com/services/consulting.html) 2024-2026
\ No newline at end of file
+:copyright: [MathWorks](https://www.mathworks.com/services/consulting.html) 2024-2026
diff --git a/tbx/docmakerdoc/conversion.md b/tbx/docmakerdoc/conversion.md
index 07413e1..8266f82 100644
--- a/tbx/docmakerdoc/conversion.md
+++ b/tbx/docmakerdoc/conversion.md
@@ -1,6 +1,7 @@
# Markdown conversion
-DocMaker supports conversion from Markdown to HTML using services from GitHub and GitLab.
+DocMaker supports conversion from Markdown to HTML using GitHub, GitLab, or
+a local cmark-gfm installation.
## GitHub
@@ -41,6 +42,30 @@ Requests to this service require [authentication](https://docs.gitlab.com/api/re
See the [GitLab API terms of use](https://handbook.gitlab.com/handbook/legal/api-terms/) and [privacy statement](https://about.gitlab.com/privacy/) for further information.
+## Local cmark-gfm
+
+To convert Markdown without making network calls, install
+[cmark-gfm](https://github.com/github/cmark-gfm) and select `LocalCmark`:
+
+```matlab
+setpref("docmaker","converter","LocalCmark")
+```
+
+The default binary name is `cmark-gfm`. To use another binary or an explicit
+path, set `DOCMAKER_LOCALCMARK_BINARY` or the `localcmark_binary` preference.
+
+Install cmark-gfm using your platform's supported package manager or release:
+
+* Debian/Ubuntu: `apt install cmark-gfm`
+* macOS: `brew install cmark-gfm`
+* Windows: download a cmark-gfm Windows release, then add its folder to
+ `PATH` or set `DOCMAKER_LOCALCMARK_BINARY` to its full path.
+
+LocalCmark uses the cmark-gfm table, strikethrough, autolink, task-list, and
+tag-filter extensions. It fails with an error if the configured binary is
+unavailable or does not support these options; it does not fall back to a
+remote converter.
+
## Example
You can configure your local machine to use your own private GitLab instance to convert Markdown to HTML with:
@@ -68,9 +93,10 @@ You can use various settings mechanisms to control the converter technology, ser
| GitHub access token | `DOCMAKER_GITHUB_TOKEN` | `DocMaker GitHub token` | `github_token` | |
| GitLab hostname | `DOCMAKER_GITLAB_HOSTNAME` | `DocMaker GitLab hostname` | `gitlab_hostname` | `gitlab.com` |
| GitLab access token | `DOCMAKER_GITLAB_TOKEN` | `DocMaker GitLab token` | `gitlab_token` | |
+| LocalCmark binary | `DOCMAKER_LOCALCMARK_BINARY` | `DocMaker LocalCmark binary` | `localcmark_binary` | `cmark-gfm` |
Use environment variables for build runners, secrets for tokens on local computers, and preferences for non-sensitive data on local computers. In case of conflict, environment variables trump secrets, which trump preferences.
___
-[home](index.md) :house: | :copyright: [MathWorks](https://www.mathworks.com/services/consulting.html) 2024-2026
\ No newline at end of file
+[home](index.md) :house: | :copyright: [MathWorks](https://www.mathworks.com/services/consulting.html) 2024-2026
diff --git a/tbx/docmakerdoc/index.md b/tbx/docmakerdoc/index.md
index ae4b155..f395fb9 100644
--- a/tbx/docmakerdoc/index.md
+++ b/tbx/docmakerdoc/index.md
@@ -15,7 +15,10 @@ This is DocMaker version 0.8. The [release notes](changelog.md) detail new feat
DocMaker requires [MATLAB](https://www.mathworks.com/support/requirements/matlab-system-requirements.html) R2021a or later to *generate* documentation.
-[`docconvert`](docconvert.md) and [`docindex`](docindex.md) require network access to a GitHub or GitLab instance to process Markdown documents and generate DocMaker artifacts.
+By default, [`docconvert`](docconvert.md) and [`docindex`](docindex.md)
+require network access to a GitHub or GitLab instance to process Markdown
+documents and generate DocMaker artifacts. You can configure local cmark-gfm
+conversion to generate these artifacts offline.
Documentation generated by DocMaker can be *viewed* in much older releases of MATLAB, and offline.
@@ -72,4 +75,4 @@ You can incorporate documentation into your software delivery pipeline by integr
___
-[home](index.md) :house: | [convert](docconvert.md) :arrow_right: | [run](docrun.md) :runner: | [index](docindex.md) :scroll: | [delete](docdelete.md) :recycle: | [workspace](workspace.md) :construction_worker: | [about](about.md) :hatching_chick: | :copyright: [MathWorks](https://www.mathworks.com/services/consulting.html) 2024-2026
\ No newline at end of file
+[home](index.md) :house: | [convert](docconvert.md) :arrow_right: | [run](docrun.md) :runner: | [index](docindex.md) :scroll: | [delete](docdelete.md) :recycle: | [workspace](workspace.md) :construction_worker: | [about](about.md) :hatching_chick: | :copyright: [MathWorks](https://www.mathworks.com/services/consulting.html) 2024-2026
diff --git a/tests/+docmaker/tLocalCmark.m b/tests/+docmaker/tLocalCmark.m
new file mode 100644
index 0000000..7881390
--- /dev/null
+++ b/tests/+docmaker/tLocalCmark.m
@@ -0,0 +1,133 @@
+classdef tLocalCmark < matlab.unittest.TestCase
+ %TLOCALCMARK Tests for the local cmark-gfm converter.
+
+ methods ( Test )
+
+ function tConstructorStoresBinary( testCase )
+
+ converter = docmaker.LocalCmark( "test-cmark" );
+
+ testCase.verifyEqual( converter.Binary, "test-cmark" )
+
+ end % tConstructorStoresBinary
+
+ function tConstructorRejectsShellMetacharacters( testCase )
+
+ testCase.verifyError( ...
+ @() docmaker.LocalCmark( "cmark-gfm & other-command" ), ...
+ "docmaker:LocalCmark:InvalidBinary" )
+
+ end % tConstructorRejectsShellMetacharacters
+
+ function tConverterUsesConfiguredBinary( testCase )
+
+ converterWasSet = isenv( "DOCMAKER_CONVERTER" );
+ binaryWasSet = isenv( "DOCMAKER_LOCALCMARK_BINARY" );
+ originalConverter = getenv( "DOCMAKER_CONVERTER" );
+ originalBinary = getenv( "DOCMAKER_LOCALCMARK_BINARY" );
+ cleanup = onCleanup( @() restoreEnvironment( ...
+ converterWasSet, originalConverter, ...
+ binaryWasSet, originalBinary ) );
+ setenv( "DOCMAKER_CONVERTER", "LocalCmark" )
+ setenv( "DOCMAKER_LOCALCMARK_BINARY", "test-cmark" )
+
+ converter = docmaker.converter();
+
+ testCase.verifyClass( converter, "docmaker.LocalCmark" )
+ testCase.verifyEqual( converter.Binary, "test-cmark" )
+
+ end % tConverterUsesConfiguredBinary
+
+ function tUnavailableBinaryFailsClearly( testCase )
+
+ converter = docmaker.LocalCmark( "missing-cmark-gfm-binary" );
+
+ testCase.verifyError( @() converter.md2xml( "# Heading" ), ...
+ "docmaker:LocalCmark:ProcessFailed" )
+
+ end % tUnavailableBinaryFailsClearly
+
+ function tConvertsGfmExtensions( testCase )
+
+ assumeCmarkAvailable( testCase )
+ converter = docmaker.LocalCmark();
+ markdown = ...
+ "# Heading" + newline + ...
+ "| A | B |" + newline + ...
+ "| - | - |" + newline + ...
+ "| 1 | 2 |" + newline + ...
+ "~~old~~ " + newline + ...
+ "- [ ] Task" + newline + ...
+ "";
+
+ document = converter.md2xml( markdown );
+ testCase.verifyTrue( hasElement( document, "table" ) )
+ testCase.verifyTrue( hasElement( document, "del" ) )
+ testCase.verifyTrue( hasElement( document, "input" ) )
+ testCase.verifyFalse( hasElement( document, "script" ) )
+
+ end % tConvertsGfmExtensions
+
+ function tDocconvertCleansLaTeXExpressions( testCase )
+
+ assumeCmarkAvailable( testCase )
+ converterWasSet = isenv( "DOCMAKER_CONVERTER" );
+ binaryWasSet = isenv( "DOCMAKER_LOCALCMARK_BINARY" );
+ originalConverter = getenv( "DOCMAKER_CONVERTER" );
+ originalBinary = getenv( "DOCMAKER_LOCALCMARK_BINARY" );
+ cleanup = onCleanup( @() restoreEnvironment( ...
+ converterWasSet, originalConverter, ...
+ binaryWasSet, originalBinary ) );
+ setenv( "DOCMAKER_CONVERTER", "LocalCmark" )
+ setenv( "DOCMAKER_LOCALCMARK_BINARY", "cmark-gfm" )
+ fixture = testCase.applyFixture( ...
+ matlab.unittest.fixtures.WorkingFolderFixture() );
+ markdownFile = fullfile( fixture.Folder, "LaTeX.md" );
+ markdown = "$$ \Phi = \frac{\dot{m}_{th}}{2} $$";
+ writelines( markdown, markdownFile )
+
+ output = docconvert( markdownFile, "Root", fixture.Folder, ...
+ "Interpreter", "latex" );
+ html = string( fileread( output ) );
+
+ testCase.verifyTrue( contains( html, "\dot{m}_{th}" ) )
+ testCase.verifyFalse( contains( html, "" ) )
+
+ end % tDocconvertCleansLaTeXExpressions
+
+ end % methods ( Test )
+
+end % classdef
+
+function assumeCmarkAvailable( testCase )
+%assumeCmarkAvailable Skip integration coverage without cmark-gfm.
+
+[status, ~] = system( '"cmark-gfm" --version 2>&1' );
+testCase.assumeEqual( status, 0, ...
+ "cmark-gfm is required for LocalCmark integration tests." )
+
+end % assumeCmarkAvailable
+
+function tf = hasElement( document, name )
+%hasElement Return true when a DOM document contains an element.
+
+tf = document.getElementsByTagName( name ).getLength() > 0;
+
+end % hasElement
+
+function restoreEnvironment( converterWasSet, converter, binaryWasSet, binary )
+%restoreEnvironment Restore LocalCmark converter environment variables.
+
+if converterWasSet
+ setenv( "DOCMAKER_CONVERTER", converter )
+else
+ unsetenv( "DOCMAKER_CONVERTER" )
+end
+
+if binaryWasSet
+ setenv( "DOCMAKER_LOCALCMARK_BINARY", binary )
+else
+ unsetenv( "DOCMAKER_LOCALCMARK_BINARY" )
+end
+
+end % restoreEnvironment
diff --git a/tests/+docmaker/tconverter.m b/tests/+docmaker/tconverter.m
new file mode 100644
index 0000000..7f98b54
--- /dev/null
+++ b/tests/+docmaker/tconverter.m
@@ -0,0 +1,28 @@
+classdef tconverter < matlab.unittest.TestCase
+ %TCONVERTER Tests for DocMaker converter configuration.
+
+ methods ( Test )
+
+ function tEmptyGitHubTokenIsMissing( testCase )
+
+ converter = docmaker.GitHub( "api.github.com", "" );
+
+ testCase.verifyClass( converter, "docmaker.GitHub" )
+ testCase.verifyTrue( ismissing( converter.Token ), ...
+ "An empty GitHub token should not authenticate requests." )
+
+ end % tEmptyGitHubTokenIsMissing
+
+ function tEmptyGitLabTokenIsMissing( testCase )
+
+ converter = docmaker.GitLab( "gitlab.com", "" );
+
+ testCase.verifyClass( converter, "docmaker.GitLab" )
+ testCase.verifyTrue( ismissing( converter.Token ), ...
+ "An empty GitLab token should not authenticate requests." )
+
+ end % tEmptyGitLabTokenIsMissing
+
+ end % methods ( Test )
+
+end % classdef