Writing scripts which run on both FoldingText and iOS Drafts 4


#1

Like FoldingText, iOS Drafts can run scripts written in Javascript.

We can write editor and text manipulation scripts which span the iOS ⇄ OS X divide, and run on both FoldingText and drafts, by writing a simple header which defines a FoldingText version of each of the 11 Drafts editor functions:

getText()                               getSelectedLineRange()
setText(string)                         getSelectedRange()
getSelectedText()                       setSelectedRange(start, length)
setSelectedText(string)                 getClipboard()
getTextInRange(start, length)           setClipboard(string)
setTextInRange(start, length, string)

For example, this block selection script which runs in Drafts

http://drafts4-actions.agiletortoise.com/k/1Rw

can also run in FoldingText, if we include the header defining the Drafts functions in terms of the FT API:

// iOS DRAFTS and iOS 1Writer compatibility header  for FT /////////
// 1Writer scripts must be preceded by the compatibility header at
// http://support.foldingtext.com/t/writing-scripts-which-run-on-both-foldingtext-and-ios-drafts-4/652/5?u=complexpoint

// Example function calls:  strText = drafts.getText(); lstRange = drafts.getTextInRange(240,80);

// Ver 0.4 fixed selection range issue, enabling extension of selection to more than one blocks
// Ver 0.3 added drafts. prefix to functions to avoid namespace clashes in apps using any function name
// which happens to be the same as an AgileTortoise Drafts 4 function name.





function run() {

	function fnFT(editor, opt) {

		// iOS DRAFTS compatibility header for FT //
		var oTree = editor.tree();
		var drafts = {
			getText: function () {
				return oTree.text();
			},
			setText: function (strText) {
				editor.setTextContent(strText);
				return true;
			},
			getSelectedText: function () {
				return editor.selectedText();
			},
			setSelectedText: function (strText) {
				editor.replaceSelection(strText, 'around');
				return true;
			},
			getTextInRange: function (iStart, iLength) {
				return oTree.createRangeFromLocation(
					iStart, iLength
				).textInRange();
			},
			setTextInRange: function (iStart, iLength, strText) {
				editor.replaceTextInRange(
					oTree.createRangeFromLocation(iStart, iLength), strText
				);
				return true;
			},
			getSelectedLineRange: function () {
				var oNode = editor.selectedRange().startNode,
					rngLine = oTree.createRangeFromNodes(
						oNode, 0, oNode, -1
					);
				return [rngLine.location(), rngLine.length()];
			},
			getSelectedRange: function () {
				var lstSeln = [];
				editor.selectedRanges().forEach(function (rng) {
					lstSeln.push(rng.location());
					lstSeln.push(rng.length());
				});
				return lstSeln;
			},
			setSelectedRange: function (iStart, iLength) {
				editor.setSelectedRange(
					oTree.createRangeFromLocation(iStart, iLength)
				);
				return true;
			},
			getClipboard: function () {
				return Pasteboard.readString();
			},
			setClipboard: function (strText) {
				Pasteboard.writeString(strText);
				return true;
			}
		};
		// iOS DRAFTS compatibility header for FT //

		function overlap(lstA, lstB) {
			// NOT IF THIS ENDS BEFORE THAT STARTS,
			// OR STARTS AFTER THAT ENDS
			return !(lstA[1] < lstB[0] || lstA[0] > lstB[1]);
		}

		// DRAFTS COMPATIBLE CODE VER 2.0 simpler approach to selecting the block:
		function selnExtendToBlock() {
			
			var rgxGap = /(\n{2,})/,
				lstParts = drafts.getText().split(rgxGap),
				lstSeln = drafts.getSelectedRange(),
				iSelnStart = lstSeln[0],
				iSelnEnd = iSelnStart + lstSeln[1],
				lstBlocks = [],
				strBlock, strGap,
				iFrom = 0,
				iTo, lngBlock;

			// Find first overlap with a selection edge	
			for (var i = 0, lng = lstParts.length; i < lng; i += 2) {
				
				strBlock = lstParts[i];
				strGap = lstParts[i + 1] || '';
				lngBlock = strBlock.length;
				
				iTo = iFrom + lngBlock;
				if (overlap([iSelnStart, iSelnEnd], [iFrom, iTo]))
					lstBlocks.push({
						txt: strBlock,
						start: iFrom,
						end: iFrom + lngBlock
					});
				iFrom = iTo + strGap.length;;
				if (iFrom > iSelnEnd) break;
			}

			// extend selection to just after last gap
			// and just before next gap
			iSelnStart = lstBlocks[0].start;
			drafts.setSelectedRange(iSelnStart, lstBlocks[lstBlocks.length - 1].end - iSelnStart);
			return true;
		}

		return selnExtendToBlock();
	}

	var docsFT = Application("FoldingText").documents(),
		varResult = docsFT.length && docsFT[0].evaluate({
			script: fnFT.toString(),
			withOptions: {}
		});

	return varResult;
}

