Using ThreeParams.dll

DataFlex wrappers for Markdown, Handlebars, PDF page counting, and case-insensitive replace.

Overview & Setup

ThreeParams.dll is a native library that exposes four families of helper functions to DataFlex. Each is fronted by a small .pkg wrapper that handles buffer sizing, UTF-8 conversion, and error reporting so you can call simple, idiomatic DataFlex functions.

PackagePublic function(s)Purpose
Markdown.pkgMarkdownToHTML, WrappedMarkdownToHTML, BlankLinesToHTMLConvert Markdown text to HTML.
HandleBars.pkgRenderHandleBarsRender a Handlebars template against a JSON String.
PdfPageCount.pkgGetPdfPageCountReturn the number of pages in a PDF file.
CIReplace.pkgCIReplace, CIReplacesCase-insensitive string replacement (first / all).

Installation

Place threeparams.dll in the workspace Programs folder, place the packages in the workspace AppHTML folder, then Use the packages you need:

Use Markdown.pkg
Use HandleBars.pkg
Use PdfPageCount.pkg
Use CIReplace.pkg
All functions exchange text with the DLL as UTF-8. The wrappers append the required null terminators and resize the receiving buffer automatically — including a second call if the first buffer estimate was too small — so you never manage memory directly.
↑ top

Markdown → HTML

MarkdownToHTML String

Converts a Markdown string into an HTML fragment (no surrounding <html> wrapper).

Function MarkdownToHTML Global String sMarkdown Returns String

Example

String sHtml
Move (MarkdownToHTML("# Invoice\n\nThank you for your **order**.")) to sHtml
// sHtml -> "<h1>Invoice</h1><p>Thank you for your <strong>order</strong>.</p>"

Before conversion the text is passed through BlankLinesToHTML (below), so the custom {N blank lines} token is honoured. On failure the function returns the literal string <h1>Error Displaying Text. Contact your Administrator</h1>.

WrappedMarkdownToHTML String

Same conversion as above, but the result is embedded in a complete, styled HTML document (fonts, table styling, hover rows, etc.). Use it when you want a ready-to-display page rather than a fragment — e.g. loading into a cWebView or writing to a file.

Function WrappedMarkdownToHTML Global String sMarkdown Returns String
String sPage
Move (WrappedMarkdownToHTML(sReportMarkdown)) to sPage
// sPage is a full <html>…</html> document with embedded CSS

BlankLinesToHTML String

Helper used internally by the converters. It replaces tokens of the form {N blank lines} (where N is 2–20) with a spacer <div> of N em height. Useful for forcing vertical whitespace that plain Markdown cannot express.

Function BlankLinesToHTML Global String sText Returns String
Move (BlankLinesToHTML("Above{3 blank lines}Below")) to sText
// -> "Above<div style='margin-bottom:3em'></div>Below"
Tables: The package also ships an oMarkdownParser object with helpers (ParseMarkdownRow, ParseMarkdownTable, IsMarkdownSeparator) for pulling structured data out of a Markdown table into DataFlex arrays — handy when you need the values, not just the rendered HTML.
↑ top

Handlebars Templates

RenderHandleBars String

Renders a Handlebars template, substituting values from a JSON data object. Returns the rendered text, or an empty string on error.

Function RenderHandleBars Global String sTemplate String sJSON Boolean isStopError Returns String
ParameterMeaning
sTemplateThe Handlebars template source.
sJSONA JSON string supplying the template's data context.
isStopErrorIf False, a failure raises a UserError. If True, the call fails silently and you inspect gsLastHandleBarsError yourself.

Example

String sTemplate sJSON sOut

Move "Dear {{customer.name}},\n\nYour balance is {{balance}}." to sTemplate
Move '{"customer":{"name":"Ada Lovelace"},"balance":"$42.00"}' to sJSON

Move (RenderHandleBars(sTemplate, sJSON, False)) to sOut
// sOut -> "Dear Ada Lovelace,\n\nYour balance is $42.00."

Looping over a list

Move "Items:\n{{#each items}}- {{this.name}} ({{this.qty}})\n{{/each}}" to sTemplate
Move '{"items":[{"name":"Widget","qty":3},{"name":"Gadget","qty":1}]}' to sJSON
Move (RenderHandleBars(sTemplate, sJSON, False)) to sOut
// Items:
// - Widget (3)
// - Gadget (1)
Combine the two engines: render a Handlebars template into Markdown, then feed that to WrappedMarkdownToHTML to produce a finished, data-driven HTML document.
↑ top

