"Send()" but for richer content

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

"Send()" but for richer content

Post by JoeSchmoe » 20 Mar 2023, 00:06

Hi everyone,

I'm writing a snippet manager similar to Phrase Express: https://www.phraseexpress.com/ The goal is to have a library of rich text snippets, and have AHK send them to various applications.

The question is, what's the best way to send the snippets? Clearly, for text, Send just works. Iseahound's ImagePut looks perfect for sending images.

But what about rich text that combines text with formatting, links, occasional images, and whatever the future may bring?

HTML seems natural, so I've been investigating putting HTML snippets on the clipboard and pasting. It's working... okay. Every program seems to handle HTML differently. Some programs indicate paragraphs with <p>, others with <div> or even <br>. Spacing after paragraphs is done with additional paragraphs (p/div/br) or with padding/margin. Finding a format that works with Gmail/Word/Google Docs/OneNote/others has been a bit of a pain.

Any ideas for a better strategy for getting rich text into a variety of programs?

User avatar
mikeyww
Posts: 26437
Joined: 09 Sep 2014, 18:38

Re: "Send()" but for richer content

Post by mikeyww » 20 Mar 2023, 07:14

Hello,

There is a WinClip.ahk library to work with the clipboard. You can then send ^v.

User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

Re: "Send()" but for richer content

Post by JoeSchmoe » 20 Mar 2023, 11:59

mikeyww wrote:
20 Mar 2023, 07:14
There is a WinClip.ahk library to work with the clipboard. You can then send ^v.
Thanks! That’s what I’ve been doing so far, but I’ve found resulting behavior to be quite problematic and I’m wondering if there might be a more elegant solution..

For example, if you paste "<p>sometext</p>" as HTML into MS Word, it bizarrely changes the font to Times New Roman, even when Times isn’t the default font for the document. Likewise, if you paste the same HTML into Gmail, it messes up the paragraph spacing, adding a blank line before each paragraph.

I want it to work like “send “sometext`n” in that I want it to match the font, paragraph spacing, and other formatting of the document I’m working in. I just want to be able to add bold, italic, and links as needed. If possible, I’d love to be able to add lists, tables and graphics.

Ideally, I'd love a relatively simple solution, so that I can add complexity later on as the program develops.
Last edited by JoeSchmoe on 20 Mar 2023, 12:12, edited 2 times in total.

User avatar
mikeyww
Posts: 26437
Joined: 09 Sep 2014, 18:38

Re: "Send()" but for richer content

Post by mikeyww » 20 Mar 2023, 12:10

My Word.......... :evil: :(

I did not find a solution but look forward to seeing one! This might require diving into how the clipboard works and what is on it.

User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

Re: "Send()" but for richer content

Post by JoeSchmoe » 20 Mar 2023, 12:20

LOL. :lol:

By the way, if anyone wants to experiment with this, I've pasted in a little script that I've used to experiment with (see below).

Press ^!+v to paste in the HTML snippet of your choice into the active window. (You can edit the snippet and then reload the script with ^s and ^r.)

Other issues I've come across relate to how editable formatted text is represented as HTML by different programs. Different programs do this very differently. You can examine this by copying some formatted text and then pressing ^!+c to see how the formatted text is represented as HTML in the clipboard. It's ugly!

Code: Select all

#Requires AutoHotkey v2.0
^r::Reload
^!+v::PasteSnippet("<p>para1</p><p>para2</p>")
^!+c::DisplayHTML() ; Select some HTML text, press ^c to copy and ^+c to display


PasteSnippet(HTML){
	A_Clipboard := ''
	HTMLtoClipboard(HTML)
	If ClipWait(1, True) {
		Send "^v"
	} Else MsgBox 'An error occurred while waiting for the clipboard.', 'Error', 48
}


HTMLToClipboard(html) {
	 ; Idea from: https://www.autohotkey.com/boards/viewtopic.php?f=83&t=114568
	 ; ALTERNATIVE VERSION, NOT YET IN V2: https://www.autohotkey.com/boards/viewtopic.php?f=6&t=80706&p=513397#p513397
	 htmlFile  := ComObject('HTMLfile')
	 htmlFile.write(html)
	 bodyRange := htmlFile.body.createTextRange()
	 bodyRange.select(), bodyRange.execCommand('Copy')
	 bodyRange := htmlFile := ''
}


DisplayHTML() {
	Snippet := HTMLFromClipboard()
	Snippet := StrReplace(Snippet, '<', '`n<')
	Snippet := StrReplace(Snippet, '>', '>`n')
	Snippet := StrReplace(Snippet, '`r', '`n')
	Snippet := StrReplace(Snippet, '`n`n', '`n')
	Snippet := StrReplace(Snippet, '`n`n', '`n')
	Snippet := StrReplace(Snippet, '`n', '`n`n')

	Filename := "Snippet.html"
	If (FileExist(FileName))
		FileDelete FileName
	FileAppend Snippet, FileName
	Run "Notepad.exe " Filename
}


HTMLFromClipboard() {
	; From https://www.autohotkey.com/boards/viewtopic.php?p=513124#p513257
	static CF_HTML := DllCall('RegisterClipboardFormat', 'Str', 'HTML Format')
	DllCall('OpenClipboard', 'Ptr', A_ScriptHwnd)
	format := 0
	Loop {
		format := DllCall('EnumClipboardFormats', 'UInt', format)
	} until format = CF_HTML || format = 0
	if format != CF_HTML {
		DllCall('CloseClipboard')
		return
	}
	hData := DllCall('GetClipboardData', 'UInt', CF_HTML, 'Ptr')
	pData := DllCall('GlobalLock', 'Ptr', hData)
	html := StrGet(pData, 'UTF-8')
	DllCall('GlobalUnlock', 'Ptr', hData)
	DllCall('CloseClipboard')
	return html
}

