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.
# 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}}]]
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).
[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:
include BarelyML.h and declare a display Component: BarelyMLDisplay bmlDisplay;
(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:
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.
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.
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!
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?
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.