From 0819adc8adae135b72abc6385ab8b7be3d7b8e0b Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Tue, 27 Oct 2026 16:51:14 +0000 Subject: [PATCH 1/9] Normalize empty tokens: "" becomes missing --- tbx/docmaker/+docmaker/GitHub.m | 4 ++-- tbx/docmaker/+docmaker/GitLab.m | 2 +- tbx/docmaker/+docmaker/MarkdownConverter.m | 19 ++++++++++++++++++- tbx/docmaker/+docmaker/converter.m | 6 +++--- 4 files changed, 24 insertions(+), 7 deletions(-) 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/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..424cd86 100644 --- a/tbx/docmaker/+docmaker/converter.m +++ b/tbx/docmaker/+docmaker/converter.m @@ -18,11 +18,11 @@ 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 ); otherwise error( "docmaker:Unhandled", "Unknown converter ""%s"".", n ) @@ -74,4 +74,4 @@ tf = HAS_SECRETS; % retrieve end -end % hasSecrets \ No newline at end of file +end % hasSecrets From 6ef4b843ea34a84d7a7f61f0c92fbffda402012f Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Tue, 27 Oct 2026 16:52:22 +0000 Subject: [PATCH 2/9] Added test for empty token normalization --- tests/+docmaker/tconverter.m | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/+docmaker/tconverter.m 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 From 260f05dc1695d7a0cfee191bd318c406f3396f8f Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Tue, 27 Oct 2026 16:54:12 +0000 Subject: [PATCH 3/9] Added Name field to metadata --- docmaker.json | 1 + 1 file changed, 1 insertion(+) 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", From 44cc5b534ad4201feb1e50622a69e0ce725ee708 Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Tue, 27 Oct 2026 17:01:57 +0000 Subject: [PATCH 4/9] Add PackageName to package task --- buildfile.m | 5 +++++ 1 file changed, 5 insertions(+) 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" ) ); From e952ed6214e488b02bda65f494383a79dd58afa1 Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Wed, 28 Oct 2026 09:25:21 +0000 Subject: [PATCH 5/9] Preparation for new release --- .github/workflows/docmaker-ci.yml | 23 ++++ README.md | 6 +- .../0ikZNnfq8Omj89uI9ZrkWUPmJAsd.xml | 6 + .../0ikZNnfq8Omj89uI9ZrkWUPmJAsp.xml | 2 + .../76Ysw5dsZobh4SitR55uuJz41y0d.xml | 2 + .../76Ysw5dsZobh4SitR55uuJz41y0p.xml | 2 + .../mHFG09DfonBnLdBP-i8fjH-c5Ucd.xml | 6 + .../mHFG09DfonBnLdBP-i8fjH-c5Ucp.xml | 2 + .../z1RViax6Ofaoj4VSrBPZcC0V2lgd.xml | 6 + .../z1RViax6Ofaoj4VSrBPZcC0V2lgp.xml | 2 + .../alNvJCF7XFIrHIM8Qu5C9uu3RZMd.xml | 2 + .../alNvJCF7XFIrHIM8Qu5C9uu3RZMp.xml | 2 + tbx/docmaker/+docmaker/LocalCmark.m | 100 +++++++++++++++ tbx/docmaker/+docmaker/converter.m | 13 +- tbx/docmaker/Contents.m | 2 +- tbx/docmaker/docconvert.m | 14 ++- tbx/docmakerdoc/about.md | 8 +- tbx/docmakerdoc/changelog.md | 7 +- tbx/docmakerdoc/conversion.md | 30 ++++- tbx/docmakerdoc/index.md | 7 +- tests/+docmaker/tLocalCmark.m | 118 ++++++++++++++++++ 21 files changed, 341 insertions(+), 19 deletions(-) create mode 100644 resources/project/UMvhTnfaEKsbCRaOOLl1x86F4Is/0ikZNnfq8Omj89uI9ZrkWUPmJAsd.xml create mode 100644 resources/project/UMvhTnfaEKsbCRaOOLl1x86F4Is/0ikZNnfq8Omj89uI9ZrkWUPmJAsp.xml create mode 100644 resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/76Ysw5dsZobh4SitR55uuJz41y0d.xml create mode 100644 resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/76Ysw5dsZobh4SitR55uuJz41y0p.xml create mode 100644 resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/mHFG09DfonBnLdBP-i8fjH-c5Ucd.xml create mode 100644 resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/mHFG09DfonBnLdBP-i8fjH-c5Ucp.xml create mode 100644 resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/z1RViax6Ofaoj4VSrBPZcC0V2lgd.xml create mode 100644 resources/project/alNvJCF7XFIrHIM8Qu5C9uu3RZM/z1RViax6Ofaoj4VSrBPZcC0V2lgp.xml create mode 100644 resources/project/hLMKfvmMZYFbdoE4Xco7ryXas6s/alNvJCF7XFIrHIM8Qu5C9uu3RZMd.xml create mode 100644 resources/project/hLMKfvmMZYFbdoE4Xco7ryXas6s/alNvJCF7XFIrHIM8Qu5C9uu3RZMp.xml create mode 100644 tbx/docmaker/+docmaker/LocalCmark.m create mode 100644 tests/+docmaker/tLocalCmark.m diff --git a/.github/workflows/docmaker-ci.yml b/.github/workflows/docmaker-ci.yml index bb2dba6..9832c5a 100644 --- a/.github/workflows/docmaker-ci.yml +++ b/.github/workflows/docmaker-ci.yml @@ -32,6 +32,29 @@ 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: Install cmark-gfm on Windows + if: ${{ matrix.platform == 'windows-latest' }} + shell: pwsh + run: | + $version = "0.29.0.gfm.13" + $archive = Join-Path $env:RUNNER_TEMP "cmark-gfm.zip" + $folder = Join-Path $env:RUNNER_TEMP "cmark-gfm" + $uri = "https://github.com/github/cmark-gfm/releases/download/" + + "$version/cmark-gfm-$version-win64.zip" + Invoke-WebRequest -Uri $uri -OutFile $archive + Expand-Archive -Path $archive -DestinationPath $folder + $binary = Get-ChildItem -Path $folder -Filter "cmark-gfm.exe" ` + -Recurse | Select-Object -First 1 + if ($null -eq $binary) { + throw "The cmark-gfm archive did not contain cmark-gfm.exe." + } + $binary.DirectoryName | Out-File -FilePath $env:GITHUB_PATH ` + -Encoding utf8 -Append + - 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/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/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/converter.m b/tbx/docmaker/+docmaker/converter.m index 424cd86..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. @@ -24,6 +26,9 @@ h = getValue( "GitLab hostname", "www.gitlab.com" ); 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 diff --git a/tbx/docmaker/Contents.m b/tbx/docmaker/Contents.m index 07e7c55..a72b80c 100644 --- a/tbx/docmaker/Contents.m +++ b/tbx/docmaker/Contents.m @@ -1,5 +1,5 @@ % 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 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..21ea507 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..4442ce9 --- /dev/null +++ b/tests/+docmaker/tLocalCmark.m @@ -0,0 +1,118 @@ +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 ) + + originalConverter = getenv( "DOCMAKER_CONVERTER" ); + originalBinary = getenv( "DOCMAKER_LOCALCMARK_BINARY" ); + cleanup = onCleanup( @() restoreEnvironment( ... + originalConverter, 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 ) + originalConverter = getenv( "DOCMAKER_CONVERTER" ); + originalBinary = getenv( "DOCMAKER_LOCALCMARK_BINARY" ); + cleanup = onCleanup( @() restoreEnvironment( ... + originalConverter, 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( converter, binary ) +%restoreEnvironment Restore LocalCmark converter environment variables. + +setenv( "DOCMAKER_CONVERTER", converter ) +setenv( "DOCMAKER_LOCALCMARK_BINARY", binary ) + +end % restoreEnvironment From 1b62836f1cf915a8436babc66fab8143cf9709b5 Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Wed, 28 Oct 2026 09:30:21 +0000 Subject: [PATCH 6/9] Added task to Contents.m --- tbx/docmaker/Contents.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tbx/docmaker/Contents.m b/tbx/docmaker/Contents.m index a72b80c..bf00f1b 100644 --- a/tbx/docmaker/Contents.m +++ b/tbx/docmaker/Contents.m @@ -1,9 +1,10 @@ % MATLAB DocMaker % 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. From 455763a85866a5ba8eb4a7588082774bc88054f2 Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Wed, 28 Oct 2026 09:35:46 +0000 Subject: [PATCH 7/9] Do not execute example code in conversion.md --- tbx/docmakerdoc/conversion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tbx/docmakerdoc/conversion.md b/tbx/docmakerdoc/conversion.md index 21ea507..8266f82 100644 --- a/tbx/docmakerdoc/conversion.md +++ b/tbx/docmakerdoc/conversion.md @@ -48,7 +48,7 @@ To convert Markdown without making network calls, install [cmark-gfm](https://github.com/github/cmark-gfm) and select `LocalCmark`: ```matlab -setpref("docmaker","converter","LocalCmark") +setpref("docmaker","converter","LocalCmark") ``` The default binary name is `cmark-gfm`. To use another binary or an explicit From c8e9338f293096d07d96f0b95b9a100a213cb377 Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Wed, 28 Oct 2026 09:39:43 +0000 Subject: [PATCH 8/9] Test cmark only on Linux --- .github/workflows/docmaker-ci.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/docmaker-ci.yml b/.github/workflows/docmaker-ci.yml index 9832c5a..34790b6 100644 --- a/.github/workflows/docmaker-ci.yml +++ b/.github/workflows/docmaker-ci.yml @@ -36,25 +36,6 @@ jobs: if: ${{ matrix.platform == 'ubuntu-latest' }} run: sudo apt-get update && sudo apt-get install -y cmark-gfm - - name: Install cmark-gfm on Windows - if: ${{ matrix.platform == 'windows-latest' }} - shell: pwsh - run: | - $version = "0.29.0.gfm.13" - $archive = Join-Path $env:RUNNER_TEMP "cmark-gfm.zip" - $folder = Join-Path $env:RUNNER_TEMP "cmark-gfm" - $uri = "https://github.com/github/cmark-gfm/releases/download/" + - "$version/cmark-gfm-$version-win64.zip" - Invoke-WebRequest -Uri $uri -OutFile $archive - Expand-Archive -Path $archive -DestinationPath $folder - $binary = Get-ChildItem -Path $folder -Filter "cmark-gfm.exe" ` - -Recurse | Select-Object -First 1 - if ($null -eq $binary) { - throw "The cmark-gfm archive did not contain cmark-gfm.exe." - } - $binary.DirectoryName | Out-File -FilePath $env:GITHUB_PATH ` - -Encoding utf8 -Append - - name: Set up MATLAB on the runner uses: matlab-actions/setup-matlab@v3 with: From 298baba6cda31e522f3ef141bea5d11827961eb0 Mon Sep 17 00:00:00 2001 From: Ken Deeley Date: Wed, 28 Oct 2026 09:46:51 +0000 Subject: [PATCH 9/9] Test environment cleanup --- tests/+docmaker/tLocalCmark.m | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/+docmaker/tLocalCmark.m b/tests/+docmaker/tLocalCmark.m index 4442ce9..7881390 100644 --- a/tests/+docmaker/tLocalCmark.m +++ b/tests/+docmaker/tLocalCmark.m @@ -21,10 +21,13 @@ function tConstructorRejectsShellMetacharacters( testCase ) function tConverterUsesConfiguredBinary( testCase ) + converterWasSet = isenv( "DOCMAKER_CONVERTER" ); + binaryWasSet = isenv( "DOCMAKER_LOCALCMARK_BINARY" ); originalConverter = getenv( "DOCMAKER_CONVERTER" ); originalBinary = getenv( "DOCMAKER_LOCALCMARK_BINARY" ); cleanup = onCleanup( @() restoreEnvironment( ... - originalConverter, originalBinary ) ); + converterWasSet, originalConverter, ... + binaryWasSet, originalBinary ) ); setenv( "DOCMAKER_CONVERTER", "LocalCmark" ) setenv( "DOCMAKER_LOCALCMARK_BINARY", "test-cmark" ) @@ -68,10 +71,13 @@ function tConvertsGfmExtensions( testCase ) 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( ... - originalConverter, originalBinary ) ); + converterWasSet, originalConverter, ... + binaryWasSet, originalBinary ) ); setenv( "DOCMAKER_CONVERTER", "LocalCmark" ) setenv( "DOCMAKER_LOCALCMARK_BINARY", "cmark-gfm" ) fixture = testCase.applyFixture( ... @@ -109,10 +115,19 @@ function assumeCmarkAvailable( testCase ) end % hasElement -function restoreEnvironment( converter, binary ) +function restoreEnvironment( converterWasSet, converter, binaryWasSet, binary ) %restoreEnvironment Restore LocalCmark converter environment variables. -setenv( "DOCMAKER_CONVERTER", converter ) -setenv( "DOCMAKER_LOCALCMARK_BINARY", binary ) +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