Markdown Functions

In this section, we provide an example of a package that facilitates the creation of HTML files from Markdown documents. Markdown is a popular format for technical documentation due to its focus on clean, readable text, easy integration of code examples, and support for images and hyperlinks. We have integrated Markdown conversion functions into the ACF language to simplify the process of generating HTML documents.

The result is a blistering fast end product, the html files loads extremely fast, are looking good, and the functions to do the converting is also very fast. We processed the 50 documents in this manual in less than 0.5 seconds in total. With the CSS files, you can style the documents in any way you want.

Background

For our documentation project, we've chosen to create HTML documents from Markdown source files. To achieve this, we use a FileMaker application that includes:

Workflow

The Markdown to HTML conversion process involves three phases:

  1. Pre-processing: This step involves copying images from Markdown documents to the HTML/images folder and updating image URLs in the Markdown documents to point to their new locations.
  2. Markdown to HTML Conversion: The Markdown documents are converted to HTML using ACF functions.
  3. Post-processing: After conversion, we add a header and a left column navigation bar to the HTML documents.

Pre-processing

In the pre-processing phase, we perform the following tasks:

The Markdown image tag has this format:

![image 1](../../../Desktop/regex-explanation.png)

We use a regular expression to extract image tags from the Markdown document. The regex pattern !\[[^\]]*\]\(((.+?\/)?([^\/)]+))\) captures image tags, including full paths, directory paths, and file paths.

This is explained this way: (Screen shot from regex101.com)

image 1

Here is the different Paths we need to take care of:


Original Image path when dragged into the MarkDown document from the Desktop:  
../../../Desktop/imageab.png

Relative Image path for its new location as seen from the MarkDown document: 
 ../html_root_folder/images/imageab.png

Resulting image path in the HTML document: 
images/imageab.png

The absolute path of the document folder is: 
/Users/ole/Projects/ACFmanual/mdDocs/

The absolute path of the HTML path is: 
/Users/ole/Projects/ACFmanual/html_root_folder/

Now, we have some filesystem fun to pick apart all these paths and copy files. This is done in the MoveImages function seen below.

Markdown to HTML Conversion

After completing the pre-processing stage, we have all the referenced images available in the HTML/images folder. We utilize the built-in functions in the ACF language to perform the Markdown to HTML conversion. This function requires the source Markdown file, style/theme information defined for the markdown2html ACF function, and the destination HTML file to be created.

Once this function is executed, we obtain a bare HTML file that contains only the text and images from the Markdown document, without any navigation, headers, or additional elements. To transform this into a comprehensive manual, we require additional elements, which are handled by the post-processing function.

Post-processing

The post-processing function's role is to integrate the generated HTML into a complete page that includes a left sidebar, top bar, navigation, and bottom section.

  1. The left navigation menu needs to be generated. We retrieve all the documents from a table named ACF_Documents, extracted from the database using an SQL query. The left navigation menu is then constructed in a loop, organizing documents into category sections and using HTML ul/li tags. CSS styling is applied to enhance its appearance.
  2. The original HTML file is read and divided into the HEAD and BODY sections.
  3. The content for the top bar is sourced from user preferences.
  4. All these components, including DIV tags to structure the complete document, are placed into an array.
  5. We utilize the implode function to merge all the parts within this array to create the final document.
  6. A simple substitution is employed to remove certain image path prefixes in a cleanup operation.
  7. We iterate through all the documents again, replacing titles within the document with links leading to their respective pages.

Finally, the fully assembled HTML file is written back to disk, completing the process.

The ACF code functions sumary

Here's a breakdown of the functions involved in Markdown conversion:

  1. The Package Header:

    package MarkDownFunctions "MarkDown funksjoner for dok prosjektet..."; 
    
  2. The Function for Preprocessing the Markdown Document and Copying Image Files:

    This function extracts image tags from the Markdown document, copies image files to the HTML/images folder, and updates image URLs.

    function MoveImages(string SourceFile, string html_root, string relpath)
    
  3. The Post-processing Function to Add Header and Left Column to the Document:

    This function adds a header and a left column navigation bar to the generated HTML document.

    function post_processing(string htmlfile, string removeText)
    
  4. The Markdown Document Conversion Function:

    This function orchestrates the entire Markdown to HTML conversion process.

    function ConvertMarkdown(string sourcefile, string htmlfile, string html_root, string style, string codestyle, string removeText)
    
  5. A Utility Function to Create HTML Filenames:

    This function generates HTML filenames based on the Markdown file's name.

    function createHTML_Filename(string file, string html_path)
    

The full listing Listing:

The Package Header:


package MarkDownFunctions "MarkDown funksjoner for dok prosjektet..."; 

Here is the MoveImages function:


function MoveImages ( string SourceFile, string html_root, string relpath ) 

    print "MoveImages  enters...."; 
    int x = open ( SourceFile, "r"  ); 
    string md = read (x ) ; 
    close ( x ) ; 
    print "Pre prosessing"; 
    string regex = "!\[[^\]]*\]\(((.+?\/)?([^\/)]+))\)"; // used to extract image tags...
    // gr 0 = full match, gr 1 = full path, gr 2 = directory path, gr 3 = file path. 
    array string tags = regex_extract ( regex, md ) ; 
    int no = sizeof ( tags ) ; 
    if ( no == 0 ) then
        print "Pre prosessing - no tags"; 
        return "";      
    end if
    
// Getting the Markdown document folder where relative images starts from. 
    string docFolder = regex_replace ( "^(.+\/)[^\/]+$", SourceFile, "\1" ) ; 

    int i, cnt = 0; 
    string fulltag, fullpath, fileFullPath, dir, file, newPath, newRelPath, newtag, res; 
    
