Textauswahl aus Safari in Obsidian speichern unter iOS

Strichzeichnung eines Bildschirmes mit Browser Fenster und ein iPhone mit einer List. Schmuckbild für den Blog-Artikel

In mei­nem Artikel Eine Textauswahl im Browser in Obsidian spei­chern habe ich detail­liert erklärt, wie ich auf der macOS-Plattform eine Text-Markierung in einem belie­bi­gen Browser als Snippet in eine Obsidian-Notiz ein­fü­ge. Diese Lösung nutzt die Alfred App, das eine JavaScript-Skript sowie ein Python-Skript ver­wen­det und mit allen Browsern funk­tio­niert.

Für iOS und iPadOS muss­te ich jedoch eine abge­speck­te Version des Workflows erstel­len, die aus­schließ­lich im Safari-Browser läuft. Dies liegt dar­an, JavaScript-Skripte via Kurzbefehle nur im Safari-Browser aus­ge­führt wer­den kann. Neben Kurzbefehle-App ver­wen­de ich zusätz­lich die Erweiterung Actions for Obsidian von Carlo Zottmann. Diese Erweiterung kos­tet zwar ein paar Euro, die sich aber loh­nen, wenn man mit den Kurzbefehlen Abläufe in Obsidian auto­ma­ti­sie­ren möch­te. Zum Testen reicht aber auch der kos­ten­lo­se Testzeitraum.

Weitere Details zum Aufbau und zur Funktionsweise der Skripte fin­den sich im ers­ten Teil des vor­he­ri­gen Artikels.

Der Kurzbefehl: Capture Browser Selection

Der Kurzbefehl ein­fa­cher auf­ge­baut als die Alfred Lösung für macOS, da die Erweiterung von “Actions for Obsidian” den Python Teil durch einen ein­fa­chen Aufruf einer Aktion ersetzt.

Screen Capture vom Kurzbefehl, die Erläuterung steht im Text

Als Eingabe wird nur das Share-Sheet aus­ge­wählt und dort nur die Optionen für den Safari-Browser. Damit ist der Kontext für die nächs­te Aktion JavaScript im aktiven Safari-Tab ausführen gesetzt. Das JavaScript, dass hier ein­ge­setzt wird ist im Prinzip das glei­che, wie in der macOS Lösung, mit dem einem Unterschied, dass in der letz­ten Zeile statt main();nun completion(main());steht. Die Funktion completion() ist not­wen­dig, damit das Ergebnis an die nächs­te Aktion über­ge­ben wird.

/**
 * Extracts the currently selected HTML from the browser window.
 * @returns {string} The HTML string of the selected content.
 */
function getSelectionHtml() {
    let html = "";
    if (window.getSelection) {
        const sel = window.getSelection();
        if (sel.rangeCount) {
            const container = document.createElement("div");
            for (let i = 0, len = sel.rangeCount; i < len; ++i) {
                container.appendChild(sel.getRangeAt(i).cloneContents());
            }
            html = container.innerHTML;
        } else {
        }
    } else {
    }
    return html;
}

/**
 * Converts HTML content into Markdown.
 * @param {string} html - HTML content to be converted.
 * @returns {string} The converted Markdown.
 */
function convertHtmlToMarkdown(html) {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    let markdown = convertNode(doc.body);
    return markdown.trim();
}

/**
 * Converts an HTML node to its Markdown representation.
 * @param {Node} node - The HTML node to convert.
 * @returns {string} Markdown representation of the node.
 */
function convertNode(node) {
    let markdown = '';

    if (node.nodeType === Node.ELEMENT_NODE) {
        switch (node.tagName) {
            case 'H1': case 'H2': case 'H3': case 'H4': case 'H5': case 'H6':
                markdown += `${'#'.repeat(parseInt(node.tagName[1]))} ${node.textContent.trim()}\n\n`;
                break;
            case 'P':
                markdown += `${node.textContent.trim()}\n\n`;
                break;
            case 'A':
                markdown += `[${node.textContent}](${node.href})`;
                break;
            case 'UL': case 'OL':
                markdown += convertList(node);
                break;
            case 'IMG':
                const alt = node.alt || 'Image';
                const src = node.src || '';
                const title = node.title ? ` "${node.title}"` : '';
                markdown += `![${alt}](${src}${title})\n`;
                break;
            case 'PRE': case 'CODE':
                markdown += `\`\`\`\n${node.textContent}\n\`\`\`\n\n`;
                break;
            case 'TABLE':
                markdown += convertTableToMarkdown(node);
                break;
            default:
                node.childNodes.forEach(child => {
                    markdown += convertNode(child);
                });
                break;
        }
    } else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
        markdown += node.textContent.trim() + ' ';
    }

    return markdown;
}

