Markup display for JUCE

Hi everyone,

I’ve released BarelyML, a JUCE-based markup display on GitHub, which is useful for displaying formatted text, tables, images, and links inside JUCE apps (for example for in-app documentation, hints, etc.). My goal was flexibility and good performance on mobile devices (scrolling tables, large areas for links, imports simple Markdown, DokuWiki and AsciiDoc content, etc.). Less than 1500 lines, under MIT license, and only dependent on JUCE.

The git repository contains also an interactive demo (PIP => just drag BarelyMLDemo.h onto a Projucer window and it will create the project for you), as a quick way to test the rendering of documents in various formats. The demo also serves as example code for the integration of BarelyML in JUCE applications.

Best regards,
Fritz

[Edited for clarity and to reflect the current state of the project]

25 Likes

Here’s a little test:

The markup document looks like this:

# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5

Here's some *bold text, _some bold italic text*, and some italic text_.

Now some <c:red>_*bold italic*_ red text</c>, and some <c#52F>arbitrarily colored *bold* text</c>.

-    An unordered list item
  -  And a subitem
  2.   and a numbered subitem
  3. one more item

And some non-Latin text:
<c:lightblue>שלום</c>, מה שלומך? (Hebrew)
<c:green>سلام</c>، حالت چطوره؟ (Persian)
*สวัสดีค่ะ* เป็นไงบ้าง (Thai).

^ Table Heading ^ Column 2     ^ Column 3 with a really long header ^
| Row 2         |              | |
|               | Row 3        | |
^ Also a header | Not a header | |

INFO: This is an info paragraph (blue tab).

HINT: This is a hint paragraph (green tab).

IMPORTANT: This is an important paragraph (red tab).

CAUTION: This is a caution paragraph (yellow tab).

WARNING: This is a warning paragraph (orange tab).

