Bike 1.4 Preview (65) "Rich Text"

  • Added Format menu with Bold, Italic, and Clear Formatting

This is a work in progress. I expect final “rich text” release is at least a few more weeks away.

Also I’m in the process of moving development from my 2015 iMac to a new MacBook. Yeah, and thank your Bike customers! :slight_smile: With that said be on lookout for any errors/warning during update process. I “think” I copied over all the right keys and build environment…

To get preview releases through Bike’s software update select: Bike > Preferences > General > Include “preview” releases.

3 Likes

I’m looking for a few different kinds of feedback here:

  • Bugs?
  • Format Options
  • File formats for rich text
  • Anything else you think is relevant

Format Options

I want format options to be useful and strait forward in this release.

For example “Bold”, “Italic” seem right. On the other hand adding a “Highlight” formatting option doesn’t seem right. (Maybe in future, but not for this release). My logic for saying “Highlight” wasn’t right is because I saw no direct mapping to HTML, but now I see that HTML has a <mark> element that seems pretty direct mapping for highlight. So maybe that would be good… anyway I don’t want to add a “ton” of formatting options at start, but I would like to add once that would be useful to you now in Bike.

With that in mind what additional formatting options would you like to see?

Note: I’m not talking about styling options. Longer term I’ll add stylesheets to bike so that you can customize what exactly “Bold” and “Italic” look like when displayed. Right now I’m just looking for model level formatting options.

And really Bold maps to “strong” and “Italic” maps to “em” in the file format. I just figured best to use normal terms for the menu items.

Format Options (Links)

Links are a special case formatting option. I think rich text links (where link is hidden behind text) is a really important feature for Bike, but I don’t think they should be added to this release. There are a lot of UI options to consider for links and I think better to have a “Links” only release where that’s the focus.

File formats for rich text (.bike)

How is rich text get encoded into .bike?

First I use the libxml2 parser in XML mode (with recover option enabled) to read and write Bike files. Generally Bike files follow XML (not HTML) conventions.

In particular whitespace is significant. And in Bike’s case it preserves whitespace in leaf elements. For example to encode " Hello world " in Bike (note double spaces) I encode directly as:

<p>  hello  world  </p>

To encode: “hello world” Bike 1.4 uses:

<p>
	<strong>hello</strong>
	<span>  </span>
	<em>hello</em>
</p>

Whitespace is preserved in leaf elements and discarded everywhere else. Generally I think this works pretty well, makes it pretty simple to read/write Bike files, and follows XML format conventions.

Web Browser viewing

A nice feature of Bike’s file format is that it’s “mostly” a subset of HTML.

The nice to have goal is that you can open a .bike file in your browser and you see all information. Unfortunately I don’t know how to make this above XML encoding approach also show exact whitespace in a web browser without javascript pre-processing.

For example in the above example the web browser will insert a space between each spanning element… and will collapse any extra spaces. I haven’t been able to find a set of css styles to fully correct for this.

I think I can correct for everything with some Javascript preprosessing that for each <p>:

  1. Delete all whitespace text nodes that are not part of a leaf element. (Basically remove the pretty-print formatting)
  2. Add style p { white-space: pre-wrap; }

But that seems messy. Thoughts or better ideas welcome! :slight_smile:

File formats for rich text (.opml, .txt)

Right now if you save to these formats rich text attributes are lost. Hope to fix this before final 1.4 release, but focus right now is getting .bike working properly.

1 Like

Licensing heads up…

I just noticed that I didn’t yet add code to require a Bike license for rich text editing. Just so there’s no surprises… I do plan to add that restriction. Rich text editing will require a license. At the same time I have (and will keep it that way) removed license requirement for editor preferences panel.

1 Like

Looking good : )

One vote here for something like <s> ... </s> elements in paragraphs.

(After a week of adding @done tags, I look forward to the possibility of using strikethrough as an alternative)

3 Likes

I can add <s>, but I’m not sure it makes for a good replacement for what you are doing with @done tags.

The formatting features that I’m adding right now are all associated at the level of individual characters. I think you would instead want to use a row level association for @done state. That will be possible eventually, but needs me to fully implement stylesheets first.

1 Like

I agree – and stylesheets sound perfect – just thinking about a short-term measure in the meanwhile : )

