mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-02-01 11:22:41 +01:00
84d6d277d7
ace.js: removed the role 'application' from innerDocument.body. JAWS do not read any text from the edit lines if this role is set. domline.createDomLine: to give JAWS the ability to read the lines correctly, it is required to set the attribute 'aria-live' to 'assertive'.
278 lines
8.5 KiB
JavaScript
278 lines
8.5 KiB
JavaScript
'use strict';
|
|
|
|
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
|
|
// %APPJET%: import("etherpad.admin.plugins");
|
|
/**
|
|
* Copyright 2009 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
// requires: top
|
|
// requires: plugins
|
|
// requires: undefined
|
|
|
|
const Security = require('./security');
|
|
const hooks = require('./pluginfw/hooks');
|
|
const _ = require('./underscore');
|
|
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
|
|
const noop = () => {};
|
|
|
|
|
|
const domline = {};
|
|
|
|
domline.addToLineClass = (lineClass, cls) => {
|
|
// an "empty span" at any point can be used to add classes to
|
|
// the line, using line:className. otherwise, we ignore
|
|
// the span.
|
|
cls.replace(/\S+/g, (c) => {
|
|
if (c.indexOf('line:') === 0) {
|
|
// add class to line
|
|
lineClass = (lineClass ? `${lineClass} ` : '') + c.substring(5);
|
|
}
|
|
});
|
|
return lineClass;
|
|
};
|
|
|
|
// if "document" is falsy we don't create a DOM node, just
|
|
// an object with innerHTML and className
|
|
domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
|
|
const result = {
|
|
node: null,
|
|
appendSpan: noop,
|
|
prepareForAdd: noop,
|
|
notifyAdded: noop,
|
|
clearSpans: noop,
|
|
finishUpdate: noop,
|
|
lineMarker: 0,
|
|
};
|
|
|
|
const document = optDocument;
|
|
|
|
if (document) {
|
|
result.node = document.createElement('div');
|
|
// JAWS and NVDA screen reader compatibility. Only needed if in a real browser.
|
|
result.node.setAttribute('aria-live', 'assertive');
|
|
} else {
|
|
result.node = {
|
|
innerHTML: '',
|
|
className: '',
|
|
};
|
|
}
|
|
|
|
let html = [];
|
|
let preHtml = '';
|
|
let postHtml = '';
|
|
let curHTML = null;
|
|
|
|
const processSpaces = (s) => domline.processSpaces(s, doesWrap);
|
|
const perTextNodeProcess = (doesWrap ? _.identity : processSpaces);
|
|
const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity);
|
|
let lineClass = 'ace-line';
|
|
|
|
result.appendSpan = (txt, cls) => {
|
|
let processedMarker = false;
|
|
// Handle lineAttributeMarker, if present
|
|
if (cls.indexOf(lineAttributeMarker) >= 0) {
|
|
let listType = /(?:^| )list:(\S+)/.exec(cls);
|
|
const start = /(?:^| )start:(\S+)/.exec(cls);
|
|
|
|
_.map(hooks.callAll('aceDomLinePreProcessLineAttributes', {
|
|
domline,
|
|
cls,
|
|
}), (modifier) => {
|
|
preHtml += modifier.preHtml;
|
|
postHtml += modifier.postHtml;
|
|
processedMarker |= modifier.processedMarker;
|
|
});
|
|
if (listType) {
|
|
listType = listType[1];
|
|
if (listType) {
|
|
if (listType.indexOf('number') < 0) {
|
|
preHtml += `<ul class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
|
|
postHtml = `</li></ul>${postHtml}`;
|
|
} else {
|
|
if (start) { // is it a start of a list with more than one item in?
|
|
if (Number.parseInt(start[1]) === 1) { // if its the first one at this level?
|
|
// Add start class to DIV node
|
|
lineClass = `${lineClass} ` + `list-start-${listType}`;
|
|
}
|
|
preHtml +=
|
|
`<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
|
|
} else {
|
|
// Handles pasted contents into existing lists
|
|
preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
|
|
}
|
|
postHtml += '</li></ol>';
|
|
}
|
|
}
|
|
processedMarker = true;
|
|
}
|
|
_.map(hooks.callAll('aceDomLineProcessLineAttributes', {
|
|
domline,
|
|
cls,
|
|
}), (modifier) => {
|
|
preHtml += modifier.preHtml;
|
|
postHtml += modifier.postHtml;
|
|
processedMarker |= modifier.processedMarker;
|
|
});
|
|
if (processedMarker) {
|
|
result.lineMarker += txt.length;
|
|
return; // don't append any text
|
|
}
|
|
}
|
|
let href = null;
|
|
let simpleTags = null;
|
|
if (cls.indexOf('url') >= 0) {
|
|
cls = cls.replace(/(^| )url:(\S+)/g, (x0, space, url) => {
|
|
href = url;
|
|
return `${space}url`;
|
|
});
|
|
}
|
|
if (cls.indexOf('tag') >= 0) {
|
|
cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => {
|
|
if (!simpleTags) simpleTags = [];
|
|
simpleTags.push(tag.toLowerCase());
|
|
return space + tag;
|
|
});
|
|
}
|
|
|
|
let extraOpenTags = '';
|
|
let extraCloseTags = '';
|
|
|
|
_.map(hooks.callAll('aceCreateDomLine', {
|
|
domline,
|
|
cls,
|
|
}), (modifier) => {
|
|
cls = modifier.cls;
|
|
extraOpenTags += modifier.extraOpenTags;
|
|
extraCloseTags = modifier.extraCloseTags + extraCloseTags;
|
|
});
|
|
|
|
if ((!txt) && cls) {
|
|
lineClass = domline.addToLineClass(lineClass, cls);
|
|
} else if (txt) {
|
|
if (href) {
|
|
const urn_schemes = new RegExp('^(about|geo|mailto|tel):');
|
|
// if the url doesn't include a protocol prefix, assume http
|
|
if (!~href.indexOf('://') && !urn_schemes.test(href)) {
|
|
href = `http://${href}`;
|
|
}
|
|
// Using rel="noreferrer" stops leaking the URL/location of the pad when
|
|
// clicking links in the document.
|
|
// Not all browsers understand this attribute, but it's part of the HTML5 standard.
|
|
// https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer
|
|
// Additionally, we do rel="noopener" to ensure a higher level of referrer security.
|
|
// https://html.spec.whatwg.org/multipage/links.html#link-type-noopener
|
|
// https://mathiasbynens.github.io/rel-noopener/
|
|
// https://github.com/ether/etherpad-lite/pull/3636
|
|
const escapedHref = Security.escapeHTMLAttribute(href);
|
|
extraOpenTags = `${extraOpenTags}<a href="${escapedHref}" rel="noreferrer noopener">`;
|
|
extraCloseTags = `</a>${extraCloseTags}`;
|
|
}
|
|
if (simpleTags) {
|
|
simpleTags.sort();
|
|
extraOpenTags = `${extraOpenTags}<${simpleTags.join('><')}>`;
|
|
simpleTags.reverse();
|
|
extraCloseTags = `</${simpleTags.join('></')}>${extraCloseTags}`;
|
|
}
|
|
html.push(
|
|
'<span class="', Security.escapeHTMLAttribute(cls || ''),
|
|
'">',
|
|
extraOpenTags,
|
|
perTextNodeProcess(Security.escapeHTML(txt)),
|
|
extraCloseTags,
|
|
'</span>');
|
|
}
|
|
};
|
|
result.clearSpans = () => {
|
|
html = [];
|
|
lineClass = 'ace-line';
|
|
result.lineMarker = 0;
|
|
};
|
|
|
|
const writeHTML = () => {
|
|
let newHTML = perHtmlLineProcess(html.join(''));
|
|
if (!newHTML) {
|
|
if ((!document) || (!optBrowser)) {
|
|
newHTML += ' ';
|
|
} else {
|
|
newHTML += '<br/>';
|
|
}
|
|
}
|
|
if (nonEmpty) {
|
|
newHTML = (preHtml || '') + newHTML + (postHtml || '');
|
|
}
|
|
html = preHtml = postHtml = ''; // free memory
|
|
if (newHTML !== curHTML) {
|
|
curHTML = newHTML;
|
|
result.node.innerHTML = curHTML;
|
|
}
|
|
if (lineClass != null) result.node.className = lineClass;
|
|
|
|
hooks.callAll('acePostWriteDomLineHTML', {
|
|
node: result.node,
|
|
});
|
|
};
|
|
result.prepareForAdd = writeHTML;
|
|
result.finishUpdate = writeHTML;
|
|
return result;
|
|
};
|
|
|
|
domline.processSpaces = (s, doesWrap) => {
|
|
if (s.indexOf('<') < 0 && !doesWrap) {
|
|
// short-cut
|
|
return s.replace(/ /g, ' ');
|
|
}
|
|
const parts = [];
|
|
s.replace(/<[^>]*>?| |[^ <]+/g, (m) => {
|
|
parts.push(m);
|
|
});
|
|
if (doesWrap) {
|
|
let endOfLine = true;
|
|
let beforeSpace = false;
|
|
// last space in a run is normal, others are nbsp,
|
|
// end of line is nbsp
|
|
for (let i = parts.length - 1; i >= 0; i--) {
|
|
const p = parts[i];
|
|
if (p === ' ') {
|
|
if (endOfLine || beforeSpace) parts[i] = ' ';
|
|
endOfLine = false;
|
|
beforeSpace = true;
|
|
} else if (p.charAt(0) !== '<') {
|
|
endOfLine = false;
|
|
beforeSpace = false;
|
|
}
|
|
}
|
|
// beginning of line is nbsp
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const p = parts[i];
|
|
if (p === ' ') {
|
|
parts[i] = ' ';
|
|
break;
|
|
} else if (p.charAt(0) !== '<') {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const p = parts[i];
|
|
if (p === ' ') {
|
|
parts[i] = ' ';
|
|
}
|
|
}
|
|
}
|
|
return parts.join('');
|
|
};
|
|
|
|
exports.domline = domline;
|