[[https://juce.com|JUCE Website]]

[[http://mnsp.ch|{{sunrise.jpeg?200}}]]

Thanks a ton for creating and sharing this!

2 Likes

You’re welcome. I’m happy if it is useful to other people, too!

Just as a side note: this is the very first release and there are almost certainly still bugs and missing features. So if you find any of those, please let me know (either here or via GitHub).

Best regards,
Fritz

[Update: there is an example project now, see BarelyMLDemo.h on GitHub]
As there’s no example project on GitHub (yet), here’s some instructions on how to use BarelyML:

  1. include BarelyML.h and declare a display Component:
    BarelyMLDisplay bmlDisplay;
  2. (optional, only needed if you want to display images):
    Declare one of your own classes to be a BarelyMLDisplay::FileSource, for example like this:
    class MainComponent : public juce::Component, BarelyMLDisplay::FileSource
    and override the method getImageForFilename:
    Image getImageForFilename(String filename) override;
    This method simply produces Image objects when given a filename. You could do anything in there, like programmatically painting your images. But quite likely you’d want to implement it something like this:
Image MainComponent::getImageForFilename(String filename) {
  Image image;
  int numBytes;
  const char* data = BinaryData::getNamedResource(filename.replaceCharacter('.', '_').toRawUTF8(),numBytes);
  if (data != nullptr) {
    image = ImageFileFormat::loadFrom(data, numBytes);
  }  else {
    File file = File(getSharedResourceFolder().getFullPathName() + "/" + filename);
    image = ImageFileFormat::loadFrom(file);
  }
  return image;
}

File MainComponent::getSharedResourceFolder()
{
  File bundle = File::getSpecialLocation (File::invokedExecutableFile).getParentDirectory();

  // macOS uses Contents/MacOS structrue, iOS bundle structure is flat
 #if JUCE_MAC
  bundle = bundle.getParentDirectory().getParentDirectory();
 #endif

  // appex is in a PlugIns folder inside the parent bundle
  if (SystemStats::isRunningInAppExtensionSandbox())
      bundle = bundle.getParentDirectory().getParentDirectory();

 #if JUCE_MAC
  bundle = bundle.getChildFile ("Contents/Resources");
 #endif

  return bundle;
}
  1. Finally, you need to initialize your BarelyMLDisplay object with the right parameters and the Markup String:
  addAndMakeVisible(&bmlDisplay);
  bmlDisplay.setFileSource(this);
  bmlDisplay.setMarkupString(CharPointer_UTF8(R"(# Heading 1

Here's some <c:red>_*bold italic*_ red text</c>,
and some <c#52F>arbitrarily colored *bold* text</c>.)"),
  Font("Helvetica Neue", 20.0f, Font::FontStyleFlags::plain));

Of course you’ll also need to properly set the bounds of the component.

Alternatively, if you have a simple Markdown String, you can directly read that using the setMarkdownString method:

bmlDisplay.setMarkdownString(CharPointer_UTF8(R"(# Header

### Here's an image

![This text will be ignored](sunrise.jpeg)

### And here's a table

| Table header    | Another one |
| --------------- | ----------- |
| row 2           |             |
|                 | row 3.      |)"),
Font("Helvetica Neue", 20.0f, Font::FontStyleFlags::plain));

1 Like

Very cool contribution - thanks so much for making this and for making it MIT license for others to use. I’ll definitely be integrating this into some of my JUCE apps shortly.

1 Like

FYI, I’ve recently updated BarelyML to version 0.2 on GitHub:

Here’s what’s new:

1 Like

I still needed some improvements, so I’ve released a second update today:

I’d also highly recommend to try out the interactive demo (available as a PIP together with the source code). It’s just the easiest way to try out what you can do with BarelyML.

3 Likes

Thank you for this, very nice to be able to use markdown in juce apps.

1 Like

You’re welcome. Anyone interested in support for Org-mode?

Thanks for your work on this, and for making it freely available! We’re planning to include it with plugdata to display in-app documentation. It’s a work-in-progress:

The code was great to work with, it’s really easy to expand the syntax support yourself if needed. I added support for basic HTML-style images for example. So yeah, thanks a lot!

2 Likes

Update: I’ve released BarelyML v0.3 on GitHub:

New features:

  • Changed the definition of FileSource to enable vector graphics (SVG)
  • Adds a URLHandler class (for custom links)
  • Adds links and images in tables
  • Various minor improvements

4 Likes

Hi Timothy,

I only now read the sentence under the image you posted. First of all, thanks, I’m happy to hear that the code being pretty straightforward made things easier for you. But I’m also curious why you wanted HTML-style images. Did you implement advanced formatting options, e.g. relative to the width of the document?

Best regards,
Fritz

Hi!

Yeah, that was why: I want to be able to control the width of images more exactly. We made the docs for an online documentation page like that, and this way I could just copy-paste the whole thing and have it look nearly identical.

I also implemented support for multiple links in one block of text. I can implement it in your version and send a PR if you want, but here’s how I did it:

Array<std::pair<String, Rectangle<float>>> linkBounds;
Array<std::tuple<String, int, int>> links;

// inside parsePureText, we have this lambda that is called to save links with their start and end position in the text
 auto parseLink = [this, &attributedString](String& link, String& linkText) {
                    if (link.isNotEmpty()) {
                    // Account for the different ways whitespace is handled inside an attributedString on Windows/Linux vs Mac
#if JUCE_MAC
                        auto start = attributedString.getText().length();
                        auto end = start + linkText.length();
#else
                        auto start = attributedString.getText().replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "").length();
                        auto end = start + linkText.replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "").length();
#endif
                        links.add({ link, start, end });
                        link = "";
                    }

// Calculate the bounds of all links based on the text indices, call this when text or size has changed
void updateLinkBounds(TextLayout& layout)
    {
        linkBounds.clear();
        
        // Look for clickable links
        for (auto& [link, start, end] : links) {
            int offset = 0;
            auto currentLinkBounds = Rectangle<float>();
            for (auto& line : layout) {
                for (auto* run : line.runs) {
                    for (int i = start - offset; i < end - offset; i++) {
                        if (i < 0 || i >= run->glyphs.size())
                            continue;
                        
                        auto& glyph = run->glyphs.getReference(i);
                        auto lineBounds = Rectangle<float>(glyph.width, 14).withPosition((glyph.anchor + line.lineOrigin));
                        currentLinkBounds = linkBounds.isEmpty() ? lineBounds : currentLinkBounds.getUnion(lineBounds);
                    }
                    
                    linkBounds.add({link, currentLinkBounds.translated(0, -11)});
                    offset += run->glyphs.size();
                }
            }
        }
    }

void mouseUp(MouseEvent const& event) override
    {
        for(auto& [link, bounds] : linkBounds)
        {
            if(bounds.contains(event.x, event.y))
            {
                URL(link).launchInDefaultBrowser();
                break;
            }
        }
    }

Though looking at this again now, I realise this will go wrong if the link gets line wrapped! Better to store a RectangleList instead of calculating the union of rectangles. I also haven’t tested this yet for tables, so there is still some work to do here.

1 Like