1 Like

Very nice! I can definitely see uses for <s> (strikethrough) and <mark> (highlight) formatting. I don’t think <s> should replace @done, as they serve different purposes in my mind. @done strikes the entire row, whereas <s> could be used to denote a change of word, idea, phrasing, etc. I find often being able to see previous choices I’ve struck-off ruled out so I don’t continue to revisit them.

Highlighting (via <mark>) would be quite useful in drawing additional focus to something that requires further attention. ie: incomplete, fact-check, word choice, etc.

4 Likes

Glad to have access to bold and italic now. I would really like the ability to set “bold” as a style default for all rows of a certain level… like, top level, or top and second level, et cetera.

Highlighting seems pretty useful as well.

Beyond that, I don’t want for a lot. I definitely don’t want fully styled text editing in Bike… keeping it simple helps me just keep writing.

And I notice that in this version, these commands do not work when in outline editing mode… would prefer if I could bold a whole row when in outline mode.

This won’t be possible with this release, but I expect it will be possible once I add stylesheets. That’s where you’ll be able to define rules to automatically perform styling based on other state.

Oops, will fix that.

1 Like

and the standard tools like textutil seem very happy with it.

Here’s a Rube Goldberg sketch of a Copy as RTF for selections in Bike:

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    ObjC.import("AppKit");

    // Copy Bike selection as RTF
    // Ver 0.01

    // Rob Trew @2022

    // main :: IO ()
    const main = () => {
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0);

        return either(
            alert("Copy Bike selection as RTF")
        )(
            x => x
        )(
            doc.exists() ? (
                bindLR(
                    menuItemClickedLR("Bike")([
                        "Edit", "Copy", "Copy"
                    ])
                )(() => bindLR((
                    delay(0.1),
                    clipOfTypeLR(
                        "com.hogbaysoftware.bike.xml"
                    )
                ))(xml => {
                    const
                        // XML tag skipped
                        f = "| /usr/bin/tail -n +2 |",
                        opts = "-convert rtf -stdin -stdout",
                        maybeRTF = Object.assign(
                            Application.currentApplication(), {
                                includeStandardAdditions: true
                            }
                        )
                        .doShellScript(
                            `echo '${xml}' ${f} textutil ${opts}`
                        );

                    return maybeRTF.startsWith("{\\rtf") ? (
                        Right(
                            (
                                setClipOfTextType("public.rtf")(
                                    maybeRTF
                                ),
                                "Bike selection copied as RTF."
                            )
                        )
                    ) : Left(maybeRTF);
                }))
            ) : Left("No documents open in Bike")
        );
    };

    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };


    // clipOfTypeLR :: String -> Either String String
    const clipOfTypeLR = utiOrBundleID => {
        const
            clip = ObjC.deepUnwrap(
                $.NSString.alloc.initWithDataEncoding(
                    $.NSPasteboard.generalPasteboard
                    .dataForType(utiOrBundleID),
                    $.NSUTF8StringEncoding
                )
            );

        return 0 < clip.length ? (
            Right(clip)
        ) : Left(
            "No clipboard content found " + (
                `for type '${utiOrBundleID}'`
            )
        );
    };

    // menuItemClickedLR :: String ->
    // [String] -> Either String IO String
    // eslint-disable-next-line max-lines-per-function
    const menuItemClickedLR = strAppName => menuParts => {
        const intMenuPath = menuParts.length;

        return 1 < intMenuPath ? (() => {
            const
                appProcs = Application("System Events")
                .processes.where({
                    name: strAppName
                });

            return 0 < appProcs.length ? (() => {
                Application(strAppName).activate();
                delay(0.1);

                return bindLR(
                    menuParts.slice(1, -1)
                    .reduce(
                        (lra, x) => bindLR(lra)(a => {
                            const menuItem = a.menuItems[x];

                            return menuItem.exists() ? (
                                Right(menuItem.menus[x])
                            ) : Left(
                                `Menu item not found: ${x}`
                            );
                        }),
                        (() => {
                            const
                                k = menuParts[0],
                                menu = appProcs[0].menuBars[0]
                                .menus.byName(k);

                            return menu.exists() ? (
                                Right(menu)
                            ) : Left(`Menu not found: ${k}`);
                        })()
                    )
                )(xs => {
                    const
                        k = menuParts[intMenuPath - 1],
                        items = xs.menuItems,
                        strPath = [strAppName]
                        .concat(menuParts).join(" > ");

                    return bindLR(
                        items[k].exists() ? (
                            Right(items[k])
                        ) : Left(`Menu item not found: ${k}`)
                    )(x => x.enabled() ? (
                        x.click(),
                        Right(`Clicked: ${strPath}`)
                    ) : Left(
                        `Menu item disabled : ${strPath}`
                    ));
                });
            })() : Left(`${strAppName} not running.`);
        })() : Left(
            "MenuItemClickedLR needs a menu path of 2+ items."
        );
    };


    // setClipOfTextType :: String -> String -> IO String
    const setClipOfTextType = utiOrBundleID =>
        txt => {
            const pb = $.NSPasteboard.generalPasteboard;

            return (
                pb.clearContents,
                pb.setStringForType(
                    $(txt),
                    utiOrBundleID
                ),
                txt
            );
        };


    // --------------------- GENERIC ---------------------
    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => m.Left ? (
            m
        ) : mf(m.Right);


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => "Left" in e ? (
            fl(e.Left)
        ) : fr(e.Right);

    // MAIN ---
    return main();
})();
1 Like