User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

Re: "Send()" but for richer content

Post by JoeSchmoe » 20 Mar 2023, 12:35

My current approach is to make an ultra-simple markup language that allows me to divide the snippet up into chunks. Each chunk of the snippet can be sent using a different approach:
  • Unformatted text can be sent with Send
  • Simple character formatting can be sent with Send + common keyboard shortcuts like ^b, ^i, and ^k for bold, italic, and links. I’ve used a similar solution for character formatting for years, and it works well for my use case. (Alternatively, I could just paste HTML using winclip or alternatives. Pasting HTML seems to work much better at the ‘sub-paragraph’ level.)
  • New paragraphs could be entered with a nice, simple, “Send “{Enter}”” a bit of code that just seems to work everywhere exactly how I want it to.
I'll probably do something like this, because using a markup language will allow me to add features like asking for user input (for example, soliciting the recipient's name for the salutation of an email), or using different phrasings so messages aren't quite so repetitive.

Clearly, this sounds like a big project, so I thought I’d check in with the smart people on this forum to see if anyone has any advice or ideas I should keep in mind. I'm sure there will be many obstacles ahead, so I want to make sure I'm using the best libraries or ideas for sending the rich text, straight from the start.

User avatar
mikeyww
Posts: 26437
Joined: 09 Sep 2014, 18:38

Re: "Send()" but for richer content

Post by mikeyww » 20 Mar 2023, 12:48

If you are having to resort to ^b and so on-- program-specific formatting "commands" (hotkeys), then I think that you could do this at least with Microsoft Office by using COM to get the fonts or HTML that you want. There might already be some posts about using COM to paste or insert HTML.

User avatar
FanaticGuru
Posts: 1905
Joined: 30 Sep 2013, 22:25

Re: "Send()" but for richer content

Post by FanaticGuru » 20 Mar 2023, 12:53

JoeSchmoe wrote:
20 Mar 2023, 00:06
I'm writing a snippet manager similar to Phrase Express: https://www.phraseexpress.com/ The goal is to have a library of rich text snippets, and have AHK send them to various applications.

You might want to check out Lintalist at https://github.com/lintalist/lintalist/

I use it for formatted snippets to insert in Outlook emails. Especially, various Dropbox links that I use repeatedly.

It can do much more than I use it for. It is a pretty extensive AHK application.

FG
Hotkey Help - Help Dialog for Currently Running AHK Scripts
AHK Startup - Consolidate Multiply AHK Scripts with one Tray Icon
Hotstring Manager - Create and Manage Hotstrings
[Class] WinHook - Create Window Shell Hooks and Window Event Hooks

Emile HasKey
Posts: 27
Joined: 06 Mar 2022, 17:45

Re: "Send()" but for richer content

Post by Emile HasKey » 28 Mar 2023, 17:26

Have you tried WinClip with Rich Text Format (RTF)?
I'm curious how many programs support it.
It might give more consistent results, or be just as bad.

Perhaps also try this:
Manually put the same string into multiple programs.
Select and copy to clipboard.
Inspect the html on the clipboard.
Maybe that way you could get clues about what html the program requires.

User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

Re: "Send()" but for richer content

Post by JoeSchmoe » 28 Mar 2023, 22:46

mikeyww wrote:
20 Mar 2023, 12:48
I think that you could do this at least with Microsoft Office by using COM to get the fonts or HTML that you want.
Thanks. If I wanted to make a commercial-grade product, I think that COM would probably need to be in the mix. I have to wonder if Phrase Express--which I'm taking as my model--uses it.
FanaticGuru wrote:
20 Mar 2023, 12:53
You might want to check out Lintalist at https://github.com/lintalist/lintalist/
At some point, I should definitely check that out. The thing is that I've got a codebase that I've been using for years for launching programs, running AHK functions, and sending very short snippets. As I transition it to a full-scale personal snippet manager, I want to build off that code because it already does so much and it does that exactly the way I want it to.

Recently I've been just having a blast upgrading that code to send formatted text as HTML (via the clipboard). As you might have seen in another thread, I'm planning to store the snippets themselves in Markdown. With a huge amount of help from @TheArkive (markdown) and @teadrinker/@FanaticGuru (HTML on clipboard), I've got code that I've been using for about a week now. While it's still definitely in a pre-alpha state of polish, it gets the job done and I'm getting used to the quirks of clipboard HTML.
Emile HasKey wrote:
28 Mar 2023, 17:26
Have you tried WinClip with Rich Text Format (RTF)?
I took a look at this and I was surprised at how much I didn't hate it! For example, here's some basic RTF taken from my clipboard (generated by Wordpad - RTF from Word is not nearly this clean):

Code: Select all

\pard\widctlpar\sl276\slmult1\f0\fs22\lang9 This paragraph has \b bold\b0 , \i italic\i0 , and \ul underline\ulnone .\par
\par
This paragraph\b  \b0 has a {{\field{\*\fldinst{HYPERLINK "http://www.google.com/" }}{\fldrslt{\ul\cf1\cf2\ul hyperlink}}}}\f0\fs22 . \par
I could work with that! The specification isn't so bad, either.