// Loop all tags, and process each one of them. 
    for ( i= 1, no)
        print "\n" + tags[i]; 
        fulltag = getValue ( tags[i], 1);
        fullpath = getValue ( tags[i], 2);
        fileFullPath = fullpath; 
        dir = getValue ( tags[i], 3);
        file = getValue ( tags[i], 4);
        if ( file_exists ( fullpath ) == 0 ) then
             // probably a relative path. Prefix with doc folder to get absolute path. 
            fullpath = docFolder + fullpath; 
        end if
        if ( file_exists ( fullpath ) ) then
            newRelPath = relpath + "images/" + file; 
            newPath = html_root + "/images/" + file; 
            if ( file_exists ( newPath ) == 0) then
                res = copy_file ( fullpath, newPath ) ; 
                if ( res == "OK") then
                    newtag = substitute ( fulltag, fileFullPath, newRelPath ) ; 
                    md = substitute ( md, fulltag, newtag ) ; 
                    cnt++; 
                end if
            end if
        end if
    end for
    if ( cnt > 0 ) then
        // save the modified MarkDown document. Take a backup first, in case we did something nasty...
        res = copy_file ( SourceFile, SourceFile+".bak" ) ; 
        // Write back to file. 
        x = open ( SourceFile, "w"  ); 
        write ( x, md ) ; 
        close ( x ) ;
    end if
    return "OK"; 
END

The Post processing function to add header and left column to the document:


function post_processing (string htmlfile, string removeText)

/* Called from ConvertMarkdown 
    Open the generated HTML document, that is a plain page of the markdown doc, 
    and put on a header, and a left navigation bar.
   */

    print "post processingn";
    int x = open ( htmlfile, "r"  ); 
    string html = read (x ) ; 
    close ( x ) ; 
    string body_part = between ( html, "<body>", "</body>" ) ; 
    string head_part = between ( html, "<head>", "</head>" ) ; 
    string stylesheet = '<link rel="stylesheet" type="text/css" href="include/css/style.css">';
    string top_bar = Preferences::topp_nav; 
    
    // Build the left nav
    string left_nav = Preferences::Left_nav; 
     
doclist = @ExecuteSQL ( "SELECT Title, slug_filename, Category FROM ACF_Documents LEFT OUTER JOIN ACF_Categories as c ON c.PrimaryKey = Category_UUID ORDER BY Sorting, Category" ; "||" ; "|*|")@;
    
    print doclist;  // Debugg print to consolle. 

    array string docs = explode ("|*|", doclist ), flt ; 
    int noDocs = sizeof ( docs ); 
    int i; 
    string curCat = "noone"; 
    for (i = 1, noDocs)
        flt = explode ( "||", docs[i]); 
        if (flt [3] != curCat ) then
            if ( curCat != "noone") then
                left_nav += "</ul>\n"; 
            end if
            left_nav += "<h4>" + flt[3] + "</h4>\n"; 
            left_nav += '<ul class="linklist">\n'; 
            curCat = flt[3]; 
        end if
        left_nav += "<li>" + flt[1] + "</li>\n"; 
    end for
    left_nav += "</ul>\n"; 
    
    // Merge the parts
    array string fileparts = {"<!DOCTYPE html>\n<head>", head_part, stylesheet, "</head>\n<body>", 
        '<div class="hd">', top_bar, 
        '</div><div class="content_band"><div class="left">',left_nav, 
        '</div><div class="content">', body_part,  
        "</div></div>", "</body>n</html>\n" }; 
        
    string result = implode ( "\n", fileparts ) ; 

    // Remove Image path prefixes 
    result = substitute (result, removeText, ""); 
    
    // substitute all titles in the documents with a link to that page. 
    for (i = 1, noDocs)
        flt = explode ( "||", docs[i]); 
        // result = substitute (result, flt[1], '<a href="'+flt[2]+'">' + flt[1]+"</a>"); 
    // Improved: Use regex-replace instead to substitute titles matching whole words
    // and not part of words, that looks i bit strange. 
        result = regex_replace ( "\b"+flt[1]+"\b", result, '<a href="'+flt[2]+'">' + flt[1]+"</a>");
        
    end for

    // Write back to file. 
    x = open ( htmlfile, "w"  ); 
    write ( x, result ) ; 
    close ( x ) ;
    print "End-post-processingn"; 
    return "OK"; 
end

The markdown document conversion function:

This function below uses the two functions above to complete the full conversion.


function ConvertMarkdown (string sourcefile, string htmlfile, string html_root, string style, string codestyle, string removeText)
    
    print "Convert markdown enters....\n"; 
    set_markdown_html_root ( html_root ) ; 
    string sf = sourcefile, df = htmlfile; 
    $$SourceFile = ""; 
    if (sf == "") then
        sf = select_file ("Velg en Markdown Fil?"); 
        $$SourceFile = sf; 
    end if
    if (sf != "") then
        if (df == "") then
         df = save_file_dialogue ("Lagre fil som?", "xx.html", html_root);
        end if
        if (df != "") then
                 // Pre processing - Handle images in document. 
            string res = MoveImages ( sf, html_root, removeText );
                // Main conversion
            res = markdown2html (sf, style+","+codestyle, df); 
                // Post processing...
            res = post_processing (df, removeText); 
        end if 
    end if
    return "OK";
end

A small utility function to produce html filenames:


function createHTML_Filename ( string file, string html_path ) 

    string newfile = substitute ( lower(file), " ", "_"); 
    newfile = substitute ( newfile, ":", "_"); 

    if (right(html_path, 1) != "/") then
        html_path += "/"; 
    end if
    return html_path + newfile + ".html";
end

This example demonstrates how ACF can be used to automate the conversion of Markdown documents into HTML, making it easier to manage technical documentation projects.