To specify modified versions of the Markdown markup syntax.
§1. In practice, nobody quite uses pure Markdown, because there are always minor additions or removals people want to make in particular use cases. We'll call modified versions of the Markdown syntax "variations", but will provide only one of these here: the baseline of CommonMark.
define MAX_MARKDOWNFEATURES 256
markdown_feature *markdown_feature_registry[MAX_MARKDOWNFEATURES]; void MarkdownVariations::start(void) { Markdown::create_item_types(); for (int i=0; i<MAX_MARKDOWNFEATURES; i++) markdown_feature_registry[i] = NULL; MarkdownVariations::define_CommonMark(); MarkdownVariations::define_GFM(); } markdown_variation *CommonMark_variation = NULL, *GitHub_flavored_Markdown_variation = NULL; markdown_variation *MarkdownVariations::CommonMark(void) { return CommonMark_variation; } markdown_variation *MarkdownVariations::GitHub_flavored_Markdown(void) { return GitHub_flavored_Markdown_variation; }
§2. A variation is essentially a named collection of features, which may affect parsing, or rendering, or both.
All newly-created variations begin with the CommonMark baseline set of features active, and no others. The caller should use MarkdownVariations::add_feature to add a home-brew feature, or MarkdownVariations::remove_feature to take away one of the CommonMark ones.
typedef struct markdown_variation { struct text_stream *name; int active_built_in_features[NO_DEFINED_MARKDOWNFEATURE_VALUES]; struct method_set *methods; CLASS_DEFINITION } markdown_variation; markdown_variation *MarkdownVariations::new(text_stream *name) { markdown_variation *variation = CREATE(markdown_variation); variation->name = Str::duplicate(name); variation->methods = Methods::new_set(); for (int i=0; i<NO_DEFINED_MARKDOWNFEATURE_VALUES; i++) variation->active_built_in_features[i] = FALSE; MarkdownVariations::make_baseline_features_active(variation); return variation; } void MarkdownVariations::add_feature(markdown_variation *variation, int feature_id) { variation->active_built_in_features[feature_id] = TRUE; } void MarkdownVariations::remove_feature(markdown_variation *variation, int feature_id) { variation->active_built_in_features[feature_id] = FALSE; } void MarkdownVariations::copy_features_of(markdown_variation *to, markdown_variation *from) { for (int i=0; i<NO_DEFINED_MARKDOWNFEATURE_VALUES; i++) to->active_built_in_features[i] = from->active_built_in_features[i]; }
- The structure markdown_variation is accessed in 2/mth, 2/trs, 4/prp, 4/jsn, 5/mrk, 5/im, 7/ld, 9/pl and here.
§3. A "feature" is an aspect of Markdown syntax or behaviour; any given variation can either support it or not.
typedef struct markdown_feature { struct text_stream *name; int feature_ID; struct method_set *methods; CLASS_DEFINITION } markdown_feature; markdown_feature *MarkdownVariations::new_feature(text_stream *name, int id) { markdown_feature *feature = CREATE(markdown_feature); if (id >= MAX_MARKDOWNFEATURES) internal_error("too many Markdown features"); feature->name = Str::duplicate(name); feature->feature_ID = id; feature->methods = Methods::new_set(); if (markdown_feature_registry[id]) internal_error("Markdown feature ID clash"); markdown_feature_registry[id] = feature; return feature; }
- The structure markdown_feature is accessed in 2/mth, 2/trs, 4/prp, 4/jsn, 5/mrk, 5/im, 7/ld, 9/pl and here.
§4. The point of the cumbersome business of both defining an ID and creating an object to represent the same feature was to make this function quick.
int MarkdownVariations::supports(markdown_variation *variation, int feature_id) { return variation->active_built_in_features[feature_id]; }
§5. The CommonMark variation. Vanilla ice cream is under-rated:
enum BLOCK_QUOTES_MARKDOWNFEATURE from 0 enum ORDERED_LISTS_MARKDOWNFEATURE enum UNORDERED_LISTS_MARKDOWNFEATURE enum INDENTED_CODE_BLOCKS_MARKDOWNFEATURE enum FENCED_CODE_BLOCKS_MARKDOWNFEATURE enum HTML_BLOCKS_MARKDOWNFEATURE enum THEMATIC_MARKERS_MARKDOWNFEATURE enum ATX_HEADINGS_MARKDOWNFEATURE enum SETEXT_HEADINGS_MARKDOWNFEATURE enum WEB_AUTOLINKS_MARKDOWNFEATURE enum EMAIL_AUTOLINKS_MARKDOWNFEATURE enum INLINE_HTML_MARKDOWNFEATURE enum BACKTICKED_CODE_MARKDOWNFEATURE enum LINKS_MARKDOWNFEATURE enum IMAGES_MARKDOWNFEATURE enum ASTERISK_EMPHASIS_MARKDOWNFEATURE enum UNDERSCORE_EMPHASIS_MARKDOWNFEATURE enum ENTITIES_MARKDOWNFEATURE
markdown_feature *block_quotes_Markdown_feature = NULL; markdown_feature *ordered_lists_Markdown_feature = NULL; markdown_feature *unordered_lists_Markdown_feature = NULL; markdown_feature *indented_code_blocks_Markdown_feature = NULL; markdown_feature *fenced_code_blocks_Markdown_feature = NULL; markdown_feature *HTML_blocks_Markdown_feature = NULL; markdown_feature *thematic_markers_Markdown_feature = NULL; markdown_feature *ATX_headings_Markdown_feature = NULL; markdown_feature *setext_headings_Markdown_feature = NULL; markdown_feature *web_autolinks_Markdown_feature = NULL; markdown_feature *email_autolinks_Markdown_feature = NULL; markdown_feature *inline_HTML_Markdown_feature = NULL; markdown_feature *backticked_code_Markdown_feature = NULL; markdown_feature *links_Markdown_feature = NULL; markdown_feature *images_Markdown_feature = NULL; markdown_feature *asterisk_emphasis_Markdown_feature = NULL; markdown_feature *underscore_emphasis_Markdown_feature = NULL; markdown_feature *entities_Markdown_feature = NULL; void MarkdownVariations::define_CommonMark(void) { block_quotes_Markdown_feature = MarkdownVariations::new_feature(I"block quotes", BLOCK_QUOTES_MARKDOWNFEATURE); ordered_lists_Markdown_feature = MarkdownVariations::new_feature(I"ordered lists", ORDERED_LISTS_MARKDOWNFEATURE); unordered_lists_Markdown_feature = MarkdownVariations::new_feature(I"unordered lists", UNORDERED_LISTS_MARKDOWNFEATURE); indented_code_blocks_Markdown_feature = MarkdownVariations::new_feature(I"indented code blocks", INDENTED_CODE_BLOCKS_MARKDOWNFEATURE); fenced_code_blocks_Markdown_feature = MarkdownVariations::new_feature(I"fenced code blocks", FENCED_CODE_BLOCKS_MARKDOWNFEATURE); HTML_blocks_Markdown_feature = MarkdownVariations::new_feature(I"HTML blocks", HTML_BLOCKS_MARKDOWNFEATURE); thematic_markers_Markdown_feature = MarkdownVariations::new_feature(I"thematic markers", THEMATIC_MARKERS_MARKDOWNFEATURE); ATX_headings_Markdown_feature = MarkdownVariations::new_feature(I"ATX headings", ATX_HEADINGS_MARKDOWNFEATURE); setext_headings_Markdown_feature = MarkdownVariations::new_feature(I"setext headings", SETEXT_HEADINGS_MARKDOWNFEATURE); web_autolinks_Markdown_feature = MarkdownVariations::new_feature(I"web autolinks", WEB_AUTOLINKS_MARKDOWNFEATURE); email_autolinks_Markdown_feature = MarkdownVariations::new_feature(I"email autolinks", EMAIL_AUTOLINKS_MARKDOWNFEATURE); inline_HTML_Markdown_feature = MarkdownVariations::new_feature(I"inline HTML", INLINE_HTML_MARKDOWNFEATURE); backticked_code_Markdown_feature = MarkdownVariations::new_feature(I"backticked code", BACKTICKED_CODE_MARKDOWNFEATURE); links_Markdown_feature = MarkdownVariations::new_feature(I"links", LINKS_MARKDOWNFEATURE); images_Markdown_feature = MarkdownVariations::new_feature(I"images", IMAGES_MARKDOWNFEATURE); asterisk_emphasis_Markdown_feature = MarkdownVariations::new_feature(I"emphasis", ASTERISK_EMPHASIS_MARKDOWNFEATURE); underscore_emphasis_Markdown_feature = MarkdownVariations::new_feature(I"emphasis", UNDERSCORE_EMPHASIS_MARKDOWNFEATURE); entities_Markdown_feature = MarkdownVariations::new_feature(I"entities", ENTITIES_MARKDOWNFEATURE); CommonMark_variation = MarkdownVariations::new(I"CommonMark 0.30"); } void MarkdownVariations::make_baseline_features_active(markdown_variation *variation) { MarkdownVariations::add_feature(variation, BLOCK_QUOTES_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, ORDERED_LISTS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, UNORDERED_LISTS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, INDENTED_CODE_BLOCKS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, FENCED_CODE_BLOCKS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, HTML_BLOCKS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, THEMATIC_MARKERS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, ATX_HEADINGS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, SETEXT_HEADINGS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, WEB_AUTOLINKS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, EMAIL_AUTOLINKS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, INLINE_HTML_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, BACKTICKED_CODE_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, LINKS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, IMAGES_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, ASTERISK_EMPHASIS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, UNDERSCORE_EMPHASIS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, ENTITIES_MARKDOWNFEATURE); }
§6. Github extensions to CommonMark. See GitHub's specification, which is based on CommonMark, though its description of new features is a little more concise, since these have been added with more prudent syntax.
enum STRIKETHROUGH_MARKDOWNFEATURE enum TABLES_MARKDOWNFEATURE enum TASK_LIST_ITEMS_MARKDOWNFEATURE enum EXTENDED_AUTOLINKS_MARKDOWNFEATURE enum DISALLOWED_RAW_HTML_MARKDOWNFEATURE
markdown_feature *strikethrough_Markdown_feature = NULL; markdown_feature *tables_Markdown_feature = NULL; markdown_feature *task_list_items_Markdown_feature = NULL; markdown_feature *extended_autolinks_Markdown_feature = NULL; markdown_feature *disallowed_raw_HTML_Markdown_feature = NULL; void MarkdownVariations::define_GFM(void) { strikethrough_Markdown_feature = MarkdownVariations::new_feature(I"strikethrough", STRIKETHROUGH_MARKDOWNFEATURE); tables_Markdown_feature = MarkdownVariations::new_feature(I"tables", TABLES_MARKDOWNFEATURE); task_list_items_Markdown_feature = MarkdownVariations::new_feature(I"task list items", TASK_LIST_ITEMS_MARKDOWNFEATURE); extended_autolinks_Markdown_feature = MarkdownVariations::new_feature(I"extended autolinks", EXTENDED_AUTOLINKS_MARKDOWNFEATURE); disallowed_raw_HTML_Markdown_feature = MarkdownVariations::new_feature(I"disallowed raw HTML", DISALLOWED_RAW_HTML_MARKDOWNFEATURE); GitHub_flavored_Markdown_variation = MarkdownVariations::new(I"GitHub-flavored Markdown 0.29"); MarkdownVariations::make_GitHub_features_active(GitHub_flavored_Markdown_variation); } void MarkdownVariations::make_GitHub_features_active(markdown_variation *variation) { MarkdownVariations::add_feature(variation, STRIKETHROUGH_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, TABLES_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, TASK_LIST_ITEMS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, EXTENDED_AUTOLINKS_MARKDOWNFEATURE); MarkdownVariations::add_feature(variation, DISALLOWED_RAW_HTML_MARKDOWNFEATURE); }
§7. Methods for features. New features need to be able to intervene in the parsing or rendering algorithms, so for that we provide methods.
RENDER_MARKDOWN_MTID allows a feature to meddle in how a node is rendered.
enum RENDER_MARKDOWN_MTID
INT_METHOD_TYPE(RENDER_MARKDOWN_MTID, markdown_feature *feature, text_stream *OUT, markdown_item *md, int mode) int MarkdownVariations::intervene_in_rendering(markdown_variation *variation, text_stream *OUT, markdown_item *md, int mode) { markdown_feature *feature; LOOP_OVER(feature, markdown_feature) { if (MarkdownVariations::supports(variation, feature->feature_ID)) { int rv = FALSE; INT_METHOD_CALL(rv, feature, RENDER_MARKDOWN_MTID, OUT, md, mode); if (rv) return TRUE; } } return FALSE; }
§8. POST_PHASE_I_MARKDOWN_MTID allows a feature to restructure or annotate the block tree produced by Phase I parsing; similarly POST_PHASE_II_MARKDOWN_MTID.
enum POST_PHASE_I_MARKDOWN_MTID enum POST_PHASE_II_MARKDOWN_MTID
VOID_METHOD_TYPE(POST_PHASE_I_MARKDOWN_MTID, markdown_feature *feature, markdown_item *tree, md_links_dictionary *link_references) VOID_METHOD_TYPE(POST_PHASE_II_MARKDOWN_MTID, markdown_feature *feature, markdown_item *tree, md_links_dictionary *link_references) void MarkdownVariations::intervene_after_Phase_I(markdown_variation *variation, markdown_item *tree, md_links_dictionary *link_references) { markdown_feature *feature; LOOP_OVER(feature, markdown_feature) { if (MarkdownVariations::supports(variation, feature->feature_ID)) { VOID_METHOD_CALL(feature, POST_PHASE_I_MARKDOWN_MTID, tree, link_references); } } } void MarkdownVariations::intervene_after_Phase_II(markdown_variation *variation, markdown_item *tree, md_links_dictionary *link_references) { markdown_feature *feature; LOOP_OVER(feature, markdown_feature) { if (MarkdownVariations::supports(variation, feature->feature_ID)) { VOID_METHOD_CALL(feature, POST_PHASE_II_MARKDOWN_MTID, tree, link_references); } } }
§9. MULTIFILE_MARKDOWN_MTID allows a feature to tell the parser that the content will end up being split across multiple HTML files. If a feature wants to do this, it should then set a "file point" at any top-level positions of its choice, and return TRUE.
enum MULTIFILE_MARKDOWN_MTID
INT_METHOD_TYPE(MULTIFILE_MARKDOWN_MTID, markdown_feature *feature, markdown_item *tree, md_links_dictionary *link_references) int MarkdownVariations::multifile_mode(markdown_variation *variation, markdown_item *tree, md_links_dictionary *link_references) { if (tree->down) { markdown_feature *feature; LOOP_OVER(feature, markdown_feature) { if (MarkdownVariations::supports(variation, feature->feature_ID)) { int rv = FALSE; INT_METHOD_CALL(rv, feature, MULTIFILE_MARKDOWN_MTID, tree, link_references); if (rv) { MarkdownVariations::assign_URLs_to_headings(tree, link_references); return TRUE; } } } } return FALSE; } void MarkdownVariations::assign_URLs_to_headings(markdown_item *tree, md_links_dictionary *link_references) { if (tree->down->type != VOLUME_MIT) { markdown_item *index = Markdown::new_volume_marker(I"all in one"); index->next = tree->down; tree->down = index; } for (markdown_item *md = tree->down; md; md = md->next) { if (md->type == VOLUME_MIT) { md->down = md->next; md->next = NULL; markdown_item *ch = md->down, *prev_ch = NULL; while ((ch) && (ch->type != VOLUME_MIT)) { prev_ch = ch, ch = ch->next; } if (ch) { prev_ch->next = NULL; md->next = ch; } } } for (markdown_item *vol = tree->down; vol; vol = vol->next) { if (vol->type == VOLUME_MIT) { if ((vol->down) && (vol->down->type != FILE_MIT)) { #ifdef SUPERVISOR_MODULE text_stream *home_URL = DocumentationCompiler::home_URL_at_volume_item(vol); #endif #ifndef SUPERVISOR_MODULE text_stream *home_URL = I"index.html"; #endif markdown_item *index = Markdown::new_file_marker(Filenames::from_text(home_URL)); index->next = vol->down; vol->down = index; } for (markdown_item *md = vol->down; md; md = md->next) { if (md->type == FILE_MIT) { md->down = md->next; md->next = NULL; markdown_item *ch = md->down, *prev_ch = NULL; while ((ch) && (ch->type != FILE_MIT) && (ch->type != VOLUME_MIT)) { prev_ch = ch, ch = ch->next; } if (ch) { prev_ch->next = NULL; md->next = ch; } } } } } markdown_item *headings[7] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL }; MarkdownVariations::multifile_r(tree->down, link_references, headings, NULL); }
void MarkdownVariations::multifile_r(markdown_item *md, md_links_dictionary *link_references, markdown_item *headings[7], markdown_item *file_item) { int non_heading_found = FALSE; for (; md; md = md->next) { if (md->type == HEADING_MIT) { int L = Markdown::get_heading_level(md); headings[L] = md; for (int i=L+1; i<=6; i++) headings[i] = NULL; text_stream *URL = Str::new(); if (file_item) { WRITE_TO(URL, "%f", Markdown::get_filename(file_item)); } TEMPORARY_TEXT(xref) TEMPORARY_TEXT(anchor) match_results mr = Regexp::create_mr(); if (Regexp::match(&mr, md->stashed, U"Chapter (%d+): *(%c*)")) { WRITE_TO(xref, "%S", mr.exp[1]); WRITE_TO(anchor, "chapter%S", mr.exp[0]); } else if (Regexp::match(&mr, md->stashed, U"Chapter (%d+)")) { WRITE_TO(xref, "%S", md->stashed); WRITE_TO(anchor, "chapter%S", mr.exp[0]); } else if (Regexp::match(&mr, md->stashed, U"Section (%d+).(%d+): *(%c*)")) { WRITE_TO(xref, "%S", mr.exp[2]); WRITE_TO(anchor, "c%Ss%S", mr.exp[0], mr.exp[1]); } else if (Regexp::match(&mr, md->stashed, U"Section (%d+).(%d+)")) { WRITE_TO(xref, "%S", md->stashed); WRITE_TO(anchor, "c%Ss%S", mr.exp[0], mr.exp[1]); } else if (Regexp::match(&mr, md->stashed, U"Section (%d+): *(%c*)")) { WRITE_TO(xref, "%S", mr.exp[1]); WRITE_TO(anchor, "s%S", mr.exp[0]); } else if (Regexp::match(&mr, md->stashed, U"Section (%d+)")) { WRITE_TO(xref, "%S", md->stashed); WRITE_TO(anchor, "s%S", mr.exp[0]); } else { WRITE_TO(xref, "%S", md->stashed); WRITE_TO(anchor, "heading%d", md->id); } Regexp::dispose_of(&mr); if (non_heading_found) { WRITE_TO(URL, "#%S", anchor); } md->user_state = STORE_POINTER_text_stream(URL); Markdown::create(link_references, xref, URL, md->stashed); DISCARD_TEXT(xref) DISCARD_TEXT(anchor) } else { non_heading_found = TRUE; } MarkdownVariations::multifile_r(md->down, link_references, headings, (md->type == FILE_MIT)?md:NULL); } } text_stream *MarkdownVariations::URL_for_heading(markdown_item *md) { if ((md) && (md->type == HEADING_MIT) && (Markdown::get_heading_level(md) <= 2)) if (GENERAL_POINTER_IS_NULL(md->user_state) == FALSE) return RETRIEVE_POINTER_text_stream(md->user_state); return NULL; }