With that said, while it might be great for a second format, I don't know if it is supported quite as widely as HTML. ... And my laziness regarding learning the quirks of HTML pasting is slowly being overcome as I get more practice with it.

The markdown→HTML→Paste idea as a way of sending formatted text is really growing on me the more I use it and get comfortable with it. I'll try to post an update in the next week or two as I smooth things out a bit. I think others might find this method useful as well.

User avatar
mikeyww
Posts: 26437
Joined: 09 Sep 2014, 18:38

Re: "Send()" but for richer content

Post by mikeyww » 29 Mar 2023, 05:52

It sounds like your latest question is how to determine how a program handles whitespace. Perhaps this is partly a matter of determining which programs are text editors where you might need an extra line feed, vs. other programs. I find that Outlook does not handle pasted text very well, as whitespace can come out differently on the other end. There is also the issue that Outlook and other programs have options to use plain text or HTML, and these handle white space in different ways. I don't think you'll have a way to know how every program works in advance. A fall-back option could be a database of known programs, added to a default approach.

list
Posts: 221
Joined: 26 Mar 2014, 14:03
Contact:

Re: "Send()" but for richer content

Post by list » 31 Mar 2023, 15:28

JoeSchmoe wrote:
28 Mar 2023, 22:46
The markdown→HTML→Paste idea as a way of sending formatted text is really growing on me the more I use it and get comfortable with it. I'll try to post an update in the next week or two as I smooth things out a bit. I think others might find this method useful as well.
That is what Lintalist has been doing for a long time. It uses WinClip to paste html, rtf and images, and a markdown to html converter and then use WinClip + you can use plugins to enrich the snippets (specially formatted text to get user input) https://lintalist.github.io/#InteractiveBundleText

But building your own solutions is fun of course!

User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

Re: "Send()" but for richer content

Post by JoeSchmoe » 01 Apr 2023, 10:13

Wow, @list , thanks for dropping in on my little thread. I took a look at Lintalist, and it's super impressive! The fact that it's up on sites like Softpedia and MajorGeeks is quite a contribution.

My inclination is to extend my existing codebase because a.) I'm having a blast, and b.) the code base I'm already using has a number of features that I couldn't live without for my day job. [I'll share some of these features in a postscript below.]

I'm super glad that you dropped by, though, because with so many years of supporting Lintalist under your belt, if anyone has a lot of experience using HTML to paste formatted text snippets into a document, it would be you. Could you share any thoughts you have on how well the HTML→Paste pathway works in your experience?

The thing that concerns me the most is the way that programs seem to assume extra formatting when I past in the simplest HTML. For example, suppose I just put "<p>example</p>" on my clipboard. Suppose I'm working on a Microsoft Word document, with Calibri 11 point as my font and no spacings before or after paragraphs. If I paste that snippet in anywhere, it will be pasted in as Times New Roman 12 point, and it will change the paragraph spacing. Why on earth would it do that?!?! It makes me a little crazy.

Worse, every other program may handle that simple HTML somewhat differently. Do I have to come up with workarounds for each program and each context?

At the same time, learning how to do this seems unavoidable, as HTML seems to be the default way of exchanging rich text between programs. (RTF might be a backup.)

Based on your experience, do you have any general advice for overcoming difficulties like this when pasting HTML snippets? (and/or using RTF as a backup?)


Postscript: Reasons I love my existing codebase

I mentioned that my existing codebase has some features I can't live without for my job. The core deliverable of my day job is online presentations, so I need a way to fire a library of about 240 little AutoHotkey macros. I've come up with a system that I call "j-codes." It's based on the fact that the letter j is quite rare, and with only a couple exceptions, always has a vowel after it. (Back in the aughts, when I was first exploring AHK, I generated a corpus of text and analyzed it to discover this.) Thus, whenever I type a 'j,' at any time, AHK instantiates and starts an InputHook, which blocks all input and starts looking for a hotstring that I type. If I need to type a literal j, usually it has a vowel, r or s, after it, so whenever the system sees those pairs of letters, it just sends them through. Likewise, if I need to type a j itself, I just type 'jj.' Finally, if I ever forget a j-code, I've got a gui that pops up on the monitor I don't share. It does a live keyword search, so I can quickly find what I forgot.

It's a pretty intense solution, but it works incredibly well for my job because my viewers don't see anything, but I can fire 240 macros while keeping my fingers on the home row of my keyboard. It's deep in my muscle memory, so it's incredibly fast for me and I like to think that to my viewers, it looks like magic. I also just love my existing codebase because everything is exactly how I like it. I just want to add the ability to send rich text to this base. [/spoiler2]

User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

Re: "Send()" but for richer content

Post by JoeSchmoe » 01 Apr 2023, 10:35

By the way, here's the current library that I'm using to send rich text. In some ways, it works wonderfully, but its still a work in progress.

To send a markdown demo, just type mddemo. After sending the demo, press ^!h to see the actual HTML that was sent.

Perhaps most helpfully, to see the Source code of the HTML on your clipboard, just press ^!c. You can use this to see how various programs export their formatted rich text into the clipboard's HTML Format.

As described in the comment, @teadrinker and @FanaticGuru get credit for the HTML⇔Clipboard conversions and @TheArkive gets credit for all the Markdown⇒HTML conversion. At this point, the actual code, omitting the demo text, is only about 650 lines long!

