A JS variant of titleCase, with an editable lexicon of prepositions

A variation on the title-case theme, this time written in JavaScript, and using lists of:

  • four word,
  • three word,
  • two word, and
  • single word prepositions

drawn from https://en.wikipedia.org/wiki/List_of_English_prepositions

(as well as lists of basic articles and conjunctions)

Title case (JS) with an editable lexicon of prepositions.kmmacros (30.6 KB)

These word lists can give slightly different results from:

  1. the [Gruber title case script](https://daringfireball.net/2008/05/title_case) which I believe is used in the KM Filter (title case) action, and also from
  2. @mrpasini's macro at: Improved Title Case Macro - Macro Library - Keyboard Maestro Discourse

Mileage and applications vary, and the source code and word lists are there to be edited : -)


JS Source
(() => {
    "use strict";

    // Rob Trew @2021
    //
    // titleCase ver 0.01
    //
    // A custom title-case function using word lists from:
    // https://en.wikipedia.org/wiki/List_of_English_prepositions
    //
    // and modelled on:
    // [Convert English Words to Title Case – titlecase](
    //    https://hackage.haskell.org/package/titlecase
    // )

    // main :: IO ()
    const main = () =>
        titleCase(
            Application("Keyboard Maestro Engine")
            .getvariable("rawTitle")
        );

    // ------------------- TITLE CASE --------------------

    // titleCase :: String -> String
    const titleCase = phrase => {
        // go :: Int -> [String] -> String
        const go = i =>
            xs => {
                const lng = xs.length;

                return 0 < lng ? (
                    3 < lng ? (() => {
                        const [a, b, c, d] = xs.slice(0, 4);

                        return parse4(go)(i)(a)(b)(c)(d)(
                            xs.slice(4)
                        );
                    })() : 2 < lng ? (() => {
                        const [a, b, c] = xs.slice(0, 3);

                        return parse3(go)(i)(a)(b)(c)(
                            xs.slice(3)
                        );
                    })() : 1 < lng ? (() => {
                        const [a, b] = xs.slice(0, 2);

                        return parse2(go)(i)(a)(b)(
                            xs.slice(2)
                        );
                    })() : parse1(go)(i)(xs[0])(
                        xs.slice(1)
                    )
                ) : "";
            };

        return go(1)(
            words(phrase)
        ).trim();
    };


    // parse4 :: (Int -> [String] -> String) ->
    // Int -> String -> String -> String -> String
    // [String] -> String
    const parse4 = f => i =>
        a => b => c => d => residue => {
            const go = s =>
                appendWords(s)(
                    f(1 + i)(residue)
                );

            return isFourWordPreposition(a)(b)(c)(d) ? (
                (1 === i) ? (
                    0 === residue.length ? (
                        unwords([
                            toTitle(a), b, c, toTitle(d)
                        ])
                    ) : go(
                        unwords([toTitle(a), b, c, d])
                    )
                ) : 0 === residue.length ? (
                    unwords([a, b, c, toTitle(d)])
                ) : go(
                    unwords([a, b, c, d])
                )
            ) : parse3(f)(i)(a)(b)(c)(
                [d].concat(residue)
            );
        };


    // parse3 :: (Int -> [String] -> String) ->
    // Int -> String -> String -> String
    // [String] -> String
    const parse3 = f => i =>
        a => b => c => residue => {
            const go = s =>
                appendWords(s)(
                    f(1 + i)(residue)
                );

            return isThreeWordPreposition(a)(b)(c) ? (
                (1 === i) ? (
                    0 === residue.length ? (
                        unwords([
                            toTitle(a), b, toTitle(c)
                        ])
                    ) : go(
                        unwords([toTitle(a), b, c])
                    )
                ) : 0 === residue.length ? (
                    unwords([a, b, toTitle(c)])
                ) : go(
                    unwords([a, b, c])
                )
            ) : parse2(f)(i)(a)(b)(
                [c].concat(residue)
            );
        };


    // parse2 :: (Int -> [String] -> String) ->
    // Int -> String -> String
    // [String] -> String
    const parse2 = f => i => a => b => residue => {
        const go = s =>
            appendWords(s)(
                f(1 + i)(residue)
            );

        return isTwoWordPreposition(a)(b) ? (
            (1 === i) ? (
                0 === residue.length ? (
                    unwords([toTitle(a), toTitle(b)])
                ) : go(
                    unwords([toTitle(a), b])
                )
            ) : 0 === residue.length ? (
                unwords([a, toTitle(b)])
            ) : go(
                unwords([a, b])
            )
        ) : parse1(f)(i)(a)(
            [b].concat(residue)
        );
    };


    // parse1 :: (Int -> [String] -> String) ->
    // Int -> String -> [String] -> String
    const parse1 = f => i => a => residue => {
        const
            lng = residue.length,
            iLast = lng - 1;

        return appendWords(
            [
                isOneWordPreposition,
                isConjunction,
                isArticle
            ]
            .some(p => p(a)) ? (
                (1 === i || iLast === i) ? (
                    toTitle(a)
                ) : a
            ) : toTitle(a)
        )(
            f(1 + i)(residue)
        );
    };


    // appendWords :: String -> String -> String
    const appendWords = a =>
        b => `${a} ${b}`;


    // ------------------- PREDICATES --------------------

    // isFourWordPreposition :: String -> String ->
    // String -> String -> Bool
    const isFourWordPreposition = a =>
        b => c => d => {
            const k = toLower(unwords([a, b, c, d]));

            return fourWordPrepositions.some(
                ws => k === unwords(ws)
            );
        };


    // isThreeWordPreposition :: String ->
    // String -> String -> Bool
    const isThreeWordPreposition = a =>
        b => c => {
            const k = toLower(unwords([a, b, c]));

            return threeWordPrepositions.some(
                ws => k === unwords(ws)
            );
        };


    // isTwoWordPreposition :: String -> String -> Bool
    const isTwoWordPreposition = a =>
        b => {
            const k = toLower(unwords([a, b]));

            return twoWordPrepositions.some(
                ws => k === unwords(ws)
            );
        };


    // isOneWordPreposition :: String -> Bool
    const isOneWordPreposition = a =>
        oneWordPrepositions.includes(
            toLower(a)
        );


    // isArticle :: String -> Bool
    const isArticle = s =>
        isElem(articles)(s);


    // isConjunction :: String -> Bool
    const isConjunction = s =>
        isElem(conjunctions)(s);


    // isElem :: [String] -> String -> Bool
    const isElem = xs =>
        s => xs.includes(toLower(s));


    // --------------------- LEXICON ---------------------

    // articles :: [String]
    const articles = ["a", "an", "the"];


    // conjunctions :: [String]
    const conjunctions = [
        "for", "and", "nor", "but", "or", "yet", "so"
    ];


    // oneWordPrepositions :: [String]
    const oneWordPrepositions = [
        "a", "abaft", "abeam", "aboard", "about", "above",
        "absent", "across", "afore", "against", "along",
        "alongside", "amid", "amidst", "among", "amongst",
        "an", "anenst", "apropos", "apud", "around",
        "aside", "astride", "at", "athwart", "atop",
        "barring", "behind", "below", "beneath", "beside",
        "besides", "between", "beyond", "but", "by",
        "chez", "circa", "concerning", "considering",
        "despite", "down", "during", "except", "excluding",
        "failing", "following", "for", "forenenst", "from",
        "given", "in", "including", "inside", "into",
        "like", "mid", "midst", "minus", "modulo", "near",
        "next", "notwithstanding", "of", "off", "on",
        "onto", "opposite", "out", "outside", "over",
        "pace", "past", "per", "plus", "pro", "qua",
        "regarding", "round", "sans", "save", "through",
        "throughout", "till", "times", "to", "toward",
        "towards", "under", "underneath", "unlike",
        "unto", "up", "upon", "versus", "vs.", "vs",
        "v.", "via", "vice", "vis-à-vis", "w/",
        "within", "w/in", "w/i", "without", "w/o", "worth"
    ];


    // twoWordPrepositions :: [[String]]
    const twoWordPrepositions = [
        ["according", "to"],
        ["ahead", "of"],
        ["apart", "from"],
        ["as", "for"],
        ["as", "of"],
        ["as", "per"],
        ["as", "regards"],
        ["aside", "from"],
        ["astern", "of"],
        ["back", "to"],
        ["close", "to"],
        ["due", "to"],
        ["except", "for"],
        ["far", "from"],
        ["in", "to"],
        ["inside", "of"],
        ["instead", "of"],
        ["left", "of"],
        ["near", "to"],
        ["next", "to"],
        ["on", "to"],
        ["opposite", "of"],
        ["opposite", "to"],
        ["out", "from"],
        ["out", "of"],
        ["outside", "of"],
        ["owing", "to"],
        ["prior", "to"],
        ["pursuant", "to"],
        ["rather", "than"],
        ["regardless", "of"],
        ["right", "of"],
        ["subsequent", "to"],
        ["such", "as"],
        ["thanks", "to"],
        ["that", "of"],
        ["up", "to"]
    ];


    // threeWordPrepositions :: [[String]]
    const threeWordPrepositions = [
        ["as", "opposed", "to"],
        ["as", "well", "as"],
        ["by", "means", "of"],
        ["by", "virtue", "of"],
        ["in", "accordance", "with"],
        ["in", "addition", "to"],
        ["in", "case", "of"],
        ["in", "front", "of"],
        ["in", "lieu", "of"],
        ["in", "order", "to"],
        ["in", "place", "of"],
        ["in", "point", "of"],
        ["in", "spite", "of"],
        ["on", "account", "of"],
        ["on", "behalf", "of"],
        ["on", "top", "of"],
        ["with", "regard", "to"],
        ["with", "respect", "to"]
    ];

    // fourWordPrepositions :: [[String]]
    const fourWordPrepositions = [
        ["at", "the", "behest", "of"],
        ["for", "the", "sake", "of"],
        ["with", "a", "view", "to"]
    ];


    // ------------------- GENERIC -------------------

    // toLower :: String -> String
    const toLower = s =>
        // Lower-case version of string.
        s.toLocaleLowerCase();


    // toUpper :: String -> String
    const toUpper = s =>
        s.toLocaleUpperCase();


    // toTitle :: String -> String
    const toTitle = s =>
        0 < s.length ? (
            `${toUpper(s[0])}${s.slice(1)}`
        ) : "";


    // words :: String -> [String]
    const words = s =>
        // List of space-delimited sub-strings.
        s.split(/\s+/u);


    // unwords :: [String] -> String
    const unwords = xs =>
        // A space-separated string derived
        // from a list of words.
        xs.join(" ");

    // MAIN ---
    return main();
})();
2 Likes