mirror of
https://github.com/google/blockly.git
synced 2026-01-08 09:30:06 +01:00
chore: Move functions from utils (#5706)
* chore: move functions from utils to more specific files * chore: use new names for utils functions * chore: run clang-format * chore: add deprecation warnings back to utils.js
This commit is contained in:
263
core/utils/parsing.js
Normal file
263
core/utils/parsing.js
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Utility methods related to block and message parsing.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @namespace Blockly.utils.parsing
|
||||
*/
|
||||
goog.module('Blockly.utils.parsing');
|
||||
|
||||
const Msg = goog.require('Blockly.Msg');
|
||||
const colourUtils = goog.require('Blockly.utils.colour');
|
||||
const internalConstants = goog.require('Blockly.internalConstants');
|
||||
const stringUtils = goog.require('Blockly.utils.string');
|
||||
|
||||
|
||||
/**
|
||||
* Internal implementation of the message reference and interpolation token
|
||||
* parsing used by tokenizeInterpolation() and replaceMessageReferences().
|
||||
* @param {string} message Text which might contain string table references and
|
||||
* interpolation tokens.
|
||||
* @param {boolean} parseInterpolationTokens Option to parse numeric
|
||||
* interpolation tokens (%1, %2, ...) when true.
|
||||
* @return {!Array<string|number>} Array of strings and numbers.
|
||||
*/
|
||||
const tokenizeInterpolationInternal = function(
|
||||
message, parseInterpolationTokens) {
|
||||
const tokens = [];
|
||||
const chars = message.split('');
|
||||
chars.push(''); // End marker.
|
||||
// Parse the message with a finite state machine.
|
||||
// 0 - Base case.
|
||||
// 1 - % found.
|
||||
// 2 - Digit found.
|
||||
// 3 - Message ref found.
|
||||
let state = 0;
|
||||
const buffer = [];
|
||||
let number = null;
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const c = chars[i];
|
||||
if (state === 0) {
|
||||
if (c === '%') {
|
||||
const text = buffer.join('');
|
||||
if (text) {
|
||||
tokens.push(text);
|
||||
}
|
||||
buffer.length = 0;
|
||||
state = 1; // Start escape.
|
||||
} else {
|
||||
buffer.push(c); // Regular char.
|
||||
}
|
||||
} else if (state === 1) {
|
||||
if (c === '%') {
|
||||
buffer.push(c); // Escaped %: %%
|
||||
state = 0;
|
||||
} else if (parseInterpolationTokens && '0' <= c && c <= '9') {
|
||||
state = 2;
|
||||
number = c;
|
||||
const text = buffer.join('');
|
||||
if (text) {
|
||||
tokens.push(text);
|
||||
}
|
||||
buffer.length = 0;
|
||||
} else if (c === '{') {
|
||||
state = 3;
|
||||
} else {
|
||||
buffer.push('%', c); // Not recognized. Return as literal.
|
||||
state = 0;
|
||||
}
|
||||
} else if (state === 2) {
|
||||
if ('0' <= c && c <= '9') {
|
||||
number += c; // Multi-digit number.
|
||||
} else {
|
||||
tokens.push(parseInt(number, 10));
|
||||
i--; // Parse this char again.
|
||||
state = 0;
|
||||
}
|
||||
} else if (state === 3) { // String table reference
|
||||
if (c === '') {
|
||||
// Premature end before closing '}'
|
||||
buffer.splice(0, 0, '%{'); // Re-insert leading delimiter
|
||||
i--; // Parse this char again.
|
||||
state = 0; // and parse as string literal.
|
||||
} else if (c !== '}') {
|
||||
buffer.push(c);
|
||||
} else {
|
||||
const rawKey = buffer.join('');
|
||||
if (/[A-Z]\w*/i.test(rawKey)) { // Strict matching
|
||||
// Found a valid string key. Attempt case insensitive match.
|
||||
const keyUpper = rawKey.toUpperCase();
|
||||
|
||||
// BKY_ is the prefix used to namespace the strings used in Blockly
|
||||
// core files and the predefined blocks in ../blocks/.
|
||||
// These strings are defined in ../msgs/ files.
|
||||
const bklyKey = stringUtils.startsWith(keyUpper, 'BKY_') ?
|
||||
keyUpper.substring(4) :
|
||||
null;
|
||||
if (bklyKey && bklyKey in Msg) {
|
||||
const rawValue = Msg[bklyKey];
|
||||
if (typeof rawValue === 'string') {
|
||||
// Attempt to dereference substrings, too, appending to the end.
|
||||
Array.prototype.push.apply(
|
||||
tokens,
|
||||
tokenizeInterpolationInternal(
|
||||
rawValue, parseInterpolationTokens));
|
||||
} else if (parseInterpolationTokens) {
|
||||
// When parsing interpolation tokens, numbers are special
|
||||
// placeholders (%1, %2, etc). Make sure all other values are
|
||||
// strings.
|
||||
tokens.push(String(rawValue));
|
||||
} else {
|
||||
tokens.push(rawValue);
|
||||
}
|
||||
} else {
|
||||
// No entry found in the string table. Pass reference as string.
|
||||
tokens.push('%{' + rawKey + '}');
|
||||
}
|
||||
buffer.length = 0; // Clear the array
|
||||
state = 0;
|
||||
} else {
|
||||
tokens.push('%{' + rawKey + '}');
|
||||
buffer.length = 0;
|
||||
state = 0; // and parse as string literal.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let text = buffer.join('');
|
||||
if (text) {
|
||||
tokens.push(text);
|
||||
}
|
||||
|
||||
// Merge adjacent text tokens into a single string.
|
||||
const mergedTokens = [];
|
||||
buffer.length = 0;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (typeof tokens[i] === 'string') {
|
||||
buffer.push(tokens[i]);
|
||||
} else {
|
||||
text = buffer.join('');
|
||||
if (text) {
|
||||
mergedTokens.push(text);
|
||||
}
|
||||
buffer.length = 0;
|
||||
mergedTokens.push(tokens[i]);
|
||||
}
|
||||
}
|
||||
text = buffer.join('');
|
||||
if (text) {
|
||||
mergedTokens.push(text);
|
||||
}
|
||||
buffer.length = 0;
|
||||
|
||||
return mergedTokens;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a string with any number of interpolation tokens (%1, %2, ...).
|
||||
* It will also replace string table references (e.g., %{bky_my_msg} and
|
||||
* %{BKY_MY_MSG} will both be replaced with the value in
|
||||
* Msg['MY_MSG']). Percentage sign characters '%' may be self-escaped
|
||||
* (e.g., '%%').
|
||||
* @param {string} message Text which might contain string table references and
|
||||
* interpolation tokens.
|
||||
* @return {!Array<string|number>} Array of strings and numbers.
|
||||
* @alias Blockly.parsing.tokenizeInterpolation
|
||||
*/
|
||||
const tokenizeInterpolation = function(message) {
|
||||
return tokenizeInterpolationInternal(message, true);
|
||||
};
|
||||
exports.tokenizeInterpolation = tokenizeInterpolation;
|
||||
|
||||
/**
|
||||
* Replaces string table references in a message, if the message is a string.
|
||||
* For example, "%{bky_my_msg}" and "%{BKY_MY_MSG}" will both be replaced with
|
||||
* the value in Msg['MY_MSG'].
|
||||
* @param {string|?} message Message, which may be a string that contains
|
||||
* string table references.
|
||||
* @return {string} String with message references replaced.
|
||||
* @alias Blockly.parsing.replaceMessageReferences
|
||||
*/
|
||||
const replaceMessageReferences = function(message) {
|
||||
if (typeof message !== 'string') {
|
||||
return message;
|
||||
}
|
||||
const interpolatedResult = tokenizeInterpolationInternal(message, false);
|
||||
// When parseInterpolationTokens === false, interpolatedResult should be at
|
||||
// most length 1.
|
||||
return interpolatedResult.length ? String(interpolatedResult[0]) : '';
|
||||
};
|
||||
exports.replaceMessageReferences = replaceMessageReferences;
|
||||
|
||||
/**
|
||||
* Validates that any %{MSG_KEY} references in the message refer to keys of
|
||||
* the Msg string table.
|
||||
* @param {string} message Text which might contain string table references.
|
||||
* @return {boolean} True if all message references have matching values.
|
||||
* Otherwise, false.
|
||||
* @alias Blockly.parsing.checkMessageReferences
|
||||
*/
|
||||
const checkMessageReferences = function(message) {
|
||||
let validSoFar = true;
|
||||
|
||||
const msgTable = Msg;
|
||||
|
||||
// TODO (#1169): Implement support for other string tables,
|
||||
// prefixes other than BKY_.
|
||||
const m = message.match(/%{BKY_[A-Z]\w*}/ig);
|
||||
for (let i = 0; i < m.length; i++) {
|
||||
const msgKey = m[i].toUpperCase();
|
||||
if (msgTable[msgKey.slice(6, -1)] === undefined) {
|
||||
console.warn('No message string for ' + m[i] + ' in ' + message);
|
||||
validSoFar = false; // Continue to report other errors.
|
||||
}
|
||||
}
|
||||
|
||||
return validSoFar;
|
||||
};
|
||||
exports.checkMessageReferences = checkMessageReferences;
|
||||
|
||||
/**
|
||||
* Parse a block colour from a number or string, as provided in a block
|
||||
* definition.
|
||||
* @param {number|string} colour HSV hue value (0 to 360), #RRGGBB string,
|
||||
* or a message reference string pointing to one of those two values.
|
||||
* @return {{hue: ?number, hex: string}} An object containing the colour as
|
||||
* a #RRGGBB string, and the hue if the input was an HSV hue value.
|
||||
* @throws {Error} If the colour cannot be parsed.
|
||||
* @alias Blockly.parsing.parseBlockColour
|
||||
*/
|
||||
const parseBlockColour = function(colour) {
|
||||
const dereferenced =
|
||||
(typeof colour === 'string') ? replaceMessageReferences(colour) : colour;
|
||||
|
||||
const hue = Number(dereferenced);
|
||||
if (!isNaN(hue) && 0 <= hue && hue <= 360) {
|
||||
return {
|
||||
hue: hue,
|
||||
hex: colourUtils.hsvToHex(
|
||||
hue, internalConstants.HSV_SATURATION,
|
||||
internalConstants.HSV_VALUE * 255),
|
||||
};
|
||||
} else {
|
||||
const hex = colourUtils.parse(dereferenced);
|
||||
if (hex) {
|
||||
// Only store hue if colour is set as a hue.
|
||||
return {hue: null, hex: hex};
|
||||
} else {
|
||||
let errorMsg = 'Invalid colour: "' + dereferenced + '"';
|
||||
if (colour !== dereferenced) {
|
||||
errorMsg += ' (from "' + colour + '")';
|
||||
}
|
||||
throw Error(errorMsg);
|
||||
}
|
||||
}
|
||||
};
|
||||
exports.parseBlockColour = parseBlockColour;
|
||||
245
core/utils/svg_math.js
Normal file
245
core/utils/svg_math.js
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Utility methods for SVG math.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Utility methods realted to figuring out positions of SVG elements.
|
||||
* @namespace Blockly.utils.svgMath
|
||||
*/
|
||||
goog.module('Blockly.utils.svgMath');
|
||||
|
||||
const global = goog.require('Blockly.utils.global');
|
||||
const style = goog.require('Blockly.utils.style');
|
||||
const userAgent = goog.require('Blockly.utils.userAgent');
|
||||
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
|
||||
const {Rect} = goog.require('Blockly.utils.Rect');
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
|
||||
|
||||
|
||||
/**
|
||||
* Static regex to pull the x,y values out of an SVG translate() directive.
|
||||
* Note that Firefox and IE (9,10) return 'translate(12)' instead of
|
||||
* 'translate(12, 0)'.
|
||||
* Note that IE (9,10) returns 'translate(16 8)' instead of 'translate(16, 8)'.
|
||||
* Note that IE has been reported to return scientific notation (0.123456e-42).
|
||||
* @type {!RegExp}
|
||||
*/
|
||||
const XY_REGEX = /translate\(\s*([-+\d.e]+)([ ,]\s*([-+\d.e]+)\s*)?/;
|
||||
|
||||
/**
|
||||
* Static regex to pull the x,y values out of a translate() or translate3d()
|
||||
* style property.
|
||||
* Accounts for same exceptions as XY_REGEX.
|
||||
* @type {!RegExp}
|
||||
*/
|
||||
const XY_STYLE_REGEX =
|
||||
/transform:\s*translate(?:3d)?\(\s*([-+\d.e]+)\s*px([ ,]\s*([-+\d.e]+)\s*px)?/;
|
||||
|
||||
|
||||
/**
|
||||
* Return the coordinates of the top-left corner of this element relative to
|
||||
* its parent. Only for SVG elements and children (e.g. rect, g, path).
|
||||
* @param {!Element} element SVG element to find the coordinates of.
|
||||
* @return {!Coordinate} Object with .x and .y properties.
|
||||
* @alias Blockly.svgMath.getRelativeXY
|
||||
*/
|
||||
const getRelativeXY = function(element) {
|
||||
const xy = new Coordinate(0, 0);
|
||||
// First, check for x and y attributes.
|
||||
const x = element.getAttribute('x');
|
||||
if (x) {
|
||||
xy.x = parseInt(x, 10);
|
||||
}
|
||||
const y = element.getAttribute('y');
|
||||
if (y) {
|
||||
xy.y = parseInt(y, 10);
|
||||
}
|
||||
// Second, check for transform="translate(...)" attribute.
|
||||
const transform = element.getAttribute('transform');
|
||||
const r = transform && transform.match(XY_REGEX);
|
||||
if (r) {
|
||||
xy.x += Number(r[1]);
|
||||
if (r[3]) {
|
||||
xy.y += Number(r[3]);
|
||||
}
|
||||
}
|
||||
|
||||
// Then check for style = transform: translate(...) or translate3d(...)
|
||||
const style = element.getAttribute('style');
|
||||
if (style && style.indexOf('translate') > -1) {
|
||||
const styleComponents = style.match(XY_STYLE_REGEX);
|
||||
if (styleComponents) {
|
||||
xy.x += Number(styleComponents[1]);
|
||||
if (styleComponents[3]) {
|
||||
xy.y += Number(styleComponents[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return xy;
|
||||
};
|
||||
exports.getRelativeXY = getRelativeXY;
|
||||
|
||||
/**
|
||||
* Return the coordinates of the top-left corner of this element relative to
|
||||
* the div Blockly was injected into.
|
||||
* @param {!Element} element SVG element to find the coordinates of. If this is
|
||||
* not a child of the div Blockly was injected into, the behaviour is
|
||||
* undefined.
|
||||
* @return {!Coordinate} Object with .x and .y properties.
|
||||
* @alias Blockly.svgMath.getInjectionDivXY
|
||||
*/
|
||||
const getInjectionDivXY = function(element) {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
while (element) {
|
||||
const xy = getRelativeXY(element);
|
||||
x = x + xy.x;
|
||||
y = y + xy.y;
|
||||
const classes = element.getAttribute('class') || '';
|
||||
if ((' ' + classes + ' ').indexOf(' injectionDiv ') !== -1) {
|
||||
break;
|
||||
}
|
||||
element = /** @type {!Element} */ (element.parentNode);
|
||||
}
|
||||
return new Coordinate(x, y);
|
||||
};
|
||||
exports.getInjectionDivXY = getInjectionDivXY;
|
||||
|
||||
/**
|
||||
* Check if 3D transforms are supported by adding an element
|
||||
* and attempting to set the property.
|
||||
* @return {boolean} True if 3D transforms are supported.
|
||||
* @alias Blockly.svgMath.is3dSupported
|
||||
*/
|
||||
const is3dSupported = function() {
|
||||
if (is3dSupported.cached_ !== undefined) {
|
||||
return is3dSupported.cached_;
|
||||
}
|
||||
// CC-BY-SA Lorenzo Polidori
|
||||
// stackoverflow.com/questions/5661671/detecting-transform-translate3d-support
|
||||
if (!global.globalThis['getComputedStyle']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const el = document.createElement('p');
|
||||
let has3d = 'none';
|
||||
const transforms = {
|
||||
'webkitTransform': '-webkit-transform',
|
||||
'OTransform': '-o-transform',
|
||||
'msTransform': '-ms-transform',
|
||||
'MozTransform': '-moz-transform',
|
||||
'transform': 'transform',
|
||||
};
|
||||
|
||||
// Add it to the body to get the computed style.
|
||||
document.body.insertBefore(el, null);
|
||||
|
||||
for (const t in transforms) {
|
||||
if (el.style[t] !== undefined) {
|
||||
el.style[t] = 'translate3d(1px,1px,1px)';
|
||||
const computedStyle = global.globalThis['getComputedStyle'](el);
|
||||
if (!computedStyle) {
|
||||
// getComputedStyle in Firefox returns null when Blockly is loaded
|
||||
// inside an iframe with display: none. Returning false and not
|
||||
// caching is3dSupported means we try again later. This is most likely
|
||||
// when users are interacting with blocks which should mean Blockly is
|
||||
// visible again.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
||||
document.body.removeChild(el);
|
||||
return false;
|
||||
}
|
||||
has3d = computedStyle.getPropertyValue(transforms[t]);
|
||||
}
|
||||
}
|
||||
document.body.removeChild(el);
|
||||
is3dSupported.cached_ = has3d !== 'none';
|
||||
return is3dSupported.cached_;
|
||||
};
|
||||
exports.is3dSupported = is3dSupported;
|
||||
|
||||
/**
|
||||
* Get the position of the current viewport in window coordinates. This takes
|
||||
* scroll into account.
|
||||
* @return {!Rect} An object containing window width, height, and
|
||||
* scroll position in window coordinates.
|
||||
* @alias Blockly.svgMath.getViewportBBox
|
||||
* @package
|
||||
*/
|
||||
const getViewportBBox = function() {
|
||||
// Pixels, in window coordinates.
|
||||
const scrollOffset = style.getViewportPageOffset();
|
||||
return new Rect(
|
||||
scrollOffset.y, document.documentElement.clientHeight + scrollOffset.y,
|
||||
scrollOffset.x, document.documentElement.clientWidth + scrollOffset.x);
|
||||
};
|
||||
exports.getViewportBBox = getViewportBBox;
|
||||
|
||||
/**
|
||||
* Gets the document scroll distance as a coordinate object.
|
||||
* Copied from Closure's goog.dom.getDocumentScroll.
|
||||
* @return {!Coordinate} Object with values 'x' and 'y'.
|
||||
* @alias Blockly.svgMath.getDocumentScroll
|
||||
*/
|
||||
const getDocumentScroll = function() {
|
||||
const el = document.documentElement;
|
||||
const win = window;
|
||||
if (userAgent.IE && win.pageYOffset !== el.scrollTop) {
|
||||
// The keyboard on IE10 touch devices shifts the page using the pageYOffset
|
||||
// without modifying scrollTop. For this case, we want the body scroll
|
||||
// offsets.
|
||||
return new Coordinate(el.scrollLeft, el.scrollTop);
|
||||
}
|
||||
return new Coordinate(
|
||||
win.pageXOffset || el.scrollLeft, win.pageYOffset || el.scrollTop);
|
||||
};
|
||||
exports.getDocumentScroll = getDocumentScroll;
|
||||
|
||||
/**
|
||||
* Converts screen coordinates to workspace coordinates.
|
||||
* @param {!WorkspaceSvg} ws The workspace to find the coordinates on.
|
||||
* @param {!Coordinate} screenCoordinates The screen coordinates to
|
||||
* be converted to workspace coordinates
|
||||
* @return {!Coordinate} The workspace coordinates.
|
||||
* @alias Blockly.svgMath.screenToWsCoordinates
|
||||
*/
|
||||
const screenToWsCoordinates = function(ws, screenCoordinates) {
|
||||
const screenX = screenCoordinates.x;
|
||||
const screenY = screenCoordinates.y;
|
||||
|
||||
const injectionDiv = ws.getInjectionDiv();
|
||||
// Bounding rect coordinates are in client coordinates, meaning that they
|
||||
// are in pixels relative to the upper left corner of the visible browser
|
||||
// window. These coordinates change when you scroll the browser window.
|
||||
const boundingRect = injectionDiv.getBoundingClientRect();
|
||||
|
||||
// The client coordinates offset by the injection div's upper left corner.
|
||||
const clientOffsetPixels =
|
||||
new Coordinate(screenX - boundingRect.left, screenY - boundingRect.top);
|
||||
|
||||
// The offset in pixels between the main workspace's origin and the upper
|
||||
// left corner of the injection div.
|
||||
const mainOffsetPixels = ws.getOriginOffsetInPixels();
|
||||
|
||||
// The position of the new comment in pixels relative to the origin of the
|
||||
// main workspace.
|
||||
const finalOffsetPixels =
|
||||
Coordinate.difference(clientOffsetPixels, mainOffsetPixels);
|
||||
|
||||
// The position in main workspace coordinates.
|
||||
const finalOffsetMainWs = finalOffsetPixels.scale(1 / ws.scale);
|
||||
return finalOffsetMainWs;
|
||||
};
|
||||
exports.screenToWsCoordinates = screenToWsCoordinates;
|
||||
|
||||
exports.TEST_ONLY = {
|
||||
XY_REGEX,
|
||||
XY_STYLE_REGEX,
|
||||
};
|
||||
Reference in New Issue
Block a user