Code: Select all


;##################################################################################################################
;##################################################################################################################
;############
;############     MDSend
;############
;##################################################################################################################
;##################################################################################################################


; The following is just a demo of combining code by TheArkive and SKAN/teadrinker/FanaticGuru.
; Very little of the following is my own work.
; It just combines two scripts:
; 1.) For putting HTML in the clipboard, SetClipboardHTMLtext, by teadrinker and FanaticGuru:
; https://www.autohotkey.com/boards/viewtopic.php?f=82&t=115178
; 2.) For transforming Markdown into HTML, M-ArkDown (Ark_make_html) by TheArkive:
; https://github.com/TheArkive/M-ArkDown_ahk2/blob/master/_MD_Gen.ahk

LastSent:=""
^!h::ShowText(LastSent)
^!c::ShowText(ExtractHtmlData())
;^!v::PasteClipAsHTML()

::mddemo::{
	MDSend(SampleText())
}

PasteClipAsHTML() {

}

MDSendLine(MarkDownText, RestoreClipboard:=True) {
	options := {css: "p {margin: 0;}", linemode:true}
    XSLog("MDSendLine() is about to make_HTML")
    HTML := Ark_make_html(MarkDownText, options)
    XSLog("MDSendLine() finished make_HTML")
    HTMLSend(HTML, RestoreClipboard, MarkDownText)
}


MDSend(MarkDownText, RestoreClipboard:=True, PlainText:=MarkDownText) {
	options := {css: "p {margin: 0;}"}
    XSLog("MDSend() is about to make_HTML")
    HTML := Ark_make_html(MarkDownText, options)
    XSLog("MDSend() is about to call HTMLSend()")
    HTMLSend(HTML, RestoreClipboard, PlainText)
}

HTMLSend(HTML, RestoreClipboard:=True, PlainText:=HTML) {
    Global LastSent := HTML

    If RestoreClipboard
        SavedClip := ClipboardAll()
    A_Clipboard := ""

    XSLog("HTMLSend() is about to set clipboard")
  	TD_Condensed_SetClipboardHTMLtext(HTML, PlainText)
    ; wc.SetHTML(HTML)
    XSLog("HTMLSend() just set clipboard")

	If ClipWait(1, True) {
        XSLog("HTMLSend() finished ClipWait - about to Send '^v'")
		Send '^v'
    } Else MsgBox 'An error occurred while waiting for the clipboard.', 'Error', 48

    If RestoreClipboard {
        Sleep 150
        XSLog("HTMLSend() Sent '^v' and slept 150. Now restoring clipboard.")
        A_Clipboard := SavedClip
        SavedClip := ""
    }
}

TextSend(Text, RestoreClipboard:=True) {
    If RestoreClipboard
        SavedClip := ClipboardAll()

    XSLog("TextSend() is about to blank clipboard")
    A_Clipboard := ""
    XSLog("TextSend() has blanked clipboard. About to reset it.")
    A_Clipboard := Text
    XSLog("TextSend() just set clipboard")

	If ClipWait(1, True) {
        XSLog("TextSend() finished ClipWait - about to Send '^v'")
		Send '^v'
	} Else MsgBox 'An error occurred while waiting for the clipboard.', 'Error', 48

    If RestoreClipboard {
        Sleep 150
        XSLog("HTMLSend() Sent '^v' and slept 150. Now restoring clipboard.")
        A_Clipboard := SavedClip
        SavedClip := ""
    }
}



; Credit: teadrinker and FanaticGuru - https://www.autohotkey.com/boards/viewtopic.php?p=513124#p513671
; Helpful: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
TD_Condensed_SetClipboardHTMLtext(Html, Text := Html)
{
	Html := 'Version:0.9`nStartHTML:-1`nEndHTML:-1`nStartFragment:00074`nEndFragment:' Format('{:05u}', StrLen(Html) + 74) '`n' Html
	DllCall('OpenClipboard', 'Ptr', A_ScriptHwnd)
	DllCall('EmptyClipboard')
	hMem := DllCall('GlobalAlloc', 'UInt', 0x42, 'Ptr', StrPut(Html, 'cp0'), 'Ptr')
	StrPut(Html, DllCall('GlobalLock', 'Ptr', hMem, 'Ptr'), 'cp0')
	DllCall('GlobalUnlock', 'Ptr', hMem)
	DllCall('SetClipboardData', 'UInt', DllCall('RegisterClipboardFormat', 'Str', 'HTML Format'), 'Ptr', hMem)
	hMem := DllCall('GlobalAlloc', 'UInt', 0x42, 'Ptr', StrPut(Text, 'UTF-16') * 2, 'Ptr')
	StrPut(Text, DllCall('GlobalLock', 'Ptr', hMem, 'Ptr'), 'UTF-16')
	DllCall('GlobalUnlock', 'Ptr', hMem)
	DllCall('SetClipboardData', 'UInt', 13, 'Ptr', hMem)
	DllCall('CloseClipboard')
}



