Module 0399: OER authoring using a command-line interface

Tak Auyeung

2023-12-26

Creative Commons License
The work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

1 Tools of the trade

1.1 nvim

1.2 pandoc

2 Handy tricks

2.1 Mermaid

Mermaid is a syntax that allow drawing graphs using simple text specification. While pandoc, by default, supports the conversion of Mermaid specifications into graphs, it has some severely limitations. In order to fully utilize Mermaid, a better (but clumsier) approach is to utilize JavaScript to convert Mermaid scripts directly.

To do this, you will first need to include the following in a Markdown document. It is best to put the following script in a file, the use !include to bring that into the Markdown file.

<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.js" type="text/javascript"></script>
<script>
  mermaid.initialize(
    {
      startOnLoad: true, 
      flowchart: 
      {
        htmlLabels: true,
        useMaxWidth: false
      },
      securityLevel: 'loose'
    }
  );
  function mirt(id, mm)
  {
    document.getElementById(id).innerHTML = mm
  }
</script>

The previous script defines a function mirt to copy text content into a named div element. As long as the div element has a class="mermaid", the Mermaid interpreter runs on the browser side to render the graph using the latest version of Mermaid.

To specify a graph, use the following template. The id needs to be unique in the document, and it needs to be the same in the div definition and also as the first argument to the function mirt.

<div class="mermaid" id="ballotSD">
</div>

<script>
  mirt('ballotSD',`
graph TD
  started(["\`*vote started*\`"])
  started--"vote yes"-->yes
  started--"vote no"-->no
  started--"vote\nabstain"-->abstain
  started--"extend\ntime"-->started
  started--"time out"-->abstain
  yes(["\`**yes**\`"])
  no(["\`**no**\`"])
  abstain(["\`**abstain**\`"])
`)
</script>

The following is a more complex example. The use of backtick (`) can be used to include Markdown code in the description of a node in Mermaid. However, math equations do not work in this context.

<div class="mermaid" id="motionSD">
</div>

<script>
  mirt('motionSD',`
graph TD
  created(["\`*created*\`"])
  beingDiscussed[being discussed]
  beingAmended[being amended]
  beingVoted[being voted on]
  passed(["\`**passed**\`"])
  defeated(["\`**defeated**\`"])
  created-->beingDiscussed
  beingDiscussed--"close (2/3)"-->beingVoted
  beingDiscussed--"amend"-->beingAmended
  beingAmended--"accept (>50%)"-->beingDiscussed
  beingVoted--">50%"-->passed
  beingVoted--"<50%"-->defeated
`)

3 Useful files

3.1.1 init.vim

The following file typically has a path of /home/user/.config/nvim/init.vim, it specifies how nvim should initialize.

set guicursor=a:blinkon100
call plug#begin('~/.vim/plugged')

" Declare the list of plugins.
" Plug 'tpope/vim-sensible'
" Plug 'junegunn/seoul256.vim'
"
" " List ends here. Plugins become visible to Vim after this call.
"
if has('nvim')
    Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins', 'for': 'javascript' }
    Plug 'jaredgorski/Mies.vim'
    Plug 'chriskempson/base16-vim'
 else
    Plug 'Shougo/deoplete.nvim'
    Plug 'roxma/nvim-yarp'
    Plug 'roxma/vim-hug-neovim-rpc'
endif

if &diff
  set diffopt=filler,context:1
  set diffopt+=iwhite
  set diffopt+=icase
  highlight DiffAdd ctermbg=22
  highlight DiffChange ctermbg=19
  highlight DiffDelete ctermbg=52
  highlight DiffText ctermbg=19
  set nu
endif
let g:deoplete#enable_at_startup = 1
" Plug 'iamcco/markdown-preview.nvim', {'do': 'cd app && yarn install' }
Plug 'Shougo/denite.nvim'
Plug 'tpope/vim-dispatch'
call plug#end()

set sw=2
set ts=2
set expandtab
set wrap linebreak nolist
"  Plug 'HerringtonDarkholme/yats.vim'

" set to 1, nvim will open the preview window after entering the markdown buffer
" default: 0
let g:mkdp_auto_start = 1

" set to 1, the nvim will auto close current preview window when change
" from markdown buffer to another buffer
" default: 1
let g:mkdp_auto_close = 1

" set to 1, the vim will refresh markdown when save the buffer or
" leave from insert mode, default 0 is auto refresh markdown as you edit or
" move the cursor
" default: 0
let g:mkdp_refresh_slow = 0

" set to 1, the MarkdownPreview command can be use for all files,
" by default it can be use in markdown file
" default: 0
let g:mkdp_command_for_global = 0

" set to 1, preview server available to others in your network
" by default, the server listens on localhost (127.0.0.1)
" default: 0
let g:mkdp_open_to_the_world = 0

" use custom IP to open preview page
" useful when you work in remote vim and preview on local browser
" more detail see: https://github.com/iamcco/markdown-preview.nvim/pull/9
" default empty
let g:mkdp_open_ip = ''

" specify browser to open preview page
" default: ''
let g:mkdp_browser = '/home/tauyeung/firefox/firefox -P TakAtWork'

" set to 1, echo preview page url in command line when open preview page
" default is 0
let g:mkdp_echo_preview_url = 1

" a custom vim function name to open preview page
" this function will receive url as param
" default is empty
let g:mkdp_browserfunc = 'g:Open_browser'

" options for markdown render
" mkit: markdown-it options for render
" katex: katex options for math
" uml: markdown-it-plantuml options
" maid: mermaid options
" disable_sync_scroll: if disable sync scroll, default 0
" sync_scroll_type: 'middle', 'top' or 'relative', default value is 'middle'
"   middle: mean the cursor position alway show at the middle of the preview page
"   top: mean the vim top viewport alway show at the top of the preview page
"   relative: mean the cursor position alway show at the relative positon of the preview page
" hide_yaml_meta: if hide yaml metadata, default is 1
" sequence_diagrams: js-sequence-diagrams options
" content_editable: if enable content editable for preview page, default: v:false
let g:mkdp_preview_options = {
    \ 'mkit': {},
    \ 'katex': {},
    \ 'uml': {},
    \ 'maid': {},
    \ 'disable_sync_scroll': 0,
    \ 'sync_scroll_type': 'middle',
    \ 'hide_yaml_meta': 1,
    \ 'sequence_diagrams': {},
    \ 'flowchart_diagrams': {},
    \ 'content_editable': v:false
    \ }

" use a custom markdown style must be absolute path
" like '/Users/username/markdown.css' or expand('~/markdown.css')
let g:mkdp_markdown_css = ''

" use a custom highlight style must absolute path
" like '/Users/username/highlight.css' or expand('~/highlight.css')
let g:mkdp_highlight_css = ''

" use a custom port to start server or random for empty
let g:mkdp_port = '8088'

" preview page title
" ${name} will be replace with the file name
let g:mkdp_page_title = '「${name}」'

function g:Open_browser(url)
"  exec "Start! /home/tauyeung/firefox/firefox -P TakAtWork " . a:url
  exec "Start! firefox " . a:url
endfunction

function MyDiff()
  let opt = ""
  if &diffopt =~ "icase"
    let opt = opt . "-i "
  endif
  if &diffopt =~ "iwhite"
    let opt = opt . "-b "
  endif
  silent execute "!diff -a --binary --ignore-blank-lines " . opt . v:fname_in . " " . v:fname_new . " > " . v:fname_out
endfunction

set diffexpr=MyDiff()
hi Search ctermfg=17
hi Cursor ctermfg=52
hi MatchParen ctermbg=53
hi SpellBad ctermfg=Black
hi SpellLocal ctermfg=Black

" now moved to TxtOptions function set spell spelllang=en_us

set nu

" run the lua script in ./lua/plugins.lua
lua require('plugins')

function TxtOptions()
  set spell spelllang=en_us
  syntax match texBlock /\$\{1,2}.\{-}\$\{1,2}/ contains=@NoSpell
  syntax match mdInclude /^\!include.*$/ contains=@NoSpell
  lua require('gg')
  set background=light
  colorscheme mies
"  colorscheme moonlight
  LspStart
endfunction

autocmd BufNewFile,BufRead *.{md,txt} call TxtOptions()

3.1.2 plugins.lua

The following file configures the Lua-based plugins in nvim. It has a path of /home/user/.config/nvim/lua/plugins.lua.

local fn = vim.fn
local install_path = fn.stdpath('data')..'/site/pack/packer/start/packer.nvim'
if fn.empty(fn.glob(install_path)) > 0 then
  packer_bootstrap = fn.system({'git', 'clone', '--depth', '1', 'https://github.com/wbthomason/packer.nvim', install_path})
end

return require('packer').startup(function(use)
  -- Packer can manage itself
  use 'wbthomason/packer.nvim'
  use {
    "williamboman/mason.nvim",
    "williamboman/mason-lspconfig.nvim",
    "neovim/nvim-lspconfig",
    "folke/lsp-colors.nvim",
  }
  use { 'lewis6991/gitsigns.nvim' }
  use({
  'glepnir/galaxyline.nvim',
  branch = 'main',
  -- some optional icons
  requires = { 'kyazdani42/nvim-web-devicons', opt = true },
  })
  use { 'lifepillar/vim-solarized8' }
  use { 'shaunsingh/moonlight.nvim' }
  if packer_bootstrap then
    require('packer').sync()
  end
end)

3.1.3 gg.lua

Note how init.vim calls gg.lua. gg.lua is a Lua file to originally configure Grammar Guard. Grammar Guard is a grammar checker, but it is now rolled into a different package. However, the way ltex is configured remains the same. The following is the file /home/user/.config/nvim/lua/gg.lua:

-- hook to nvim-lspconfig
-- require("grammar-guard").init()

local utils = {}
utils.concat_fileLines = function(file)
  local dictionary = {}
  for line in io.lines(file) do
    table.insert(dictionary, line)
  end
  return dictionary
end

require("mason").setup()
require("mason-lspconfig").setup({})
require("lspconfig").ltex.setup({
	settings = {
		ltex = {
			enabled = { "latex", "tex", "bib", "markdown" },
			language = "en-US",
      configurationTarget = { dictionary = "userExternalFile", },
			diagnosticSeverity = "information",
      dictionary = { ["en-US"] =  utils.concat_fileLines("/home/tauyeung/.config/nvim/spell/ltex.en-US.dictionary") },
			sentenceCacheSize = 2000,
			additionalRules = {
				enablePickyRules = true,
				motherTongue = "en",
			},
			trace = { server = "verbose" },
			disabledRules = { ['en-US'] = { "EN_QUOTES" } },
--			disabledRules = {},
			hiddenFalsePositives = {
        ["en-US"] = { "{ \"rule\": \"MORFOLOGIK_RULE_EN_US\", \"sentence\": \"^!include.*$\" }" }
      },
		},
	},
})
local opts = { noremap=true, silent=true }
vim.keymap.set('n', '<space>e', vim.diagnostic.open_float, opts)

3.2 Convenience

The following is a Node script that converts a markdown file into HTML.

#!/usr/local/bin/node
"use strict";

const child_process = require('child_process')


function shelledCommand(command)
{
  function whatToDo(resolve, reject)
  {
    function execHandler(error, stdout, stderr)
    {
      resolve(
        { 
          command: command,
          error: error,
          stdout: stdout,
          stderr: stderr
        }
      )
    }
    child_process.exec(command, {}, execHandler)
  }
  return new Promise(
    whatToDo
  )
}

async function findLargestNumber()
{
  let result = await shelledCommand("ls")
  let files = result.stdout.split('\n')
  let max = 0
  files.forEach(
    fn =>
    {
      let fnMatch = fn.match(/^(\D*)(\d+)\.*/)
      if (fnMatch !== null)
      {
        console.log(JSON.stringify(fnMatch))
        let fnNum = parseInt(fnMatch[2],10) 
        console.log(fnNum)
        if (fnNum > max)
        {
          max = fnNum
        }
      }
    }
  )
  return max
}

async function doCmd(fn)
{
  const cmd = ` pandoc --standalone --css https://power.arc.losrios.edu/~auyeunt/pandoc.css -t html5 --filter pandoc-include -V date="$(date +%Y-%m-%d%n)" --filter pandoc-mustache --number-sections --mathjax=https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js -s -o ${fn.replace(/\.md$/,'.html')} --syntax-definition=/home/tauyeung/KatePart/ttpasm.xml -F mermaid-filter --toc ${fn}`
  // const cmd = ` pandoc --css pandoc.css -t html5 --filter pandoc-include --filter pandoc-mustache --number-sections --mathjax=https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js -s -o ${fn.replace(/\.md$/,'.html')} --syntax-definition=/home/tauyeung/KatePart/ttpasm.xml -F mermaid-filter --toc ${fn}`
 let result = await shelledCommand(cmd);
  return result
}

async function main()
{
  if (process.argv < 3)
  {
    console.log('need name of input .md file')
  }
  else
  {
    let result = await doCmd(process.argv[2])
    console.log(`result:\n${result}`)
    console.log(`error:\n${result.error}`)
    console.log(`stdout:\n${result.stdout}`)
    console.log(`stderr:\n${result.stderr}`)
  }
}

main()

The following is a template for Markdown documents. Obviously, edit to customize for yourself!

---
header-includes: <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js" type="text/javascript"></script>
title: "Module 0000: DDDD Title"
author: Tak Auyeung
output: 
	md_document:
  	variant: markdown_mmd
---
!include /home/tauyeung/md/pandoc/mermaidInit.md

!include /home/tauyeung/md/pandoc/cc-by.md

[comment]: # this line should be considered as comment

4 Advanced

pandoc can expand the supported languages in code blocks. The following is a highlighted code block of TTPASM (Tak’s Toy Processor Assembly) source code.

ld a,(a)

This is done by the --syntax-definition= command line switch of pandoc.