#2

Applescript (rather than Yosemite Javascript) version of the calling script for FT:

-- iOS DRAFTS and iOS 1Writer compatibility header  for FT /////////
-- 1Writer scripts must be preceded by the compatibility header at
-- http://support.foldingtext.com/t/writing-scripts-which-run-on-both-foldingtext-and-ios-drafts-4/652/5?u=complexpoint

-- Example function calls:  strText = drafts.getText(); lstRange = drafts.getTextInRange(240,80);


-- Ver 0.3 added drafts. prefix to functions to avoid namespace clashes in apps using any function name
-- which happens to be the same as an AgileTortoise Drafts 4 function name.



property pstrJS : "

	function fnFT(editor, opt) {

		// iOS DRAFTS compatibility header for FT //
		var oTree = editor.tree();
		var drafts = {
			getText: function () {
				return oTree.text();
			},
			setText: function (strText) {
				editor.setTextContent(strText);
				return true;
			},
			getSelectedText: function () {
				return editor.selectedText();
			},
			setSelectedText: function (strText) {
				editor.replaceSelection(strText, 'around');
				return true;
			},
			getTextInRange: function (iStart, iLength) {
				return oTree.createRangeFromLocation(
					iStart, iLength
				).textInRange();
			},
			setTextInRange: function (iStart, iLength, strText) {
				editor.replaceTextInRange(
					oTree.createRangeFromLocation(iStart, iLength), strText
				);
				return true;
			},
			getSelectedLineRange: function () {
				var oNode = editor.selectedRange().startNode,
					rngLine = oTree.createRangeFromNodes(
						oNode, 0, oNode, -1
					);
				return [rngLine.location(), rngLine.length()];
			},
			getSelectedRange: function () {
				var lstSeln = [];
				editor.selectedRanges().forEach(function (rng) {
					lstSeln.push(rng.location());
					lstSeln.push(rng.length());
				});
				return lstSeln;
			},
			setSelectedRange: function (iStart, iLength) {
				editor.setSelectedRange(
					oTree.createRangeFromLocation(iStart, iLength)
				);
				return true;
			},
			getClipboard: function () {
				return Pasteboard.readString();
			},
			setClipboard: function (strText) {
				Pasteboard.writeString(strText);
				return true;
			}
		};
		// iOS DRAFTS compatibility header for FT //

		function overlap(lstA, lstB) {
			// NOT IF THIS ENDS BEFORE THAT STARTS,
			// OR STARTS AFTER THAT ENDS
			return !(lstA[1] < lstB[0] || lstA[0] > lstB[1]);
		}

		// DRAFTS COMPATIBLE CODE VER 2.0 simpler approach to selecting the block:
		function selnExtendToBlock() {
			
			var rgxGap = /(\\n{2,})/,
				lstParts = drafts.getText().split(rgxGap),
				lstSeln = drafts.getSelectedRange(),
				iSelnStart = lstSeln[0],
				iSelnEnd = iSelnStart + lstSeln[1],
				lstBlocks = [],
				strBlock, strGap,
				iFrom = 0,
				iTo, lngBlock;

			// Find first overlap with a selection edge	
			for (var i = 0, lng = lstParts.length; i < lng; i += 2) {
				
				strBlock = lstParts[i];
				strGap = lstParts[i + 1] || '';
				lngBlock = strBlock.length;
				
				iTo = iFrom + lngBlock;
				if (overlap([iSelnStart, iSelnEnd], [iFrom, iTo]))
					lstBlocks.push({
						txt: strBlock,
						start: iFrom,
						end: iFrom + lngBlock
					});
				iFrom = iTo + strGap.length;;
				if (iFrom > iSelnEnd) break;
			}

			// extend selection to just after last gap
			// and just before next gap
			iSelnStart = lstBlocks[0].start;
			drafts.setSelectedRange(iSelnStart, lstBlocks[lstBlocks.length - 1].end - iSelnStart);
			return true;
		}

		return selnExtendToBlock();
	}