; Credit: teadrinker - https://www.autohotkey.com/boards/viewtopic.php?p=513124#p513257
ExtractHtmlData() {
    static CF_HTML := DllCall('RegisterClipboardFormat', 'Str', 'HTML Format')
    DllCall('OpenClipboard', 'Ptr', A_ScriptHwnd)
    format := 0
    Loop {
        format := DllCall('EnumClipboardFormats', 'UInt', format)
    } until format = CF_HTML || format = 0
    if format != CF_HTML {
        DllCall('CloseClipboard')
        return
    }
    hData := DllCall('GetClipboardData', 'UInt', CF_HTML, 'Ptr')
    pData := DllCall('GlobalLock', 'Ptr', hData, 'Ptr')
    html := StrGet(pData, 'UTF-8')
    DllCall('GlobalUnlock', 'Ptr', hData)
    DllCall('CloseClipboard')
    return html
}



ShowText(text,x:=900,rows:=40,eta:=false) {
	static MyGui, TextCtl
	MyGui := Gui('+Resize', 'Show Text')
	MyGui.SetFont("s16")
	TextCtl := MyGui.Add("Edit",'vText w' x ' r' rows , text)
	MyGui.Show()  ; NoActivate avoids deactivating the currently active window.
	MyGui.OnEvent("Size", ShowTextGuiResize)
}

ShowTextGuiResize(GuiObj, MinMax, Width, Height){
		GuiObj["Text"].Move(10,10,Width-20, Height-35)
	}


XSLog(string) {    ; Used for benchmarking and debugging
	;karvis.Ramlog(string)
	; With the above line uncommented, the following code runs in 30 MICROseconds:
	;Loop 10000
	;	XSLog(A_Index)
}



;########################################################
;#####
;#####     Cutdown M-ArkDown - Ark_make_html()
;#####
;#####     Based on https://github.com/TheArkive/M-ArkDown_ahk2 by TheArkive
;#####     Adapted to make clipboard snippets instead of real webpages
;#####
;########################################################