PDF Page Count

GetPdfPageCount Integer

Returns the number of pages in a PDF file, or a negative error code.

Function GetPdfPageCount Global String sFilename Boolean isStopError Returns Integer
ParameterMeaning
sFilenameFull path to the PDF file.
isStopErrorIf False, errors raise a UserError. If True, errors are silent — check the negative return value and gsLastPdfPageCountError.

Example

Integer iPages
Move (GetPdfPageCount("C:\docs\contract.pdf", True)) to iPages
If (iPages < 0) Begin
    Showln "Could not read PDF: " gsLastPdfPageCountError
End
Else Begin
    Showln "The document has " iPages " page(s)."
End
CodeMeaning
-1Invalid filename (null, not valid UTF-8, or file does not exist).
-2Not a valid PDF (missing PDF header or unparseable).
↑ top

Case-Insensitive Replace

Two wrappers mirror the parameter order of DataFlex's built-in Replace / Replaces functions (Find, Source, With), but match without regard to letter case.

CIReplace String — replace first match

Function CIReplace Global String sFind String sSource String sWith Returns String

CIReplaces String — replace all matches

Function CIReplaces Global String sFind String sSource String sWith Returns String

Example

String s

Move (CIReplace("hello", "HELLO world, hello again", "Hi")) to s
// First match only -> "Hi world, hello again"

Move (CIReplaces("hello", "HELLO world, hello again", "Hi")) to s
// All matches      -> "Hi world, Hi again"
Note the argument order: Find first, then Source, then With. The replacement preserves the casing of sWith exactly as supplied; only the matching is case-insensitive.
↑ top

Error Handling

Each package keeps the last error message in a global string and exposes a …ErrorToText translator. When a function takes an isStopError flag, pass True to suppress the automatic UserError and handle problems yourself.

AreaLast-error globalTranslator function
HandlebarsgsLastHandleBarsErrorHandleBarsErrorToText
PDF page countgsLastPdfPageCountErrorPdfPageCountErrorToText
CI ReplacegsLastCIReplaceErrorCIReplaceErrorToText

Handlebars error codes

CodeMeaning
-1Null input pointer.
-2Invalid UTF-8 in input string.
-3Negative or invalid buffer length.
-4JSON parse error.
-5Handlebars template compile error.
-6Template render error.
-7Rendered output not valid UTF-8.

Case-insensitive replace error codes

CodeMeaning
-1A required pointer was null.
-2Source was not valid UTF-8.
-3Find was not valid UTF-8.
-4Replacement was not valid UTF-8.
↑ top

Worked Example — Chaining the Engines

The real power comes from combining the functions. This example builds a customer invoice in three stages:

  1. Handlebars merges live data (a JSON object) into a Markdown template.
  2. Markdown → HTML turns that merged Markdown into a styled HTML page.
  3. The finished HTML is ready to display, email, or save.

Step 1 — The Handlebars + Markdown template

Note that the template is itself Markdown — headings, bold, a table, and the {2 blank lines} spacer extension — with Handlebars tags woven through it.

# Invoice {{invoiceNo}}

**Billed to:** {{customer.name}}
**Date:** {{date}}