Nice to see! I’ve thought about building RTF in as another supported file format, but I think that won’t happen for a while, so this is good to have.

2 Likes

Dumb question, but wow would you go about using the script?

You can bind it to a keystroke with something like Keyboard Maestro:

BIKE - Copy as Rich Text (RTF).kmmacros.zip (3.2 KB)

or FastScripts.

(and then just select the text and use the keystroke)

1 Like

There’s also some Information on using scripts here:

2 Likes

I wouldn’t suggest this just for Bike, but if you don’t already have Keyboard Maestro it’s really worth purchasing! In the case of Bike, @complexpoint has made a group of very useful macros for Keyboard Maestro that are activated with a keyboard shortcut.

… yes, it’s worth learning about Apple Shortcuts but Keyboard Maestro can solve sooooo many issues. And it’s only $36 (last I looked).

Thanks! I’ve been on the fence about Keyboard Maestro for some time as I don’t do that much automation, and what little I do I typically muddle through with AppleScript, Automator, or Shortcuts. That said, I’ve been looking for an alternative to TextExpander since they went subscription and with all of the great things I’ve heard about Keyboard Maestro it’s probably time for me to jump in. Thanks for the push!

I am very far from being a “master” of KM. Really just a beginner. But there are a number of very nice folks who create KM macros for specific apps. So it’s one of those applications that allow you to benefit from the generosity of others who have better and more advanced skills.

1 Like

I am planning to add “Strikethrough” and “Highlight”.

Right now I have: (Menu Name)(XML Element)(Keyboard Shortcut)

  • Strong <strong> ⌘B
  • Emphasis <em> ⌘I
  • Strikethrough <s> ⇧⌘-
  • Highlight <mark> ⇧⌘

Questions:

  • Do I need “Underline”? On positive it’s a standard in all macOS native UI’s that I’ve seen. On negative it seems outdated, is no longer natively supported on web (as semantic element, I’m not talking styling), not supported in Markdown… and no one’s asked for it yet.

  • What about “Code”. I think I’ll add it since many other apps support it and I think I might use myself. Any thoughts on good keyboard shortcut?

  • Should I use “Strong” and “Emphasis” to match tag names or “Bold” and “Italic” to match macOS standard names.

  • Same question with “Highlight” … seems to be the term that most apps use for such formatting, but “Mark” is what the underlying tag will be.

3 Likes

My opinions:

Drop underline. Leave it for styles.

Code is cool. ⌘⇧C.

Yes, on tag names.

What would highlight look like? “Mark” could confuse some folks.

1 Like
  • I agree with Jim that underline can probably be skipped – a hangover from typewriters, I think.

  • I think its OK for the English (and other language) menu labels to diverge from the underlying tag names. People are used to “Bold” and “Italic”, and I guess that will probably be the main styling use of <strong> and <em>, neither of which will have close relationships to the localisation names in other human languages anyway.

  • Code – yes, very useful :slight_smile: (and helpful for any export mapping to Markdown etc)

1 Like