"
on run
	tell application "FoldingText"
		
		set lstDocs to documents
		if lstDocs ≠ {} then
			tell item 1 of lstDocs
				set varResult to (evaluate script pstrJS with options {})
			end tell
		end if
		return varResult
	end tell
end run


#3

This is pretty cool. The app 1Writer on iOS also recently supports Javascript, which may make similar overlap possible.

1Writer Javascript Documentation


#4

Good thought. I’ll take a look – thanks for the pointer.


#5

To write FoldingText scripts which can also run on iOS iWriter (as well as iOS Drafts):

  1. In the FT script, include the Drafts compatibility header (following post in this thread), and write the script using the shared functions, rather than using FT functions directly.
  2. In iOS Drafts, use the script preceded by the one-line header: var drafts = this;
  3. In iOS 1Writer, precede the script with the FoldingText/Drafts/iWriter compatibility header below.

Format for function calls, across all 3 applications, in scripts preceded by the header for that app:

strText = drafts.getText(); 
lstRange = drafts.getTextInRange(240,80);
// DRAFTS: functionS (for sharing scripts between FoldingText, iOS Drafts, and iOS 1Writer)
// getText()                               getSelectedLineRange()
// setText(string)                         getSelectedRange()
// getSelectedText()                       setSelectedRange(start, length)
// setSelectedText(string)                 getClipboard()
// getTextInRange(start, length)           setClipboard(string)
// setTextInRange(start, length, string)

// 1Writer versions of the: functions - include as header at the start of 1Writer 
// Javascript actions
// For the FoldingText versions of these: functions, see:

// (http://support.foldingtext.com/t/writing-scripts-which-run-on-both-foldingtext-and-ios-drafts-4/652/2)
// Ver 0.3 prefixed functions with drafts eg drafts.getText(), to avoid namespace collisions where 1Writer 
// function names coincide
// Ver 0.2 (amended getSelectedLineRange: function () to iStart, iLength format)
var drafts = {
	getText: function () {
		return editor.getText(); //1W
	},
	setText: function (strText) {
		editor.setText(strText); //1W
		return true;
	},
	getSelectedText: function () {
		return editor.getSelectedText(); //1W
	},
	setSelectedText: function (strText) {
		editor.replaceSelection(replacement); //1W
		return true;
	},
	getTextInRange: function (iStart, iLength) {
		//1 W
		// in 1Writer we have to do this by changing the selection
		// temporarily, reading text, and then restoring the selection
		var lstSeln = editor.getSelectedRange(),
			strText;
		editor.setSelectedRange(iStart, iStart + iLength);
		strText = editor.getSelectedText();
		editor.setSelectedRange(lstSeln[0], lstSeln[1]);
		return strText;
	},
	setTextInRange: function (iStart, iLength, strText) {
		editor.replaceTextInRange(iStart, iStart + iLength, strText); // 1W
		return true;
	},
	getSelectedLineRange: function () {
		var lstStartEnd = editor.getSelectedLineRange(), // 1W
			iStart = lstStartEnd[0];
		return [iStart, iStart + lstStartEnd[1]];
	},
	getSelectedRange: function () {
		var lstStartEnd = editor.getSelectedRange(), // 1W
			iStart = lstStartEnd[0];
		return [iStart, iStart + lstStartEnd[1]];
	},
	setSelectedRange: function (iStart, iLength) {
		editor.setSelectedRange(iStart, iStart + iLength); // 1W
		return true;
	},
	getClipboard: function () {
		return app.getClipboard(); // 1W
	},
	setClipboard: function (strText) {
		app.setClipboard(strText); // 1w
		return true;
	}
};

#6

// iOS DRAFTS compatibility header for FT //
var oTree = editor.tree();
var drafts = {
	getText: function () {
		return oTree.text();
	},
	setText: function (strText) {
		editor.setTextContent(strText);
		return true;
	},
	getSelectedText: function () {
		return editor.selectedText();
	},
	setSelectedText: function (strText) {
		editor.replaceSelection(strText, 'around');
		return true;
	},
	getTextInRange: function (iStart, iLength) {
		return oTree.createRangeFromLocation(
			iStart, iLength
		).textInRange();
	},
	setTextInRange: function (iStart, iLength, strText) {
		editor.replaceTextInRange(
			oTree.createRangeFromLocation(iStart, iLength), strText
		);
		return true;
	},
	getSelectedLineRange: function () {
		var oNode = editor.selectedRange().startNode,
			rngLine = oTree.createRangeFromNodes(
				oNode, 0, oNode, -1
			);
		return [rngLine.location(), rngLine.length()];
	},
	getSelectedRange: function () {
		var lstSeln = [];
		editor.selectedRanges().forEach(function (rng) {
			lstSeln.push(rng.location());
			lstSeln.push(rng.length());
		});
		return lstSeln;
	},
	setSelectedRange: function (iStart, iLength) {
		editor.setSelectedRange(
			oTree.createRangeFromLocation(iStart, iLength)
		);
		return true;
	},
	getClipboard: function () {
		return Pasteboard.readString();
	},
	setClipboard: function (strText) {
		Pasteboard.writeString(strText);
		return true;
	}
};

#7

Adding Textwell (iOS and OS X) to the pool of those which can share scripts with FoldingText and Drafts:

// Writing scripts which run on FoldingText and iOS Drafts
// iOS Drafts 4.0 compatibility header for TextWell 1.1 (OS X + iOS)

var drafts = {
	getText: function () {
		return T.text;
	},
	setText: function (strText) {
		T('replaceWhole', {
			text: strText
		});
		return T('done', {});
	},
	getSelectedText: function () {
		return T.selectedText;
	},
	setSelectedText: function (strText) {
		T('replaceCurrent', {
			text: strText
		});
		return T('done', {});
	},
	getTextInRange: function (iStart, iLength) {
		return T.chars(iStart, iStart + iLength);
	},
	setTextInRange: function (iStart, iLength, strText) {
		T('replaceRange', {
			text: strText,
			replacingRange: {
				loc: iStart,
				len: iLength
			}
		});
		return T('done', {});
	},
	getSelectedLineRange: function () {
		var dctRange = T.range,
			strText = T.text,
			iLoc = dctRange.loc,
			iEnd = iLoc + dctRange.len,
			lngChars = strText.length;

		while ((iLoc > 0) && (strText[iLoc] !== '\n')) {
			iLoc--;
		}
		while ((iEnd < lngChars) && (strText[iEnd] !== '\n')) {
			iEnd++;
		}
		return [iLoc, iEnd - iLoc];
	},

	getSelectedRange: function () {
		var dctRange = T.range;
		return [dctRange.loc, dctRange.len];
	},

	setSelectedRange: function (iStart, iLength) {
		T('replaceRange', {
			text: '',
			replacingRange: {
				loc: 0,
				len: 0
			},
			selectingRange: {
				loc: iStart,
				len: iLength
			}
		});
		return T('done', {});;
	},
	getClipboard: function () {
		return T.pboard;
	},
	setClipboard: function (strText) {
		T('copy', {
			text: strText
		});
		return T('done', {});;
	}
};

function overlap(lstA, lstB) {
	// NOT IF THIS ENDS BEFORE THAT STARTS,
	// OR STARTS AFTER THAT ENDS
	return !(lstA[1] < lstB[0] || lstA[0] > lstB[1]);
}

// DRAFTS COMPATIBLE CODE VER 2.0 simpler approach to selecting the block:
function selnExtendToBlock() {

	var rgxGap = /(\n{2,})/,
		lstParts = drafts.getText().split(rgxGap),
		lstSeln = drafts.getSelectedRange(),
		iSelnStart = lstSeln[0],
		iSelnEnd = iSelnStart + lstSeln[1],
		lstBlocks = [],
		strBlock, strGap,
		iFrom = 0,
		iTo, lngBlock;

	// Find first overlap with a selection edge	
	for (var i = 0, lng = lstParts.length; i < lng; i += 2) {

		strBlock = lstParts[i];
		strGap = lstParts[i + 1] || '';
		lngBlock = strBlock.length;

		iTo = iFrom + lngBlock;
		if (overlap([iSelnStart, iSelnEnd], [iFrom, iTo]))
			lstBlocks.push({
				txt: strBlock,
				start: iFrom,
				end: iFrom + lngBlock
			});
		iFrom = iTo + strGap.length;
		if (iFrom > iSelnEnd) break;
	}

	// extend selection to just after last gap
	// and just before next gap
	iSelnStart = lstBlocks[0].start;
	drafts.setSelectedRange(iSelnStart, lstBlocks[lstBlocks.length - 1].end - iSelnStart);
	return true;
}

selnExtendToBlock();

#8