| Item | Qty | Price |
|------|----:|------:|
{{#each lineItems}}| {{this.name}} | {{this.qty}} | {{this.price}} |
{{/each}}

{{#if isPaid}}
*Paid in full — thank you!*
{{else}}
**Balance due: {{total}}**
{{/if}}

{2 blank lines}

> Questions? Reply to this email.

Step 2 — The data context (JSON)

{
  "invoiceNo": "INV-1042",
  "date": "2026-06-06",
  "customer": { "name": "Ada Lovelace" },
  "lineItems": [
    { "name": "Widget", "qty": 3, "price": "$1.00" },
    { "name": "Gadget", "qty": 1, "price": "$4.00" }
  ],
  "isPaid": false,
  "total": "$7.00"
}

Step 3 — The DataFlex code

Use HandleBars.pkg
Use Markdown.pkg

Procedure BuildInvoice
    String sTemplate sJSON sMarkdown sHtml

    // --- 1. Load the Handlebars/Markdown template and the JSON data ---
    Move (ReadFileToString("invoice.hbs.md")) to sTemplate   // your own loader
    Move (ReadFileToString("invoice.json"))   to sJSON

    // --- 2. Handlebars: merge data into Markdown ---
    Move (RenderHandleBars(sTemplate, sJSON, True)) to sMarkdown
    If (gsLastHandleBarsError <> "") Begin
        Showln "Render failed: " gsLastHandleBarsError
        Procedure_Return
    End

    // --- 3. Markdown -> a complete, styled HTML document ---
    Move (WrappedMarkdownToHTML(sMarkdown)) to sHtml

    // sHtml is now a full <html>…</html> page, ready to
    // display in a cWebView, email, or write to disk.
    Send WriteStringToFile sHtml "invoice.html"   // your own writer
End_Procedure

What comes out

After step 2, sMarkdown holds fully-merged Markdown:

# Invoice INV-1042

**Billed to:** Ada Lovelace
**Date:** 2026-06-06

| Item | Qty | Price |
|------|----:|------:|
| Widget | 3 | $1.00 |
| Gadget | 1 | $4.00 |

**Balance due: $7.00**

{2 blank lines}

> Questions? Reply to this email.

Step 3 then renders that to HTML — the table becomes a styled <table>, **Balance due** becomes bold, the {2 blank lines} token becomes a 2 em spacer, and the whole thing is wrapped in the document shell with embedded CSS.

Why chain this way? Handlebars handles the logic (loops, conditionals, field substitution) while Markdown handles the presentation in a form that is easy for non-programmers to edit. Keeping the template as a plain .md file means business users can tweak wording and layout without touching DataFlex code or HTML.

↑ top

Appendix A — Markdown Quick Reference

Markdown is a lightweight plain-text format that converts cleanly to HTML.

Headings

# Heading 1
## Heading 2
### Heading 3

Emphasis

MarkdownResult
*italic* or _italic_italic
**bold**bold
~~strikethrough~~strikethrough
`inline code`inline code

Lists

- Bullet item
- Another bullet
  - Nested bullet

1. First
2. Second
3. Third

Links & Images

[Link text](https://example.com)
![Alt text](image.png)

Blockquotes

> This is a quoted line.
> It can span multiple lines.

Code blocks

```
Multiple lines
of preformatted code
```

Tables

| Name    | Qty | Price |
|---------|----:|------:|
| Widget  |   3 | $1.00 |
| Gadget  |   1 | $4.00 |

Use colons in the separator row to set alignment: :--- left, :--: centre, ---: right. Escape a literal pipe inside a cell as \|.

Horizontal rule

---

Extension: blank-line spacer

Specific to these wrappers — insert {N blank lines} (with N from 2 to 20) to add N em of vertical space:

Section one.

{4 blank lines}

Section two appears well below.

↑ top

Appendix B — Handlebars Quick Reference

Handlebars is a logic-light templating language. A template is text containing {{expressions}} that are replaced with values from a JSON data context.

Basic substitution

Hello {{name}}!

With data {"name":"World"}Hello World!

Nested properties

{{customer.address.city}}

HTML escaping

SyntaxBehaviour
{{value}}HTML-escaped output (safe by default).
{{{value}}}Raw, unescaped output — use only with trusted content.

Conditionals

{{#if isPaid}}
  Paid in full. Thank you!
{{else}}
  Balance outstanding: {{balance}}
{{/if}}

{{#unless inStock}}
  Currently out of stock.
{{/unless}}

Iteration

{{#each lineItems}}
  {{this.description}} — {{this.amount}}
{{/each}}

Inside an {{#each}} block, {{this}} is the current element, {{@index}} is its zero-based position, and {{@first}} / {{@last}} flag the ends of the list.

Context with #with

{{#with customer}}
  {{name}} of {{address.city}}
{{/with}}

Comments

{{! This note will not appear in the output }}
The data argument to RenderHandleBars must be valid JSON; a parse error returns code -4. Watch quoting when building JSON inside DataFlex string literals — single-quoted DataFlex strings let you embed the double quotes JSON requires.

↑ top

Support

Need a hand? If you want help using these functions or run into a problem, contact our support team at support@threeparams.com and we'll be glad to assist.