/**
 * Converts HTML lists to Markdown.
 * @param {HTMLElement} list - The list element.
 * @returns {string} Markdown formatted list.
 */
function convertList(list) {
    let markdown = '';
    const items = list.children;
    for (let item of items) {
        if (item.tagName === 'LI') {
            const prefix = list.tagName === 'OL' ? (Array.from(list.children).indexOf(item) + 1) + '.' : '-';
            markdown += `${prefix} ${item.textContent.trim()}\n`;
        }
    }
    return markdown + '\n';
}

/**
 * Converts HTML tables to Markdown.
 * @param {HTMLTableElement} table - The table element.
 * @returns {string} Markdown formatted table.
 */
function convertTableToMarkdown(table) {
    let markdown = '';
    const rows = table.querySelectorAll('tr');
    rows.forEach((row, index) => {
        let rowMarkdown = '|';
        row.querySelectorAll('th, td').forEach(cell => {
            rowMarkdown += ` ${cell.textContent.trim()} |`;
        });
        markdown += rowMarkdown + '\n';

        // Add header separator for the first row if it contains headers
        if (index === 0 && row.querySelector('th')) {
            markdown += '|' + Array.from(row.children).map(() => ' --- ').join('|') + '|\n';
        }
    });
    return markdown + '\n';
}

/**
 * Main function that orchestrates the conversion from HTML to Markdown.
 * @returns {void}
 */
function main() {
    const html = getSelectionHtml();
    if (!html) {
        return;
    }

    let markdown = convertHtmlToMarkdown(html);
    const datum = new Date().toLocaleDateString();
    const title = document.title;
    const url = window.location.href;
    const header = `\n\n>[\!snippets] ${datum} [${title}](${url})\n`;
    markdown = markdown.split('\n').map(line => '> ' + line).join('\n');
    markdown = header + markdown;
    return (markdown);
}

completion(main());

Das Ergebnis wird nun in der Aktion Prependaus der Sammlung “Actions for Obsidian” über­ge­ben, die dann auch noch mit dem Pfad zu der Notiz (bei mir 98 Adminstration/Bookmarks/Snippets), dem Namen der Vault, sowie dem Ort und dem Marker ver­se­hen wird. Als Pfad wird der inter­ne Obsidian Pfad ver­wen­det, daher muss hier nicht die Extension .md mit ange­ge­ben wer­den.

Wenn der Kurzbefehl aus­ge­führt wird, wird neben Obsidian auch die App “Actions for Obsidian” aktiv, was wohl tech­ni­sche Gründe hat. Ich pre­fe­rie­re zur Kontrolle auf den Fokus auf der “Snippet” Datei zu haben, daher schlies­se ich mit einer wei­te­ren Aktion Open note ab. Eigentlich fin­det sich unter den erwei­ter­ten Optionen der Prepend-Aktion eine Option, die das auto­ma­tisch erle­di­gen soll­te, die aber hier kei­ne Auswirkung hat.

Screen Capture von einem Teil des Kurzbefehl, die Erläuterung steht im Text

Wenn der Kurzbefehl defi­niert ist, kann er über das Share-Sheet im Safari-Browser auf­ge­ru­fen wer­den. Komischerweise aber nur da und nicht über das Share-Sheet das an der Auswahl ange­zeigt wird.

Wie schon erwähnt, funk­tio­niert der Kurzbefehl auch auf dem Mac, wobei es dort reicht den Kurzbefehl in der Menüleiste “anzu­pin­nen” und dar­über auf­zu­ru­fen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert