Add anchor tags to HTTP Server output
This means you can now load the raw text in a browser and the resulting page will have basic blue links that can be clicked on that will in turn be loaded by the HTTP service. A significant feature, so worthy of a minor version bump to; v1.1.0
This commit is contained in:
parent
1b42630b7f
commit
3149db4bd3
|
@ -60,10 +60,23 @@ func (h *slashFix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
func handleHTTPServerRequest(w http.ResponseWriter, r *http.Request) {
|
func handleHTTPServerRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
urlForBrowsh, _ := url.PathUnescape(strings.TrimPrefix(r.URL.Path, "/"))
|
urlForBrowsh, _ := url.PathUnescape(strings.TrimPrefix(r.URL.Path, "/"))
|
||||||
rawTextRequestID := pseudoUUID()
|
rawTextRequestID := pseudoUUID()
|
||||||
sendMessageToWebExtension("/raw_text_request," + rawTextRequestID + "," + urlForBrowsh)
|
mode := getRawTextMode(r)
|
||||||
|
sendMessageToWebExtension(
|
||||||
|
"/raw_text_request," + rawTextRequestID + "," +
|
||||||
|
mode + "," +
|
||||||
|
urlForBrowsh)
|
||||||
waitForResponse(rawTextRequestID, w)
|
waitForResponse(rawTextRequestID, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 'PLAIN' mode returns raw text without any HTML whatsoever.
|
||||||
|
// 'HTML' mode returns some basic HTML tags for things like anchor links.
|
||||||
|
func getRawTextMode(r *http.Request) string {
|
||||||
|
var mode = "HTML"
|
||||||
|
if (strings.Contains(r.Host, "text.")) { mode = "PLAIN" }
|
||||||
|
if (r.Header.Get("X-Browsh-Raw-Mode") == "PLAIN") { mode = "PLAIN" }
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
func waitForResponse(rawTextRequestID string, w http.ResponseWriter) {
|
func waitForResponse(rawTextRequestID string, w http.ResponseWriter) {
|
||||||
var rawTextRequestResponse string
|
var rawTextRequestResponse string
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
|
@ -13,9 +13,16 @@ func TestHTTPServer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = Describe("HTTP Server", func() {
|
var _ = Describe("HTTP Server", func() {
|
||||||
It("should return text", func() {
|
It("should return plain text", func() {
|
||||||
response := getPath("/smorgasbord")
|
response := getPath("/smorgasbord", "plain")
|
||||||
Expect(response).To(ContainSubstring("multiple hot Smörgås"))
|
Expect(response).To(ContainSubstring("multiple hot Smörgås"))
|
||||||
Expect(response).To(ContainSubstring("A special Swedish type of smörgåsbord"))
|
Expect(response).To(ContainSubstring("A special Swedish type of smörgåsbord"))
|
||||||
|
Expect(response).ToNot(ContainSubstring("<a href"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return HTML text", func() {
|
||||||
|
response := getPath("/smorgasbord", "html")
|
||||||
|
Expect(response).To(ContainSubstring(
|
||||||
|
"<a href=\"/http://localhost:4444/smorgasbord/another.html\">Another page</a>"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,11 +26,16 @@ func startBrowsh() {
|
||||||
browsh.HTTPServerStart()
|
browsh.HTTPServerStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPath(path string) string {
|
func getPath(path string, mode string) string {
|
||||||
browshServiceBase := "http://localhost:" + *browsh.HTTPServerPort
|
browshServiceBase := "http://localhost:" + *browsh.HTTPServerPort
|
||||||
staticFileServerBase := "http://localhost:" + staticFileServerPort
|
staticFileServerBase := "http://localhost:" + staticFileServerPort
|
||||||
fullBase := browshServiceBase + "/" + staticFileServerBase
|
fullBase := browshServiceBase + "/" + staticFileServerBase
|
||||||
response, err := http.Get(fullBase + path)
|
client := &http.Client{}
|
||||||
|
request, err := http.NewRequest("GET", fullBase + path, nil)
|
||||||
|
if mode == "plain" {
|
||||||
|
request.Header.Add("X-Browsh-Raw-Mode", "PLAIN")
|
||||||
|
}
|
||||||
|
response, err := client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("%s", err))
|
panic(fmt.Sprintf("%s", err))
|
||||||
} else {
|
} else {
|
||||||
|
@ -57,7 +62,7 @@ var _ = ginkgo.BeforeSuite(func() {
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
// Allow the browser to sort its sizing out, because sometimes the first test catches the
|
// Allow the browser to sort its sizing out, because sometimes the first test catches the
|
||||||
// browser before it's completed its resizing.
|
// browser before it's completed its resizing.
|
||||||
getPath("/smorgasbord")
|
getPath("/smorgasbord", "plain")
|
||||||
})
|
})
|
||||||
|
|
||||||
var _ = ginkgo.AfterSuite(func() {
|
var _ = ginkgo.AfterSuite(func() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Browsh",
|
"name": "Browsh",
|
||||||
"version": "1.0.13",
|
"version": "1.1.0",
|
||||||
|
|
||||||
"description": "Renders the browser as realtime, interactive, TTY-compatible text",
|
"description": "Renders the browser as realtime, interactive, TTY-compatible text",
|
||||||
|
|
||||||
|
|
|
@ -176,7 +176,6 @@ export default class extends utils.mixins(CommonMixin, TTYCommandsMixin) {
|
||||||
this.log(`Tab ${channel.name} connected for communication with background process`);
|
this.log(`Tab ${channel.name} connected for communication with background process`);
|
||||||
let tab = this.tabs[parseInt(channel.name)];
|
let tab = this.tabs[parseInt(channel.name)];
|
||||||
tab.postConnectionInit(channel);
|
tab.postConnectionInit(channel);
|
||||||
tab.setMode(this._is_raw_text_mode);
|
|
||||||
this._is_connected_to_browser_dom = true;
|
this._is_connected_to_browser_dom = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ export default class extends utils.mixins(CommonMixin, TabCommandsMixin) {
|
||||||
this._tab_reloads = 0;
|
this._tab_reloads = 0;
|
||||||
// The maximum amount of times to try to recover a tab that won't connect
|
// The maximum amount of times to try to recover a tab that won't connect
|
||||||
this._max_number_of_tab_recovery_reloads = 3;
|
this._max_number_of_tab_recovery_reloads = 3;
|
||||||
|
// Type of raw text mode; HTML or plain
|
||||||
|
this.raw_text_mode = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
postDOMLoadInit(terminal, dimensions) {
|
postDOMLoadInit(terminal, dimensions) {
|
||||||
|
@ -22,6 +24,9 @@ export default class extends utils.mixins(CommonMixin, TabCommandsMixin) {
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
this._sendTTYDimensions();
|
this._sendTTYDimensions();
|
||||||
this._listenForMessages();
|
this._listenForMessages();
|
||||||
|
let mode = 'interactive';
|
||||||
|
if (this.raw_text_mode !== '') { mode = this.raw_text_mode }
|
||||||
|
this.channel.postMessage(`/mode,${mode}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
isConnected() {
|
isConnected() {
|
||||||
|
@ -104,26 +109,8 @@ export default class extends utils.mixins(CommonMixin, TabCommandsMixin) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setMode(is_raw_text_mode) {
|
setMode(mode) {
|
||||||
if (is_raw_text_mode) {
|
this.raw_text_mode = mode;
|
||||||
this.channel.postMessage('/mode,raw_text');
|
|
||||||
this._requestRawText();
|
|
||||||
} else {
|
|
||||||
this.channel.postMessage('/mode,interactive');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Browsh is setup in HTTP-server mode then this is the moment that we ask the tab to
|
|
||||||
// render the entire DOM as plain text. We must only do this for tabs subsequent to the
|
|
||||||
// initial tab that loads at boot time (there must always remain a single tab to keep the
|
|
||||||
// browser running).
|
|
||||||
_requestRawText() {
|
|
||||||
// The assumption is that Tab ID 1 is always the first tab. However, I have a vague
|
|
||||||
// memory of seeing the "Firefox Privacy Notice" tab load before the CLI argument requested
|
|
||||||
// URL. So, maybe ID 1 isn't 100% reliable.
|
|
||||||
if (this.id !== 1) {
|
|
||||||
this.channel.postMessage('/request_raw_text');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_listenForMessages() {
|
_listenForMessages() {
|
||||||
|
@ -146,6 +133,7 @@ export default class extends utils.mixins(CommonMixin, TabCommandsMixin) {
|
||||||
// first. So let's just close that tab.
|
// first. So let's just close that tab.
|
||||||
// TODO: Only do this for a testing ENV?
|
// TODO: Only do this for a testing ENV?
|
||||||
_closeUnwantedStartupTabs() {
|
_closeUnwantedStartupTabs() {
|
||||||
|
if (this.title === undefined) { return false }
|
||||||
if (
|
if (
|
||||||
this.title.includes('Firefox by default shares data to:') ||
|
this.title.includes('Firefox by default shares data to:') ||
|
||||||
this.title.includes('Firefox Privacy Notice')
|
this.title.includes('Firefox Privacy Notice')
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default (MixinBase) => class extends MixinBase {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case '/raw_text_request':
|
case '/raw_text_request':
|
||||||
this._rawTextRequest(parts[1], parts.slice(2).join(','));
|
this._rawTextRequest(parts[1], parts[2], parts.slice(3).join(','));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,12 +164,13 @@ export default (MixinBase) => class extends MixinBase {
|
||||||
this.sendToTerminal('/screenshot,' + data);
|
this.sendToTerminal('/screenshot,' + data);
|
||||||
}
|
}
|
||||||
|
|
||||||
_rawTextRequest(request_id, url) {
|
_rawTextRequest(request_id, mode, url) {
|
||||||
this.createNewTab(url, tab => {
|
this.createNewTab(url, native_tab => {
|
||||||
this._acknowledgeNewTab({
|
this._acknowledgeNewTab({
|
||||||
id: tab.id,
|
id: native_tab.id,
|
||||||
request_id: request_id
|
request_id: request_id
|
||||||
})
|
});
|
||||||
|
this.tabs[native_tab.id].setMode(`raw_text_${mode.toLowerCase()}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,6 @@ export default (MixinBase) => class extends MixinBase {
|
||||||
this.sendAllBigFrames();
|
this.sendAllBigFrames();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case '/request_raw_text':
|
|
||||||
this.sendRawText();
|
|
||||||
break;
|
|
||||||
case '/scroll_status':
|
case '/scroll_status':
|
||||||
this._handleScroll(parts[1], parts[2]);
|
this._handleScroll(parts[1], parts[2]);
|
||||||
break;
|
break;
|
||||||
|
@ -50,10 +47,14 @@ export default (MixinBase) => class extends MixinBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupMode(mode) {
|
_setupMode(mode) {
|
||||||
if (mode === 'raw_text') {
|
if (mode === 'raw_text_plain' || mode === 'raw_text_html') {
|
||||||
this._is_raw_text_mode = true;
|
this._is_raw_text_mode = true;
|
||||||
|
this._is_interactive_mode = false;
|
||||||
|
this._raw_mode_type = mode;
|
||||||
|
this.sendRawText();
|
||||||
}
|
}
|
||||||
if (mode === 'interactive') {
|
if (mode === 'interactive') {
|
||||||
|
this._is_raw_text_mode = false;
|
||||||
this._is_interactive_mode = true;
|
this._is_interactive_mode = true;
|
||||||
this._setupInteractiveMode();
|
this._setupInteractiveMode();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default class extends utils.mixins(CommonMixin, CommandsMixin) {
|
||||||
// For Browsh used via the interactive CLI ap
|
// For Browsh used via the interactive CLI ap
|
||||||
this._is_interactive_mode = false;
|
this._is_interactive_mode = false;
|
||||||
// For Browsh used via the HTTP server
|
// For Browsh used via the HTTP server
|
||||||
this._is_raw_text_mode = false;
|
this._is_raw_mode = false;
|
||||||
this._setupInit();
|
this._setupInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export default class extends utils.mixins(CommonMixin, CommandsMixin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendAllBigFrames() {
|
sendAllBigFrames() {
|
||||||
if (this._is_raw_text_mode) { return }
|
if (this._is_raw_mode) { return }
|
||||||
if (!this.dimensions.tty.width) {
|
if (!this.dimensions.tty.width) {
|
||||||
this.log("Not sending big frames without TTY data")
|
this.log("Not sending big frames without TTY data")
|
||||||
return
|
return
|
||||||
|
@ -62,7 +62,7 @@ export default class extends utils.mixins(CommonMixin, CommandsMixin) {
|
||||||
sendRawText() {
|
sendRawText() {
|
||||||
this.dimensions.update();
|
this.dimensions.update();
|
||||||
this.dimensions.setSubFrameDimensions('raw_text');
|
this.dimensions.setSubFrameDimensions('raw_text');
|
||||||
this.text_builder.sendRawText();
|
this.text_builder.sendRawText(this._raw_mode_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendSmallPixelFrame() {
|
sendSmallPixelFrame() {
|
||||||
|
|
202
webext/src/dom/serialise_mixin.js
Normal file
202
webext/src/dom/serialise_mixin.js
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
import utils from 'utils';
|
||||||
|
|
||||||
|
export default (MixinBase) => class extends MixinBase {
|
||||||
|
__serialiseFrame() {
|
||||||
|
let cell, index;
|
||||||
|
const top = this.dimensions.frame.sub.top / 2;
|
||||||
|
const left = this.dimensions.frame.sub.left;
|
||||||
|
const bottom = top + (this.dimensions.frame.sub.height / 2);
|
||||||
|
const right = left + this.dimensions.frame.sub.width;
|
||||||
|
this._setupFrameMeta();
|
||||||
|
this._serialiseInputBoxes();
|
||||||
|
for (let y = top; y < bottom; y++) {
|
||||||
|
for (let x = left; x < right; x++) {
|
||||||
|
index = (y * this.dimensions.frame.width) + x;
|
||||||
|
cell = this.tty_grid.cells[index];
|
||||||
|
if (cell === undefined) {
|
||||||
|
this.frame.colours.push(0)
|
||||||
|
this.frame.colours.push(0)
|
||||||
|
this.frame.colours.push(0)
|
||||||
|
this.frame.text.push("")
|
||||||
|
} else {
|
||||||
|
cell.fg_colour.map((c) => this.frame.colours.push(c));
|
||||||
|
this.frame.text.push(cell.rune);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_serialiseRawText() {
|
||||||
|
let raw_text = "";
|
||||||
|
this._previous_cell_href = '';
|
||||||
|
this._is_inside_anchor = false;
|
||||||
|
const top = this.dimensions.frame.sub.top / 2;
|
||||||
|
const left = this.dimensions.frame.sub.left;
|
||||||
|
const bottom = top + (this.dimensions.frame.sub.height / 2);
|
||||||
|
const right = left + this.dimensions.frame.sub.width;
|
||||||
|
for (let y = top; y < bottom; y++) {
|
||||||
|
for (let x = left; x < right; x++) {
|
||||||
|
raw_text += this._addCell(x, y, right);
|
||||||
|
}
|
||||||
|
raw_text += "\n";
|
||||||
|
}
|
||||||
|
const head = `<html><title>${document.title}</title><body><pre>`
|
||||||
|
const foot = `</body></pre></html>`
|
||||||
|
return head + raw_text + foot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Ultimately we're going to need to know exactly which parts of the input
|
||||||
|
// box are obscured. This is partly possible using the element's computed
|
||||||
|
// styles, however this isn't comprehensive - think partially obscuring.
|
||||||
|
// So the best solution is to use the same trick as we do for normal text,
|
||||||
|
// except that we can't fill the input box with text, however we can
|
||||||
|
// temporarily change the background to a contrasting colour.
|
||||||
|
_getAllInputBoxes() {
|
||||||
|
let dom_rect, styles, font_rgb;
|
||||||
|
let parsed_input_boxes = {};
|
||||||
|
let raw_input_boxes = document.querySelectorAll(
|
||||||
|
'input, ' +
|
||||||
|
'textarea, ' +
|
||||||
|
'[role="textbox"]'
|
||||||
|
);
|
||||||
|
raw_input_boxes.forEach((i) => {
|
||||||
|
let type;
|
||||||
|
this._ensureBrowshID(i);
|
||||||
|
dom_rect = this._convertDOMRectToAbsoluteCoords(i.getBoundingClientRect());
|
||||||
|
const width = utils.snap(dom_rect.width * this.dimensions.scale_factor.width);
|
||||||
|
const height = utils.snap(dom_rect.height * this.dimensions.scale_factor.height);
|
||||||
|
if (width == 0 || height == 0) { return }
|
||||||
|
if (i.getAttribute('role') == 'textbox') {
|
||||||
|
type = 'textbox';
|
||||||
|
} else {
|
||||||
|
type = i.getAttribute('type');
|
||||||
|
}
|
||||||
|
styles = window.getComputedStyle(i);
|
||||||
|
font_rgb = styles['color'].replace(/[^\d,]/g, '').split(',').map((i) => parseInt(i));
|
||||||
|
if (this._isUnwantedInboxBox(i, styles)) { return }
|
||||||
|
parsed_input_boxes[i.getAttribute('data-browsh-id')] = {
|
||||||
|
id: i.getAttribute('data-browsh-id'),
|
||||||
|
x: utils.snap(dom_rect.left * this.dimensions.scale_factor.width),
|
||||||
|
y: utils.snap(dom_rect.top * this.dimensions.scale_factor.height),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
tag_name: i.nodeName,
|
||||||
|
type: type,
|
||||||
|
colour: [font_rgb[0], font_rgb[1], font_rgb[2]]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return parsed_input_boxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureBrowshID(element) {
|
||||||
|
if (element.getAttribute('data-browsh-id') === null) {
|
||||||
|
element.setAttribute('data-browsh-id', utils.uuidv4());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isUnwantedInboxBox(input_box, styles) {
|
||||||
|
if (styles.display === 'none' || styles.visibility === 'hidden') { return true }
|
||||||
|
if (input_box.getAttribute('aria-hidden') == 'true') { return true }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendRawText() {
|
||||||
|
let payload = {
|
||||||
|
body: this._serialiseRawText()
|
||||||
|
}
|
||||||
|
this.sendMessage(`/raw_text,${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendFrame() {
|
||||||
|
this._serialiseFrame();
|
||||||
|
if (this.frame.text.length > 0) {
|
||||||
|
this.sendMessage(`/frame_text,${JSON.stringify(this.frame)}`);
|
||||||
|
} else {
|
||||||
|
this.log("Not sending empty text frame");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_addCell(x, y, right) {
|
||||||
|
let text = "";
|
||||||
|
const index = (y * this.dimensions.frame.width) + x;
|
||||||
|
this._cell_for_raw_text = this.tty_grid.cells[index];
|
||||||
|
if (this._raw_mode_type === 'raw_text_html') {
|
||||||
|
this._is_line_end = (x === right - 1);
|
||||||
|
text += this._addCellAsHTML();
|
||||||
|
} else {
|
||||||
|
text += this._addCellAsPlainText();
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
_addCellAsHTML() {
|
||||||
|
this._HTML = '';
|
||||||
|
if (!this._cell_for_raw_text) {
|
||||||
|
this._addHTMLForNonExistentCell();
|
||||||
|
return this._HTML;
|
||||||
|
}
|
||||||
|
this._current_cell_href = this._cell_for_raw_text.parent_element.href;
|
||||||
|
this._is_HREF_changed = this._current_cell_href !== this._previous_cell_href
|
||||||
|
this._handleCellOutsideAnchor();
|
||||||
|
this._handleCellInsideAnchor();
|
||||||
|
this._HTML += this._cell_for_raw_text.rune;
|
||||||
|
if (this._will_be_inside_anchor !== undefined) {
|
||||||
|
this._is_inside_anchor = this._will_be_inside_anchor;
|
||||||
|
}
|
||||||
|
this._previous_cell_href = this._current_cell_href;
|
||||||
|
return this._HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
_addHTMLForNonExistentCell() {
|
||||||
|
if (this._is_inside_anchor) {
|
||||||
|
this._previous_cell_href = undefined;
|
||||||
|
this._closeAnchorTag();
|
||||||
|
}
|
||||||
|
this._HTML += " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleCellOutsideAnchor() {
|
||||||
|
if (this._is_inside_anchor) { return }
|
||||||
|
if (this._current_cell_href || this._is_HREF_changed) {
|
||||||
|
this._openAnchorTag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleCellInsideAnchor() {
|
||||||
|
if (!this._is_inside_anchor) { return }
|
||||||
|
if (this._is_HREF_changed || !this._current_cell_href || this._is_line_end) {
|
||||||
|
this._closeAnchorTag();
|
||||||
|
if (this._current_cell_href) {
|
||||||
|
this._openAnchorTag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_openAnchorTag() {
|
||||||
|
this._will_be_inside_anchor = true;
|
||||||
|
this._HTML += `<a href="/${this._current_cell_href}">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_closeAnchorTag() {
|
||||||
|
this._will_be_inside_anchor = false;
|
||||||
|
this._HTML += `</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_addCellAsPlainText() {
|
||||||
|
if (this._cell_for_raw_text === undefined) { return " " }
|
||||||
|
return this._cell_for_raw_text.rune;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupFrameMeta() {
|
||||||
|
this.frame = {
|
||||||
|
meta: this.dimensions.getFrameMeta(),
|
||||||
|
text: [],
|
||||||
|
colours: []
|
||||||
|
};
|
||||||
|
this.frame.meta.id = parseInt(this.channel.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_serialiseInputBoxes() {
|
||||||
|
this.frame.input_boxes = this._getAllInputBoxes();
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,13 @@ import _ from 'lodash';
|
||||||
|
|
||||||
import utils from 'utils';
|
import utils from 'utils';
|
||||||
import CommonMixin from 'dom/common_mixin';
|
import CommonMixin from 'dom/common_mixin';
|
||||||
|
import SerialiseMixin from 'dom/serialise_mixin';
|
||||||
import TTYCell from 'dom/tty_cell';
|
import TTYCell from 'dom/tty_cell';
|
||||||
import TTYGrid from 'dom/tty_grid';
|
import TTYGrid from 'dom/tty_grid';
|
||||||
|
|
||||||
// Convert the text on the page into a snapped 2-dimensional grid to be displayed directly
|
// Convert the text on the page into a snapped 2-dimensional grid to be displayed directly
|
||||||
// in the terminal.
|
// in the terminal.
|
||||||
export default class extends utils.mixins(CommonMixin) {
|
export default class extends utils.mixins(CommonMixin, SerialiseMixin) {
|
||||||
constructor(channel, dimensions, graphics_builder) {
|
constructor(channel, dimensions, graphics_builder) {
|
||||||
super();
|
super();
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
|
@ -26,7 +27,8 @@ export default class extends utils.mixins(CommonMixin) {
|
||||||
this._sendFrame();
|
this._sendFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRawText() {
|
sendRawText(type) {
|
||||||
|
this._raw_mode_type = type;
|
||||||
// TODO:
|
// TODO:
|
||||||
// The presence of the `getScreenshotWithText()` and `setTimeout()` calls are a hack
|
// The presence of the `getScreenshotWithText()` and `setTimeout()` calls are a hack
|
||||||
// that I am unable to understand the reasoning for - unfortunately they came about
|
// that I am unable to understand the reasoning for - unfortunately they came about
|
||||||
|
@ -354,137 +356,6 @@ export default class extends utils.mixins(CommonMixin) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Ultimately we're going to need to know exactly which parts of the input
|
|
||||||
// box are obscured. This is partly possible using the element's computed
|
|
||||||
// styles, however this isn't comprehensive - think partially obscuring.
|
|
||||||
// So the best solution is to use the same trick as we do for normal text,
|
|
||||||
// except that we can't fill the input box with text, however we can
|
|
||||||
// temporarily change the background to a contrasting colour.
|
|
||||||
_getAllInputBoxes() {
|
|
||||||
let dom_rect, styles, font_rgb;
|
|
||||||
let parsed_input_boxes = {};
|
|
||||||
let raw_input_boxes = document.querySelectorAll(
|
|
||||||
'input, ' +
|
|
||||||
'textarea, ' +
|
|
||||||
'[role="textbox"]'
|
|
||||||
);
|
|
||||||
raw_input_boxes.forEach((i) => {
|
|
||||||
let type;
|
|
||||||
this._ensureBrowshID(i);
|
|
||||||
dom_rect = this._convertDOMRectToAbsoluteCoords(i.getBoundingClientRect());
|
|
||||||
const width = utils.snap(dom_rect.width * this.dimensions.scale_factor.width);
|
|
||||||
const height = utils.snap(dom_rect.height * this.dimensions.scale_factor.height);
|
|
||||||
if (width == 0 || height == 0) { return }
|
|
||||||
if (i.getAttribute('role') == 'textbox') {
|
|
||||||
type = 'textbox';
|
|
||||||
} else {
|
|
||||||
type = i.getAttribute('type');
|
|
||||||
}
|
|
||||||
styles = window.getComputedStyle(i);
|
|
||||||
font_rgb = styles['color'].replace(/[^\d,]/g, '').split(',').map((i) => parseInt(i));
|
|
||||||
if (this._isUnwantedInboxBox(i, styles)) { return }
|
|
||||||
parsed_input_boxes[i.getAttribute('data-browsh-id')] = {
|
|
||||||
id: i.getAttribute('data-browsh-id'),
|
|
||||||
x: utils.snap(dom_rect.left * this.dimensions.scale_factor.width),
|
|
||||||
y: utils.snap(dom_rect.top * this.dimensions.scale_factor.height),
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
tag_name: i.nodeName,
|
|
||||||
type: type,
|
|
||||||
colour: [font_rgb[0], font_rgb[1], font_rgb[2]]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return parsed_input_boxes;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ensureBrowshID(element) {
|
|
||||||
if (element.getAttribute('data-browsh-id') === null) {
|
|
||||||
element.setAttribute('data-browsh-id', utils.uuidv4());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_isUnwantedInboxBox(input_box, styles) {
|
|
||||||
if (styles.display === 'none' || styles.visibility === 'hidden') { return true }
|
|
||||||
if (input_box.getAttribute('aria-hidden') == 'true') { return true }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendRawText() {
|
|
||||||
let payload = {
|
|
||||||
body: this._serialiseRawText()
|
|
||||||
}
|
|
||||||
this.sendMessage(`/raw_text,${JSON.stringify(payload)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendFrame() {
|
|
||||||
this._serialiseFrame();
|
|
||||||
if (this.frame.text.length > 0) {
|
|
||||||
this.sendMessage(`/frame_text,${JSON.stringify(this.frame)}`);
|
|
||||||
} else {
|
|
||||||
this.log("Not sending empty text frame");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
__serialiseFrame() {
|
|
||||||
let cell, index;
|
|
||||||
const top = this.dimensions.frame.sub.top / 2;
|
|
||||||
const left = this.dimensions.frame.sub.left;
|
|
||||||
const bottom = top + (this.dimensions.frame.sub.height / 2);
|
|
||||||
const right = left + this.dimensions.frame.sub.width;
|
|
||||||
this._setupFrameMeta();
|
|
||||||
this._serialiseInputBoxes();
|
|
||||||
for (let y = top; y < bottom; y++) {
|
|
||||||
for (let x = left; x < right; x++) {
|
|
||||||
index = (y * this.dimensions.frame.width) + x;
|
|
||||||
cell = this.tty_grid.cells[index];
|
|
||||||
if (cell === undefined) {
|
|
||||||
this.frame.colours.push(0)
|
|
||||||
this.frame.colours.push(0)
|
|
||||||
this.frame.colours.push(0)
|
|
||||||
this.frame.text.push("")
|
|
||||||
} else {
|
|
||||||
cell.fg_colour.map((c) => this.frame.colours.push(c));
|
|
||||||
this.frame.text.push(cell.rune);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_serialiseRawText() {
|
|
||||||
let cell, index;
|
|
||||||
let raw_text = "";
|
|
||||||
const top = this.dimensions.frame.sub.top / 2;
|
|
||||||
const left = this.dimensions.frame.sub.left;
|
|
||||||
const bottom = top + (this.dimensions.frame.sub.height / 2);
|
|
||||||
const right = left + this.dimensions.frame.sub.width;
|
|
||||||
for (let y = top; y < bottom; y++) {
|
|
||||||
for (let x = left; x < right; x++) {
|
|
||||||
index = (y * this.dimensions.frame.width) + x;
|
|
||||||
cell = this.tty_grid.cells[index];
|
|
||||||
if (cell) {
|
|
||||||
raw_text += cell.rune;
|
|
||||||
} else {
|
|
||||||
raw_text += " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
raw_text += "\n";
|
|
||||||
}
|
|
||||||
return raw_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupFrameMeta() {
|
|
||||||
this.frame = {
|
|
||||||
meta: this.dimensions.getFrameMeta(),
|
|
||||||
text: [],
|
|
||||||
colours: []
|
|
||||||
};
|
|
||||||
this.frame.meta.id = parseInt(this.channel.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
_serialiseInputBoxes() {
|
|
||||||
this.frame.input_boxes = this._getAllInputBoxes();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purely for debugging.
|
// Purely for debugging.
|
||||||
//
|
//
|
||||||
// Draws a red border around all the DOMClientRect nodes.
|
// Draws a red border around all the DOMClientRect nodes.
|
||||||
|
|
Loading…
Reference in a new issue