;   Ignore the last 2 params.  Those are used internally for recursion.
Ark_make_html(_in_text, options:={}, final:=true, md_type:="") {
    Global LastSend
    If !RegExMatch(_in_text,"[`r`n]+$") && (final) && md_type!="header" { ; add trailing CRLF if doesn't exist
        _in_text .= "`r`n"
    }

    If (options.HasOwnProp("linemode") && options.linemode) {
        Return "<html><head></head>`r`n<body><!--StartFragment -->" inline_code(_in_text) "<!--EndFragment --></body></html>"
    }

    body := ""
    table_done := false

    a := StrSplit(_in_text,"`n","`r")
    i := 0

    While (i < a.Length) {                                          ; ( ) \x28 \x29
        i++, line := a[i]                                           ; [ ] \x5B \x5D
        blockquote := "", ul := "", ul2 := "", code_block := ""     ; { } \x7B \x7D
        ol := "", ol2 := "", ol_type := ""
        table := ""

        ; header h1 - h6
        If RegExMatch(line, "^(#+) (.+)", &match) {
            ; dbg("HEADER H1-H6")

            depth := match[1], _class := "", title := ltgt(match[2])

            If RegExMatch(line, "\x5B *([\w ]+) *\x5D$", &_match)
                _class := _match[1], title := SubStr(title, 1, StrLen(match[2]) - _match.Len(0))

            opener := "<h" match.Len(1) (_class?' class="' _class '"':'') '>'

            body .= (body?"`r`n":"") opener Trim(Ark_make_html(title,,false,"header"),"`r`n")
            ; body .= (body?"`r`n":"") opener title
                  ;. '<a href="#' id '"><span class="link">•</span></a>'
                  . "</h" match.Len(1) ">"

            Continue
        }

        ; spoiler
        If RegExMatch(line, "^<spoiler=([^>]+)>$", &match) {
            disp_text := ltgt(match[1])
            spoiler_text := ""
            i++, line := a[i]
            While !RegExMatch(line, "^</spoiler>$") {
                spoiler_text .= (spoiler_text?"`r`n":"") line
                i++, line := a[i]
            }

            body .= (body?"`r`n":"") '<p><details><summary class="spoiler">'
                  . disp_text "</summary>" Ark_make_html(spoiler_text,,false,"spoiler") "</details></p>"
            Continue
        }

        ; hr
        If RegExMatch(line, "^([=\-\*_ ]{3,}[=\-\*_ ]*)(?:\x5B *[^\x5D]* *\x5D)?$", &match) {
            ; dbg("HR: " line)

            hr_style := ""

            If Trim(line)=""
                Continue

            If RegExMatch(line, "\x5B *([^\x5D]*) *\x5D", &match) {
                hr_str := match[1]
                arr := StrSplit(hr_str," ")

                For i, style in arr {
                    If (SubStr(style, -2) = "px")
                        hr_style .= (hr_style?" ":"") "border-top-width: " style ";"
                    Else If RegExMatch(style, "(dotted|dashed|solid|double|groove|ridge|inset|outset|none|hidden)")
                        hr_style .= (hr_style?" ":"") "border-top-style: " style ";"
                    Else
                        hr_style .= (hr_style?" ":"") "border-top-color: " style ";"
                }
            }
            body .= (body?"`r`n":"") '<hr style="' hr_style '">'
            Continue
        }

        ; blockquote - must come earlier because of nested elements
        While RegExMatch(line, "^\> ?(.*)", &match) {
            ; dbg("BLOCKQUOTE 1")

            blockquote .= (blockquote?"`r`n":"") match[1]

            If a.Has(i+1)
                i++, line := Trim(a[i]," `t")
            Else
                Break
        }

        If (blockquote) {
            ; dbg("BLOCKQUOTE 2")

            body .= (body?"`r`n":"") "<blockquote>" Ark_make_html(blockquote,, false, "blockquote") "</blockquote>"
            Continue
        }

        ; code block
        If (line = "``````") {
            ; dbg("CODEBLOCK")

            If (i < a.Length)
                i++, line := a[i]
            Else
                Break

            While (line != "``````") {
                code_block .= (code_block?"`r`n":"") line
                If (i < a.Length)
                    i++, line := a[i]
                Else
                    Break
            }

            body .= (body?"`r`n":"") "<pre><code>" StrReplace(StrReplace(code_block,"<","&lt;"),">","&gt;") "</code></pre>"
            Continue
        }

        ; table
        While RegExMatch(line, "^\|.*?\|$") {
            ; dbg("TABLE 1")

            table .= (table?"`r`n":"") line

            If a.Has(i+1)
                i++, line := a[i]
            Else
                Break
        }

        If (table) {
            ; dbg("TABLE 2")

            table_done := true

            body .= (body?"`r`n":"") '<table class="normal">'
            b := [], h := [], t := " `t"

            Loop Parse table, "`n", "`r"
            {
                body .= "<tr>"
                c := StrSplit(A_LoopField,"|"), c.RemoveAt(1), c.RemoveAt(c.Length)

                If (A_Index = 1) {
                    align := ""
                    Loop c.Length {
                        If RegExMatch(Trim(c[A_Index],t), "^:(.+?)(?<!\\):$", &match) {
                            m := StrReplace(inline_code(match[1]),"\:",":")
                            h.Push(["center",m])
                        } Else If RegExMatch(Trim(c[A_Index],t), "^([^:].+?)(?<!\\):$", &match) {
                            m := StrReplace(inline_code(match[1]),"\:",":")
                            h.Push(["right",m])
                        } Else If RegExMatch(Trim(c[A_Index],t), "^:(.+)", &match) {
                            m := StrReplace(inline_code(match[1]),"\:",":")
                            h.Push(["left",m])
                        } Else {
                            m := StrReplace(inline_code(Trim(c[A_Index],t)),"\:",":")
                            h.Push(["",m])
                        }
                    }
                } Else If (A_Index = 2) {
                    Loop c.Length {
                        If RegExMatch(c[A_Index], "^:\-+:$", &match)
                            b.Push(align:="center")
                        Else If RegExMatch(c[A_Index], "^\-+:$", &match)
                            b.Push(align:="right")
                        Else
                            b.Push(align:="left")
                        If (!h[A_Index][1])
                            h[A_Index][1] := align
                        body .= '<th align="' h[A_Index][1] '">' h[A_Index][2] '</th>'
                    }
                } Else {
                    Loop c.Length {
                        m := inline_code(c[A_Index]) ; Ark_make_html(c[A_Index],, false, "table data")
                        body .= '<td align="' b[A_Index]  '">' Trim(m," `t") '</td>'
                    }
                }
                body .= "</tr>"
            }
            body .= "</table>"
            Continue
        }

        ; unordered lists
        If RegExMatch(line, "^( *)[\*\+\-] (.+?)(\\?)$", &match) {

            ; dbg("UNORDERED LISTS")

            While RegExMatch(line, "^( *)([\*\+\-] )?(.+?)(\\?)$", &match) { ; previous IF ensures first iteration is a list item
                ul2 := ""

                If !match[1] && match[2] && match[3] {
                    ul .= (ul?"</li>`r`n":"") "<li>" Ark_make_html(match[3],,false,"ul item")

                    If match[4]
                        ul .= "<br>"

                    If (i < a.Length)
                        i++, line := a[i]
                    Else
                        Break

                    Continue

                } Else If !match[2] && match[3] {
                    ul .= Ark_make_html(match[3],,false,"ul item append")

                    If match[4]
                        ul .= "<br>"

                    If (i < a.Length)
                        i++, line := a[i]
                    Else
                        Break

                    Continue

                } Else If match[1] && match[3] {

                    While RegExMatch(line, "^( *)([\*\+\-] )?(.+?)(\\?)$", &match) {
                        If (Mod(StrLen(match[1]),2) || !match[1] || !match[3])
                            Break

                        ul2 .= (ul2?"`r`n":"") SubStr(line, 3)

                        If (i < a.Length)
                            i++, line := a[i]
                        Else {
                            line := ""
                            Break
                        }
                    }

                    ul .= "`r`n" Ark_make_html(ul2,,false,"ul")
                    Continue
                }

                If (i < a.Length)
                    i++, line := a[i]
                Else
                    Break
            }
        }

        If (ul) {
            body .= (body?"`r`n":"") "<ul>`r`n" ul "</li></ul>`r`n"
            Continue
        }

        ; ordered lists
        If RegExMatch(line, "^( *)[\dA-Za-z]+(?:\.|\x29) +(.+?)(\\?)$", &match) {

            ; dbg("ORDERED LISTS")

            While RegExMatch(line, "^( *)([\dA-Za-z]+(?:\.|\x29) )?(.+?)(\\?)$", &match) { ; previous IF ensures first iteration is a list item
                ol2 := ""

                If !match[1] && match[2] && match[3] {
                    ol .= (ol?"</li>`r`n":"") "<li>" Ark_make_html(match[3],,false,"ol item")

                    If (A_Index = 1)
                        ol_type := 'type="' RegExReplace(match[2], "[\.\) ]","") '"'

                    If match[4]
                        ol .= "<br>"

                    If (i < a.Length)
                        i++, line := a[i]
                    Else
                        Break

                    Continue

                } Else If !match[2] && match[3] {
                    ol .= Ark_make_html(match[3],,false,"ol item append")

                    If match[4]
                        ol .= "<br>"

                    If (i < a.Length)
                        i++, line := a[i]
                    Else
                        Break

                    Continue

                } Else If match[1] && match[3] {

                    While RegExMatch(line, "^( *)([\dA-Za-z]+(?:\.|\x29) )?(.+?)(\\?)$", &match) {
                        If (Mod(StrLen(match[1]),2) || !match[1] || !match[3])
                            Break

                        ol2 .= (ol2?"`r`n":"") SubStr(line, 3)

                        If (i < a.Length)
                            i++, line := a[i]
                        Else {
                            line := ""
                            Break
                        }
                    }

                    ol .= "`r`n" Ark_make_html(ol2,,false,"ol")
                    Continue
                }

                If (i < a.Length)
                    i++, line := a[i]
                Else
                    Break
            }
        }

        If (ol) {
            body .= (body?"`r`n":"") "<ol " ol_type ">`r`n" ol "</li></ol>`r`n"
            Continue
        }

        ; =======================================================================
        ; ...
        ; =======================================================================

        If RegExMatch(md_type,"^(ol|ul)") { ; ordered/unordered lists
            body .= (body?"`r`n":"") inline_code(line)
            Continue
        } Else If RegExMatch(line, "\\$") { ; manual line break at end \
            body .= (body?"`r`n":"") "<p>"
            reps := 0

            While RegExMatch(line, "(.+)\\$", &match) {
                reps++
                body .= ((A_Index>1)?"<br>":"") inline_code(match[1])

                If (i < a.Length)
                    i++, line := a[i]
                Else
                    Break
            }

            If line
                body .= (reps?"<br>":"") inline_code(line) "</p>"
            Else
                body .= "</p>"
        } Else If line {
            If md_type != "header"
                body .= (body?"`r`n":"") "<p>" inline_code(line) "</p>"
            Else
                body .= (body?"`r`n":"") inline_code(line)
        }
    }

    body := RegExReplace(body, "</p>(\s*)<p","</p><p>&nbsp;</p>$1<p")

    If final {
        result := "<html><head><style>`r`n" options.css "</style></head>`r`n`r`n<body><!--StartFragment -->"  body "<!--EndFragment --></body></html>"
        ; html1 . options.css . html2 . html3 . body . html4
    } Else
        result := body

    return Result

    ; =======================================================================
    ; Local Functions
    ; =======================================================================

    inline_code(_in) {
        output := _in

        ; inline code
        While RegExMatch(output, "``(.+?)``", &match) {
            output := StrReplace(output, match[0], "<code>" ltgt(match[1]) "</code>",,,1)
        }

        ; image
        r := 1
        While (s := RegExMatch(output, "!\x5B *([^\x5D]*) *\x5D\x28 *([^\x29]+) *\x29(\x28 *[^\x29]* *\x29)?", &match, r)) {
            If IsInCode(match[0], output) || IsInTag(match[0], output) {
                r := s + match.Len(0)
                Continue
            }
            dims := Trim(match[3],"()")
            output := StrReplace(output, match[0], '<img src="' match[2] '"' (dims?" " dims:"")
                    . ' alt="' ltgt(match[1]) '" title="' ltgt(match[1]) '">',,,1)
        }

        ; link / url
        r := 1
        While (s := RegExMatch(output, "\x5B *([^\x5D]+) *\x5D\x28 *([^\x29]+) *\x29", &match, r)) {
            If IsInCode(match[0], output) || IsInTag(match[0], output) {
                r := s + match.Len(0)
                Continue
            }
            output := StrReplace(output, match[0], '<a href="' match[2] '" target="_blank" rel="noopener noreferrer">'
                    . match[1] "</a>",,,1)
        }

        ; strong + emphesis (bold + italics)
        While (s := RegExMatch(output, "(?<!\w)[\*]{3,3}([^\*]+)[\*]{3,3}", &match, r))
           || (s := RegExMatch(output, "(?<!\w)[\_]{3,3}([^\_]+)[\_]{3,3}", &match, r)) {
            If IsInCode(match[0], output) || IsInTag(match[0], output) {
                r := s + match.Len(0)
                Continue
            }
            output := StrReplace(output, match[0], "<em><strong>" ltgt(match[1]) "</strong></em>",,,1)
        }

        ; strong (bold)
        While (s := RegExMatch(output, "(?<!\w)[\*]{2,2}([^\*]+)[\*]{2,2}", &match, r))
           || (s := RegExMatch(output, "(?<!\w)[\_]{2,2}([^\_]+)[\_]{2,2}", &match, r)) {
            If IsInCode(match[0], output) || IsInTag(match[0], output) {
                r := s + match.Len(0)
                Continue
            }
            output := StrReplace(output, match[0], "<strong>" ltgt(match[1]) "</strong>",,,1)
        }

        ; emphesis (italics)
        While (s := RegExMatch(output, "(?<!\w)[\*]{1,1}([^\*]+)[\*]{1,1}", &match, r))
           || (s := RegExMatch(output, "(?<!\w)[\_]{1,1}([^\_]+)[\_]{1,1}", &match, r)) {
            If IsInCode(match[0], output) || IsInTag(match[0], output) {
                r := s + match.Len(0)
                Continue
            }
            output := StrReplace(output, match[0], "<em>" ltgt(match[1]) "</em>",,,1)
        }

        ; strikethrough
        While (s := RegExMatch(output, "(?<!\w)~{2,2}([^~]+)~{2,2}", &match, r)) {
            If IsInCode(match[0], output) || IsInTag(match[0], output) {
                r := s + match.Len(0)
                Continue
            }
            output := StrReplace(output, match[0], "<del>" ltgt(match[1]) "</del>",,,1)
        }

        return output
    }

    ltgt(_in) {
        return StrReplace(StrReplace(_in,"<","&lt;"),">","&gt;")
    }

    rpt(_in, reps) {
        final_str := ""         ; Had to change "final" var to "final_str".
        Loop reps               ; This may still be a bug in a133...
            final_str .= _in
        return final_str
    }

    IsInTag(needle, haystack) {
        start := InStr(haystack, needle) + StrLen(needle)
        sub_str := SubStr(haystack, start)

        tag_start := InStr(sub_str,"<")
        tag_end := InStr(sub_str,">")

        If (!tag_start && tag_end) Or (tag_end < tag_start)
            return true
        Else
            return false
    }

    IsInCode(needle, haystack) {
        start := InStr(haystack, needle) + StrLen(needle)
        sub_str := SubStr(haystack, start)

        code_start := InStr(sub_str,"<code>")
        code_end := InStr(sub_str,"</code>")

        If (!code_start && code_end) Or (code_end < code_start)
            return true
        Else
            return false
    }
}

/*
; font-family: Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;

html, body {
    font-family: [_font_name_];
    font-size: [_font_size_]px;
    font-weight: [_font_weight_];
}

body {
    line-height: 1.6em;
}

h1, h2, h3, h4, h5, h6
{ font-weight:bold; margin-top: 24px; margin-bottom: [_font_size_]px; }

.underline { border-bottom: 1px solid #00ACAF; }
h1.underline { padding-bottom: 10px; }
h2.underline { padding-bottom: 8px; }
h3.underline { padding-bottom: 6px; }
h4.underline { padding-bottom: 4px; }
h5.underline { padding-bottom: 2px; }
h1 { font-size:1.75em; }
h2 { font-size:1.5em; }
h3 { font-size:1.30em; }
h4 { font-size:1.15em; }
h5 { font-size:1em; }
h6 { font-size:0.75em; }

blockquote {
    border-left: 4px solid ;
    padding: 0 15px;
    margin-top: 0px;
    margin-bottom: [_font_size_]px;
}

p { margin-bottom: [_font_size_]px; margin-top: 0px; }
li { margin: 4px 0; }


*/
/* ul, ol { margin: 0; padding: 0; }
ul { margin: 4px 0; }
*/


SampleText(){
SyntaxGuide := "
(
# Markdown syntax guide

## Headers

# This is a Heading h1
## This is a Heading h2
### This is a Heading h3
#### This is a Heading h4
##### This is a Heading h5
###### This is a Heading h6

## Emphasis

*This text will be italic*
_This will also be italic_

**This text will be bold**
__This will also be bold__

_You **can** combine them_

## Lists

### Unordered

* Item 1
* Item 2
* Item 2a
* Item 2b

### Ordered

1. Item 1
1. Item 2
1. Item 3
  1. Item 3a
  1. Item 3b

## Images

![AutoHotkey Logo](https://www.autohotkey.com/static/ahk_logo_no_text.svg "AutoHotkey Logo.")

## Links

Help docs can be found [here](https://www.autohotkey.com/docs/).

## Blockquotes

> Markdown is a lightweight markup language with plain-text-formatting syntax, created in 2004 by John Gruber with Aaron Swartz.
>
>> Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor.

## Tables

| Left columns  | Right columns |
| ------------- |:-------------:|
| left foo      | right foo     |
| left bar      | right bar     |
| left baz      | right baz     |

## Blocks of code

```
let message = 'Hello world';
alert(message);
```

## Inline code

This is created with `Ark_make_html()` and `PutHTMLonClipboard()`.
)"
;SyntaxGuide := "_You **can** combine them_"
Return SyntaxGuide
}


list
Posts: 221
Joined: 26 Mar 2014, 14:03
Contact:

Re: "Send()" but for richer content

Post by list » 02 Apr 2023, 12:09

All I can say it depends on the target application, one thing I do do is set the clipboard contents twice, one for the formatted (html, rtf) text and then for the .text version of the clipboard as well, as you may know, the clipboard can hold multiple formats at once and will apply the suitable one e.g. the html in word, but the text in notepad.

You can see it here, I set the HTML, then strip the html tags, and set the text, so in Word it will paste the formatted text, in applications which don't accept rich text (such as notepad) the text version, otherwise it wouldn't paste anything in notepad as that part of the clipboard is empty or may have something else (I haven't checked the code above if you are already doing that) just thought I'd mention it:
https://github.com/lintalist/lintalist/blob/master/lintalist.ahk#L963

User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

Re: "Send()" but for richer content

Post by JoeSchmoe » 03 Apr 2023, 23:50

Thanks for the tip. I'm familiar with that tip, but I appreciate you taking the time.

On another note, I guess it shouldn't surprise me, but I've noticed that Chrome doesn't seem to accept RTF formats. This means I'll standardize on HTML, as I do most of my work in the browser.

Post Reply

Return to “Ask for Help (v2)”