Adding OS X 10.10 (Yosemite) Script Editor to the set of editors which can share text/editor scripts with OS X FoldingText and iOS Drafts 4.

Include the following header before the scripts:

// Ver 0.4 Rob Trew 
// Header for using OSX Yosemite Script Editor scripts
// written in an idiom compatible with FoldingText, Drafts, 1Writer, TextWell
// In iOS Drafts 4 scripts, the header is simply:
//	var drafts = this;
// For headers for FoldingText and other editors, see:
// See: http://support.foldingtext.com/t/writing-scripts-which-run-on-both-foldingtext-and-ios-drafts-4/652/7

// Call functions, after this header, with the prefix:
//		drafts.
// e.g.
//		var drafts = Library('DraftsSE');
//		var dctSelnRange = drafts.getSelectedRange(),
//			iFrom = dctSelnRange[0],
//			iTo = iFrom + dctSelnRange[1];

// These functions are intended to be compatible with Agile Tortoise's Drafts documentation at:
// https://agiletortoise.zendesk.com/hc/en-us/articles/202465564-Script-Keys

// getText()                               getSelectedLineRange()
// setText(string)                         getSelectedRange()
// getSelectedText()                       setSelectedRange(start, length)
// setSelectedText(string)                 getClipboard()
// getTextInRange(start, length)           setClipboard(string)
// setTextInRange(start, length, string)

// Installation: Save this as a compiled .scpt to ~/Library/Script Libraries/DraftsSE.scpt
// and then invoke in other scripts as above, through the JXA Library object.

var appSE = Application("Script Editor"),
	lstDocs = appSE.documents(),
	oDoc = lstDocs.length ? lstDocs[0] : null;

appSE.includeStandardAdditions = true;
appSE.activate();

function getText() {
	if (oDoc) return oDoc.text();
}

function setText(strText) {
	if (oDoc) oDoc.text = strText;
}

function getSelectedText() {
	if (oDoc) return oDoc.selection.contents();
}

function setSelectedText(strText) {
	if (oDoc) oDoc.selection.contents = strText;
}

function getTextInRange(iStart, iLength) {
	if (oDoc) return oDoc.text().substring(iStart, iStart + iLength);
}

function setTextInRange(iStart, iLength, strText) {
	if (oDoc) { // record the existing selection coordinates
		var lngDelta = (strText.length - iLength) - 2,
			oSeln = oDoc.selection, dct = oSeln.characterRange(),
			iFrom = dct.x, iTo = dct.y;

		try { // use the selection to edit elsewhere, then restore with any adjustment
			oDoc.selection = oDoc.text.characters.slice(iStart, iStart + iLength);
			oSeln.contents = strText;
			oDoc.selection = oDoc.text.characters.slice(
				(iStart < iFrom) ? iFrom + lngDelta : iFrom,
				((iStart + iLength) < iTo) ? iTo + lngDelta : iTo
			);
		} catch (e) {}
	}
}

function getSelectedLineRange() {
	// works, but let me know if you see a shorter route :-)
	var dct, strFull, iFrom, iTo, lngChars;
	if (oDoc) {
		dct = oDoc.selection.characterRange();
		strFull = oDoc.text();
		lngChars = strFull.length;
		iFrom = dct.x;
		while (iFrom--)
			if (strFull[iFrom] === '\n') break;
		iFrom += 1;
		iTo = dct.y - 1;
		while (iTo++ < lngChars)
			if (strFull[iTo] === '\n') break;
		return [iFrom, (iTo - iFrom)];
	}
}

function getSelectedRange() {
	var dct, iFrom, iTo;
	if (oDoc) {
		dct = oDoc.selection.characterRange();
	}
	iFrom = dct.x;
	iTo = dct.y;
	return [iFrom, iTo - iFrom];
}

function setSelectedRange(iStart, iLength) {
	if (oDoc) {
		oDoc.selection = oDoc.text.characters.slice(iStart, iStart + iLength);
	}
}

function getClipboard() {
	return appSE.theClipboard();
}

function setClipboard(strText) {
	appSE.setTheClipboardTo(strText);
}

#9

Any chance for adding BBEdit with JXA to this list? I have been working a bit with JS on Drafts 5 and was wondering about a Mac option to reuse that code.


#10

A bit busy now but I should be able to take a look in c. 10 days. Could you remind me if I don’t seem to have done anything c. 14 days hence ?


#11

(bing)


#12

I’ve sent you a first experiment for testing in a message on the Drafts 5 forum.


#13

And a lightly edited version here: