From 297921fce570bfab413bab4e16a4ae694ecc4f28 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 12 Jul 2017 01:02:51 -0700 Subject: Moved glitch files to their own location ;) --- app/javascript/glitch/util/bio_metadata.js | 380 +++++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 app/javascript/glitch/util/bio_metadata.js (limited to 'app/javascript/glitch/util') diff --git a/app/javascript/glitch/util/bio_metadata.js b/app/javascript/glitch/util/bio_metadata.js new file mode 100644 index 000000000..bdbb1750b --- /dev/null +++ b/app/javascript/glitch/util/bio_metadata.js @@ -0,0 +1,380 @@ +/*********************************************************************\ + + To my lovely code maintainers, + + The syntax recognized by the Mastodon frontend for its bio metadata + feature is a subset of that provided by the YAML 1.2 specification. + In particular, Mastodon recognizes metadata which is provided as an + implicit YAML map, where each key-value pair takes up only a single + line (no multi-line values are permitted). To simplify the level of + processing required, Mastodon metadata frontmatter has been limited + to only allow those characters in the `c-printable` set, as defined + by the YAML 1.2 specification, instead of permitting those from the + `nb-json` characters inside double-quoted strings like YAML proper. + ¶ It is important to note that Mastodon only borrows the *syntax* + of YAML, not its semantics. This is to say, Mastodon won't make any + attempt to interpret the data it receives. `true` will not become a + boolean; `56` will not be interpreted as a number. Rather, each key + and every value will be read as a string, and as a string they will + remain. The order of the pairs is unchanged, and any duplicate keys + are preserved. However, YAML escape sequences will be replaced with + the proper interpretations according to the YAML 1.2 specification. + ¶ The implementation provided below interprets `
` as `\n` and + allows for an open

tag at the beginning of the bio. It replaces + the escaped character entities `'` and `"` with single or + double quotes, respectively, prior to processing. However, no other + escaped characters are replaced, not even those which might have an + impact on the syntax otherwise. These minor allowances are provided + because the Mastodon backend will insert these things automatically + into a bio before sending it through the API, so it is important we + account for them. Aside from this, the YAML frontmatter must be the + very first thing in the bio, leading with three consecutive hyphen- + minues (`---`), and ending with the same or, alternatively, instead + with three periods (`...`). No limits have been set with respect to + the number of characters permitted in the frontmatter, although one + should note that only limited space is provided for them in the UI. + ¶ The regular expression used to check the existence of, and then + process, the YAML frontmatter has been split into a number of small + components in the code below, in the vain hope that it will be much + easier to read and to maintain. I leave it to the future readers of + this code to determine the extent of my successes in this endeavor. + + Sending love + warmth eternal, + - kibigo [@kibi@glitch.social] + +\*********************************************************************/ + +/* CONVENIENCE FUNCTIONS */ + +const unirex = str => new RegExp(str, 'u'); +const rexstr = exp => '(?:' + exp.source + ')'; + +/* CHARACTER CLASSES */ + +const DOCUMENT_START = /^/; +const DOCUMENT_END = /$/; +const ALLOWED_CHAR = // `c-printable` in the YAML 1.2 spec. + /[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]/u; +const WHITE_SPACE = /[ \t]/; +const INDENTATION = / */; // Indentation must be only spaces. +const LINE_BREAK = /\r?\n|\r|/; +const ESCAPE_CHAR = /[0abt\tnvfre "\/\\N_LP]/; +const HEXADECIMAL_CHARS = /[0-9a-fA-F]/; +const INDICATOR = /[-?:,[\]{}&#*!|>'"%@`]/; +const FLOW_CHAR = /[,[\]{}]/; + +/* NEGATED CHARACTER CLASSES */ + +const NOT_WHITE_SPACE = unirex('(?!' + rexstr(WHITE_SPACE) + ')[^]'); +const NOT_LINE_BREAK = unirex('(?!' + rexstr(LINE_BREAK) + ')[^]'); +const NOT_INDICATOR = unirex('(?!' + rexstr(INDICATOR) + ')[^]'); +const NOT_FLOW_CHAR = unirex('(?!' + rexstr(FLOW_CHAR) + ')[^]'); +const NOT_ALLOWED_CHAR = unirex( + '(?!' + rexstr(ALLOWED_CHAR) + ')[^]' +); + +/* BASIC CONSTRUCTS */ + +const ANY_WHITE_SPACE = unirex(rexstr(WHITE_SPACE) + '*'); +const ANY_ALLOWED_CHARS = unirex(rexstr(ALLOWED_CHAR) + '*'); +const NEW_LINE = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) +); +const SOME_NEW_LINES = unirex( + '(?:' + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ')+' +); +const POSSIBLE_STARTS = unirex( + rexstr(DOCUMENT_START) + rexstr(/]*>/) + '?' +); +const POSSIBLE_ENDS = unirex( + rexstr(SOME_NEW_LINES) + '|' + + rexstr(DOCUMENT_END) + '|' + + rexstr(/<\/p>/) +); +const CHARACTER_ESCAPE = unirex( + rexstr(/\\/) + + '(?:' + + rexstr(ESCAPE_CHAR) + '|' + + rexstr(/x/) + rexstr(HEXADECIMAL_CHARS) + '{2}' + '|' + + rexstr(/u/) + rexstr(HEXADECIMAL_CHARS) + '{4}' + '|' + + rexstr(/U/) + rexstr(HEXADECIMAL_CHARS) + '{8}' + + ')' +); +const ESCAPED_CHAR = unirex( + rexstr(/(?!["\\])/) + rexstr(NOT_LINE_BREAK) + '|' + + rexstr(CHARACTER_ESCAPE) +); +const ANY_ESCAPED_CHARS = unirex( + rexstr(ESCAPED_CHAR) + '*' +); +const ESCAPED_APOS = unirex( + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/) +); +const ANY_ESCAPED_APOS = unirex( + rexstr(ESCAPED_APOS) + '*' +); +const FIRST_KEY_CHAR = unirex( + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + rexstr(NOT_INDICATOR) + '|' + + rexstr(/[?:-]/) + + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + '(?=' + rexstr(NOT_FLOW_CHAR) + ')' +); +const FIRST_VALUE_CHAR = unirex( + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + rexstr(NOT_INDICATOR) + '|' + + rexstr(/[?:-]/) + + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + // Flow indicators are allowed in values. +); +const LATER_KEY_CHAR = unirex( + rexstr(WHITE_SPACE) + '|' + + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + '(?=' + rexstr(NOT_FLOW_CHAR) + ')' + + rexstr(/[^:#]#?/) + '|' + + rexstr(/:/) + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' +); +const LATER_VALUE_CHAR = unirex( + rexstr(WHITE_SPACE) + '|' + + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + // Flow indicators are allowed in values. + rexstr(/[^:#]#?/) + '|' + + rexstr(/:/) + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' +); + +/* YAML CONSTRUCTS */ + +const YAML_START = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(/---/) +); +const YAML_END = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(/(?:---|\.\.\.)/) +); +const YAML_LOOKAHEAD = unirex( + '(?=' + + rexstr(YAML_START) + + rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) + + rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) + + ')' +); +const YAML_DOUBLE_QUOTE = unirex( + rexstr(/"/) + rexstr(ANY_ESCAPED_CHARS) + rexstr(/"/) +); +const YAML_SINGLE_QUOTE = unirex( + rexstr(/'/) + rexstr(ANY_ESCAPED_APOS) + rexstr(/'/) +); +const YAML_SIMPLE_KEY = unirex( + rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*' +); +const YAML_SIMPLE_VALUE = unirex( + rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*' +); +const YAML_KEY = unirex( + rexstr(YAML_DOUBLE_QUOTE) + '|' + + rexstr(YAML_SINGLE_QUOTE) + '|' + + rexstr(YAML_SIMPLE_KEY) +); +const YAML_VALUE = unirex( + rexstr(YAML_DOUBLE_QUOTE) + '|' + + rexstr(YAML_SINGLE_QUOTE) + '|' + + rexstr(YAML_SIMPLE_VALUE) +); +const YAML_SEPARATOR = unirex( + rexstr(ANY_WHITE_SPACE) + + ':' + rexstr(WHITE_SPACE) + + rexstr(ANY_WHITE_SPACE) +); +const YAML_LINE = unirex( + '(' + rexstr(YAML_KEY) + ')' + + rexstr(YAML_SEPARATOR) + + '(' + rexstr(YAML_VALUE) + ')' +); + +/* FRONTMATTER REGEX */ + +const YAML_FRONTMATTER = unirex( + rexstr(POSSIBLE_STARTS) + + rexstr(YAML_LOOKAHEAD) + + rexstr(YAML_START) + rexstr(SOME_NEW_LINES) + + '(?:' + + '(' + rexstr(INDENTATION) + ')' + + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) + + '(?:' + + '\\1' + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) + + '){0,4}' + + ')?' + + rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) +); + +/* SEARCHES */ + +const FIND_YAML_LINES = unirex( + rexstr(NEW_LINE) + rexstr(INDENTATION) + rexstr(YAML_LINE) +); + +/* STRING PROCESSING */ + +function processString(str) { + switch (str.charAt(0)) { + case '"': + return str + .substring(1, str.length - 1) + .replace(/\\0/g, '\x00') + .replace(/\\a/g, '\x07') + .replace(/\\b/g, '\x08') + .replace(/\\t/g, '\x09') + .replace(/\\\x09/g, '\x09') + .replace(/\\n/g, '\x0a') + .replace(/\\v/g, '\x0b') + .replace(/\\f/g, '\x0c') + .replace(/\\r/g, '\x0d') + .replace(/\\e/g, '\x1b') + .replace(/\\ /g, '\x20') + .replace(/\\"/g, '\x22') + .replace(/\\\//g, '\x2f') + .replace(/\\\\/g, '\x5c') + .replace(/\\N/g, '\x85') + .replace(/\\_/g, '\xa0') + .replace(/\\L/g, '\u2028') + .replace(/\\P/g, '\u2029') + .replace( + new RegExp( + unirex( + rexstr(/\\x/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{2})' + ), 'gu' + ), (_, n) => String.fromCodePoint('0x' + n) + ) + .replace( + new RegExp( + unirex( + rexstr(/\\u/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{4})' + ), 'gu' + ), (_, n) => String.fromCodePoint('0x' + n) + ) + .replace( + new RegExp( + unirex( + rexstr(/\\U/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{8})' + ), 'gu' + ), (_, n) => String.fromCodePoint('0x' + n) + ); + case '\'': + return str + .substring(1, str.length - 1) + .replace(/''/g, '\''); + default: + return str; + } +} + +/* BIO PROCESSING */ + +export function processBio(content) { + content = content.replace(/"/g, '"').replace(/'/g, '\''); + let result = { + text: content, + metadata: [], + }; + let yaml = content.match(YAML_FRONTMATTER); + if (!yaml) return result; + else yaml = yaml[0]; + let start = content.search(YAML_START); + let end = start + yaml.length - yaml.search(YAML_START); + result.text = content.substr(0, start) + content.substr(end); + let metadata = null; + let query = new RegExp(FIND_YAML_LINES, 'g'); + while ((metadata = query.exec(yaml))) { + result.metadata.push([ + processString(metadata[1]), + processString(metadata[2]), + ]); + } + return result; +} + +/* BIO CREATION */ + +export function createBio(note, data) { + if (!note) note = ''; + let frontmatter = ''; + if ((data && data.length) || note.match(/^\s*---\s+/)) { + if (!data) frontmatter = '---\n...\n'; + else { + frontmatter += '---\n'; + for (let i = 0; i < data.length; i++) { + let key = '' + data[i][0]; + let val = '' + data[i][1]; + + // Key processing + if (key === (key.match(YAML_SIMPLE_KEY) || [])[0]) /* do nothing */; + else if (key.indexOf('\'') === -1 && key === (key.match(ANY_ESCAPED_APOS) || [])[0]) key = '\'' + key + '\''; + else { + key = key + .replace(/\x00/g, '\\0') + .replace(/\x07/g, '\\a') + .replace(/\x08/g, '\\b') + .replace(/\x0a/g, '\\n') + .replace(/\x0b/g, '\\v') + .replace(/\x0c/g, '\\f') + .replace(/\x0d/g, '\\r') + .replace(/\x1b/g, '\\e') + .replace(/\x22/g, '\\"') + .replace(/\x5c/g, '\\\\'); + let badchars = key.match( + new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu') + ) || []; + for (let j = 0; j < badchars.length; j++) { + key = key.replace( + badchars[i], + '\\u' + badchars[i].codePointAt(0).toLocaleString('en', { + useGrouping: false, + minimumIntegerDigits: 4, + }) + ); + } + key = '"' + key + '"'; + } + + // Value processing + if (val === (val.match(YAML_SIMPLE_VALUE) || [])[0]) /* do nothing */; + else if (val.indexOf('\'') === -1 && val === (val.match(ANY_ESCAPED_APOS) || [])[0]) val = '\'' + val + '\''; + else { + val = val + .replace(/\x00/g, '\\0') + .replace(/\x07/g, '\\a') + .replace(/\x08/g, '\\b') + .replace(/\x0a/g, '\\n') + .replace(/\x0b/g, '\\v') + .replace(/\x0c/g, '\\f') + .replace(/\x0d/g, '\\r') + .replace(/\x1b/g, '\\e') + .replace(/\x22/g, '\\"') + .replace(/\x5c/g, '\\\\'); + let badchars = val.match( + new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu') + ) || []; + for (let j = 0; j < badchars.length; j++) { + val = val.replace( + badchars[i], + '\\u' + badchars[i].codePointAt(0).toLocaleString('en', { + useGrouping: false, + minimumIntegerDigits: 4, + }) + ); + } + val = '"' + val + '"'; + } + + frontmatter += key + ': ' + val + '\n'; + } + frontmatter += '...\n'; + } + } + return frontmatter + note; +} -- cgit From 35fda84ba830415a575b5f99f7405353ab8d3c93 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Thu, 13 Jul 2017 03:26:08 -0700 Subject: Documentation pt. I --- app/javascript/glitch/actions/local_settings.js | 73 ++++++++++++++++++++++ app/javascript/glitch/reducers/local_settings.js | 79 ++++++++++++++++++++++++ app/javascript/glitch/util/bio_metadata.js | 30 +++++++++ 3 files changed, 182 insertions(+) (limited to 'app/javascript/glitch/util') diff --git a/app/javascript/glitch/actions/local_settings.js b/app/javascript/glitch/actions/local_settings.js index 18e0c245c..479b5841d 100644 --- a/app/javascript/glitch/actions/local_settings.js +++ b/app/javascript/glitch/actions/local_settings.js @@ -1,5 +1,60 @@ +/* + +`actions/local_settings` +======================== + +> For more information on the contents of this file, please contact: +> +> - kibigo! [@kibi@glitch.social] + +This file provides our Redux actions related to local settings. It +consists of the following: + + - __`changesLocalSetting(key, value)` :__ + Changes the local setting with the given `key` to the given + `value`. `key` **MUST** be an array of strings, as required by + `Immutable.Map.prototype.getIn()`. + + - __`saveLocalSettings()` :__ + Saves the local settings to `localStorage` as a JSON object. We + shouldn't ever need to call this ourselves. + +*/ + + /* * * * */ + +/* + +Constants +--------- + +We provide the following constants: + + - __`LOCAL_SETTING_CHANGE` :__ + This string constant is used to dispatch a setting change to our + reducer in `reducers/local_settings`, where the setting is + actually changed. + +*/ + export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; + /* * * * */ + +/* + +`changeLocalSetting(key, value)` +-------------------------------- + +Changes the local setting with the given `key` to the given `value`. +`key` **MUST** be an array of strings, as required by +`Immutable.Map.prototype.getIn()`. + +To accomplish this, we just dispatch a `LOCAL_SETTING_CHANGE` to our +reducer in `reducers/local_settings`. + +*/ + export function changeLocalSetting(key, value) { return dispatch => { dispatch({ @@ -12,6 +67,24 @@ export function changeLocalSetting(key, value) { }; }; + /* * * * */ + +/* + +`saveLocalSettings()` +--------------------- + +Saves the local settings to `localStorage` as a JSON object. +`changeLocalSetting()` calls this whenever it changes a setting. We +shouldn't ever need to call this ourselves. + +> __TODO :__ +> Right now `saveLocalSettings()` doesn't keep track of which user +> is currently signed in, but it might be better to give each user +> their *own* local settings. + +*/ + export function saveLocalSettings() { return (_, getState) => { const localSettings = getState().get('local_settings').toJS(); diff --git a/app/javascript/glitch/reducers/local_settings.js b/app/javascript/glitch/reducers/local_settings.js index db99f2c46..79ff96307 100644 --- a/app/javascript/glitch/reducers/local_settings.js +++ b/app/javascript/glitch/reducers/local_settings.js @@ -1,3 +1,32 @@ +/* + +`reducers/local_settings` +======================== + +> For more information on the contents of this file, please contact: +> +> - kibigo! [@kibi@glitch.social] + +This file provides our Redux reducers related to local settings. The +associated actions are: + + - __`STORE_HYDRATE` :__ + Used to hydrate the store with its initial values. + + - __`LOCAL_SETTING_CHANGE` :__ + Used to change the value of a local setting in the store. + +*/ + + /* * * * */ + +/* + +Imports +------- + +*/ + // Package imports // import Immutable from 'immutable'; @@ -7,6 +36,18 @@ import { STORE_HYDRATE } from '../../mastodon/actions/store'; // Our imports // import { LOCAL_SETTING_CHANGE } from '../actions/local_settings'; + /* * * * */ + +/* + +initialState +------------ + +You can see the default values for all of our local settings here. +These are only used if no previously-saved values exist. + +*/ + const initialState = Immutable.fromJS({ layout : 'auto', stretch : true, @@ -30,8 +71,46 @@ const initialState = Immutable.fromJS({ }, }); + /* * * * */ + +/* + +Helper functions +---------------- + +### `hydrate(state, localSettings)` + +`hydrate()` is used to hydrate the `local_settings` part of our store +with its initial values. The `state` will probably just be the +`initialState`, and the `localSettings` should be whatever we pulled +from `localStorage`. + +*/ + const hydrate = (state, localSettings) => state.mergeDeep(localSettings); + /* * * * */ + +/* + +`localSettings(state = initialState, action)` +--------------------------------------------- + +This function holds our actual reducer. + +If our action is `STORE_HYDRATE`, then we call `hydrate()` with the +`local_settings` property of the provided `action.state`. + +If our action is `LOCAL_SETTING_CHANGE`, then we set `action.key` in +our state to the provided `action.value`. Note that `action.key` MUST +be an array, since we use `setIn()`. + +> __Note :__ +> We call this function `localSettings`, but its associated object +> in the store is `local_settings`. + +*/ + export default function localSettings(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: diff --git a/app/javascript/glitch/util/bio_metadata.js b/app/javascript/glitch/util/bio_metadata.js index bdbb1750b..c5e87f356 100644 --- a/app/javascript/glitch/util/bio_metadata.js +++ b/app/javascript/glitch/util/bio_metadata.js @@ -1,3 +1,33 @@ +/* + +`util/bio_metadata` +======================== + +> For more information on the contents of this file, please contact: +> +> - kibigo! [@kibi@glitch.social] + +This file provides two functions for dealing with bio metadata. The +functions are: + + - __`processBio(content)` :__ + Processes `content` to extract any frontmatter. The returned + object has two properties: `text`, which contains the text of + `content` sans-frontmatter, and `metadata`, which is an array + of key-value pairs (in two-element array format). If no + frontmatter was provided in `content`, then `metadata` will be + an empty array. + + - __`createBio(note, data)` :__ + Reverses the process in `processBio()`; takes a `note` and an + array of two-element arrays (which should give keys and values) + and outputs a string containing a well-formed bio with + frontmatter. + +*/ + + /* * * * */ + /*********************************************************************\ To my lovely code maintainers, -- cgit From d0aad1ac854eaa53f9b7d38cc8dd90e289790629 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Fri, 14 Jul 2017 11:13:02 -0700 Subject: Documentation and cleanup --- app/javascript/glitch/actions/local_settings.js | 18 +- app/javascript/glitch/components/account/header.js | 163 ++++++++++++-- .../compose/advanced_options/container.js | 44 ++++ .../components/compose/advanced_options/index.js | 236 +++++++++++++++------ .../components/compose/advanced_options/toggle.js | 103 +++++++++ .../glitch/components/notification/container.js | 47 +++- .../glitch/components/notification/follow.js | 171 +++++++++++++++ .../components/notification/follow_notification.js | 78 ------- .../glitch/components/notification/index.js | 6 +- app/javascript/glitch/reducers/local_settings.js | 24 +-- app/javascript/glitch/util/bio_metadata.js | 4 +- 11 files changed, 705 insertions(+), 189 deletions(-) create mode 100644 app/javascript/glitch/components/compose/advanced_options/toggle.js create mode 100644 app/javascript/glitch/components/notification/follow.js delete mode 100644 app/javascript/glitch/components/notification/follow_notification.js (limited to 'app/javascript/glitch/util') diff --git a/app/javascript/glitch/actions/local_settings.js b/app/javascript/glitch/actions/local_settings.js index 479b5841d..93c5a9a17 100644 --- a/app/javascript/glitch/actions/local_settings.js +++ b/app/javascript/glitch/actions/local_settings.js @@ -21,12 +21,12 @@ consists of the following: */ - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -Constants ---------- +Constants: +---------- We provide the following constants: @@ -39,12 +39,12 @@ We provide the following constants: export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -`changeLocalSetting(key, value)` --------------------------------- +`changeLocalSetting(key, value)`: +--------------------------------- Changes the local setting with the given `key` to the given `value`. `key` **MUST** be an array of strings, as required by @@ -67,12 +67,12 @@ export function changeLocalSetting(key, value) { }; }; - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -`saveLocalSettings()` ---------------------- +`saveLocalSettings()`: +---------------------- Saves the local settings to `localStorage` as a JSON object. `changeLocalSetting()` calls this whenever it changes a setting. We diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index e2d961240..b79140c02 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -1,3 +1,45 @@ +/* + +`` +================= + +> For more information on the contents of this file, please contact: +> +> - kibigo! [@kibi@glitch.social] + +Original file by @gargron@mastodon.social et al as part of +tootsuite/mastodon. We've expanded it in order to handle user bio +frontmatter. + +The `` component provides the header for account +timelines. It is a fairly simple component which mostly just consists +of a `render()` method. + +__Props:__ + + - __`account` (`ImmutablePropTypes.map`) :__ + The account to render a header for. + + - __`me` (`PropTypes.number.isRequired`) :__ + The id of the currently-signed-in account. + + - __`onFollow` (`PropTypes.func.isRequired`) :__ + The function to call when the user clicks the "follow" button. + + - __`intl` (`PropTypes.object.isRequired`) :__ + Our internationalization object, inserted by `@injectIntl`. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + // Package imports // import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -14,25 +56,63 @@ import Avatar from '../../../mastodon/components/avatar'; // Our imports // import { processBio } from '../../util/bio_metadata'; +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Inital setup: +------------- + +The `messages` constant is used to define any messages that we need +from inside props. In our case, these are the `unfollow`, `follow`, and +`requested` messages used in the `title` of our buttons. + +*/ + const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, }); +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Implementation: +--------------- + +*/ + @injectIntl -export default class Header extends ImmutablePureComponent { +export default class AccountHeader extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, - me: PropTypes.number.isRequired, - onFollow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, + account : ImmutablePropTypes.map, + me : PropTypes.number.isRequired, + onFollow : PropTypes.func.isRequired, + intl : PropTypes.object.isRequired, }; +/* + +### `render()` + +The `render()` function is used to render our component. + +*/ + render () { const { account, me, intl } = this.props; +/* + +If no `account` is provided, then we can't render a header. Otherwise, +we get the `displayName` for the account, if available. If it's blank, +then we set the `displayName` to just be the `username` of the account. + +*/ + if (!account) { return null; } @@ -40,17 +120,30 @@ export default class Header extends ImmutablePureComponent { let displayName = account.get('display_name'); let info = ''; let actionBtn = ''; - let lockedIcon = ''; + let following = false; if (displayName.length === 0) { displayName = account.get('username'); } - if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info = ; - } +/* + +Next, we handle the account relationships. If the account follows the +user, then we add an `info` message. If the user has requested a +follow, then we disable the `actionBtn` and display an hourglass. +Otherwise, if the account isn't blocked, we set the `actionBtn` to the +appropriate icon. + +*/ if (me !== account.get('id')) { + if (account.getIn(['relationship', 'followed_by'])) { + info = ( + + + + ); + } if (account.getIn(['relationship', 'requested'])) { actionBtn = (

@@ -58,30 +151,64 @@ export default class Header extends ImmutablePureComponent {
); } else if (!account.getIn(['relationship', 'blocking'])) { + following = account.getIn(['relationship', 'following']); actionBtn = (
- +
); } } - if (account.get('locked')) { - lockedIcon = ; - } +/* - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; +`displayNameHTML` processes the `displayName` and prepares it for +insertion into the document. Meanwhile, we extract the `text` and +`metadata` from our account's `note` using `processBio()`. + +*/ + + const displayNameHTML = { + __html : emojify(escapeTextContentForBrowser(displayName)), + }; const { text, metadata } = processBio(account.get('note')); +/* + +Here, we render our component using all the things we've defined above. + +*/ + return (
-
+
- - + + + + - @{account.get('acct')} {lockedIcon} + + @{account.get('acct')} + {account.get('locked') ? : null} +
{info} diff --git a/app/javascript/glitch/components/compose/advanced_options/container.js b/app/javascript/glitch/components/compose/advanced_options/container.js index 10804454a..160f22737 100644 --- a/app/javascript/glitch/components/compose/advanced_options/container.js +++ b/app/javascript/glitch/components/compose/advanced_options/container.js @@ -1,3 +1,21 @@ +/* + +`` +=================================== + +This container connects `` to the Redux store. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + // Package imports // import { connect } from 'react-redux'; @@ -7,10 +25,36 @@ import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compos // Our imports // import ComposeAdvancedOptions from '.'; +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +State mapping: +-------------- + +The `mapStateToProps()` function maps various state properties to the +props of our component. The only property we care about is +`compose.advanced_options`. + +*/ + const mapStateToProps = state => ({ values: state.getIn(['compose', 'advanced_options']), }); +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Dispatch mapping: +----------------- + +The `mapDispatchToProps()` function maps dispatches to our store to the +various props of our component. We just need to provide a dispatch for +when an advanced option toggle changes. + +*/ + const mapDispatchToProps = dispatch => ({ onChange (option) { diff --git a/app/javascript/glitch/components/compose/advanced_options/index.js b/app/javascript/glitch/components/compose/advanced_options/index.js index dabf66095..b745d1cdf 100644 --- a/app/javascript/glitch/components/compose/advanced_options/index.js +++ b/app/javascript/glitch/components/compose/advanced_options/index.js @@ -1,137 +1,241 @@ +/* + +`` +========================== + +> For more information on the contents of this file, please contact: +> +> - surinna [@srn@dev.glitch.social] + +This adds an advanced options dropdown to the toot compose box, for +toggles that don't necessarily fit elsewhere. + +__Props:__ + + - __`values` (`ImmutablePropTypes.contains(…).isRequired`) :__ + An Immutable map with the following values: + + - __`do_not_federate` (`PropTypes.bool.isRequired`) :__ + Specifies whether or not to federate the status. + + - __`onChange` (`PropTypes.func.isRequired`) :__ + The function to call when a toggle is changed. We pass this from + our container to the toggle. + + - __`intl` (`PropTypes.object.isRequired`) :__ + Our internationalization object, inserted by `@injectIntl`. + +__State:__ + + - __`open` :__ + This tells whether the dropdown is currently open or closed. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + // Package imports // import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; import { injectIntl, defineMessages } from 'react-intl'; // Mastodon imports // import IconButton from '../../../../mastodon/components/icon_button'; +// Our imports // +import ComposeAdvancedOptionsToggle from './toggle'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Inital setup: +------------- + +The `messages` constant is used to define any messages that we need +from inside props. These are the various titles and labels on our +toggles. + +`iconStyle` styles the icon used for the dropdown button. + +*/ + const messages = defineMessages({ - local_only_short: { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, - local_only_long: { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, - advanced_options_icon_title: { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, + local_only_short : + { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, + local_only_long : + { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, + advanced_options_icon_title : + { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, }); const iconStyle = { - height: null, - lineHeight: '27px', + height : null, + lineHeight : '27px', }; -class AdvancedOptionToggle extends React.PureComponent { - - static propTypes = { - onChange: PropTypes.func.isRequired, - active: PropTypes.bool.isRequired, - name: PropTypes.string.isRequired, - shortText: PropTypes.string.isRequired, - longText: PropTypes.string.isRequired, - } - - onToggle = () => { - this.props.onChange(this.props.name); - } +/* - render() { - const { active, shortText, longText } = this.props; - return ( -
-
- -
-
- {shortText} - {longText} -
-
- ); - } +Implementation: +--------------- -} +*/ @injectIntl export default class ComposeAdvancedOptions extends React.PureComponent { static propTypes = { - values: ImmutablePropTypes.contains({ - do_not_federate: PropTypes.bool.isRequired, + values : ImmutablePropTypes.contains({ + do_not_federate : PropTypes.bool.isRequired, }).isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, + onChange : PropTypes.func.isRequired, + intl : PropTypes.object.isRequired, }; + state = { + open: false, + }; + +/* + +### `onToggleDropdown()` + +This function toggles the opening and closing of the advanced options +dropdown. + +*/ + onToggleDropdown = () => { this.setState({ open: !this.state.open }); }; +/* + +### `onGlobalClick(e)` + +This function closes the advanced options dropdown if you click +anywhere else on the screen. + +*/ + onGlobalClick = (e) => { if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { this.setState({ open: false }); } } +/* + +### `componentDidMount()`, `componentWillUnmount()` + +This function closes the advanced options dropdown if you click +anywhere else on the screen. + +*/ + componentDidMount () { window.addEventListener('click', this.onGlobalClick); window.addEventListener('touchstart', this.onGlobalClick); } - componentWillUnmount () { window.removeEventListener('click', this.onGlobalClick); window.removeEventListener('touchstart', this.onGlobalClick); } - state = { - open: false, - }; +/* - handleClick = (e) => { - const option = e.currentTarget.getAttribute('data-index'); - e.preventDefault(); - this.props.onChange(option); - } +### `setRef(c)` + +`setRef()` stores a reference to the dropdown's `
in `this.node`. + +*/ setRef = (c) => { this.node = c; } +/* + +### `render()` + +`render()` actually puts our component on the screen. + +*/ + render () { const { open } = this.state; const { intl, values } = this.props; +/* + +The `options` array provides all of the available advanced options +alongside their icon, text, and name. + +*/ const options = [ - { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, key: 'do_not_federate' }, + { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' }, ]; +/* + +`anyEnabled` tells us if any of our advanced options have been enabled. + +*/ + const anyEnabled = values.some((enabled) => enabled); + +/* + +`optionElems` takes our `options` and creates +``s out of them. We use the `name` of the +toggle as its `key` so that React can keep track of it. + +*/ + const optionElems = options.map((option) => { return ( - ); }); - return (
-
- -
-
- {optionElems} +/* + +Finally, we can render our component. + +*/ + + return ( +
+
+ +
+
+ {optionElems} +
-
); + ); } } diff --git a/app/javascript/glitch/components/compose/advanced_options/toggle.js b/app/javascript/glitch/components/compose/advanced_options/toggle.js new file mode 100644 index 000000000..d6907472a --- /dev/null +++ b/app/javascript/glitch/components/compose/advanced_options/toggle.js @@ -0,0 +1,103 @@ +/* + +`` +================================ + +> For more information on the contents of this file, please contact: +> +> - surinna [@srn@dev.glitch.social] + +This creates the toggle used by ``. + +__Props:__ + + - __`onChange` (`PropTypes.func`) :__ + This provides the function to call when the toggle is + (de-?)activated. + + - __`active` (`PropTypes.bool`) :__ + This prop controls whether the toggle is currently active or not. + + - __`name` (`PropTypes.string`) :__ + This identifies the toggle, and is sent to `onChange()` when it is + called. + + - __`shortText` (`PropTypes.string`) :__ + This is a short string used as the title of the toggle. + + - __`longText` (`PropTypes.string`) :__ + This is a longer string used as a subtitle for the toggle. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + +// Package imports // +import React from 'react'; +import PropTypes from 'prop-types'; +import Toggle from 'react-toggle'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Implementation: +--------------- + +*/ + +export default class ComposeAdvancedOptionsToggle extends React.PureComponent { + + static propTypes = { + onChange: PropTypes.func.isRequired, + active: PropTypes.bool.isRequired, + name: PropTypes.string.isRequired, + shortText: PropTypes.string.isRequired, + longText: PropTypes.string.isRequired, + } + +/* + +### `onToggle()` + +The `onToggle()` function simply calls the `onChange()` prop with the +toggle's `name`. + +*/ + + onToggle = () => { + this.props.onChange(this.props.name); + } + +/* + +### `render()` + +The `render()` function is used to render our component. We just render +a `` and place next to it our text. + +*/ + + render() { + const { active, shortText, longText } = this.props; + return ( +
+
+ +
+
+ {shortText} + {longText} +
+
+ ); + } + +} diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js index 60303537d..bed086172 100644 --- a/app/javascript/glitch/components/notification/container.js +++ b/app/javascript/glitch/components/notification/container.js @@ -1,3 +1,21 @@ +/* + +`` +========================= + +This container connects ``s to the Redux store. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + // Package imports // import { connect } from 'react-redux'; @@ -8,6 +26,20 @@ import { makeGetNotification } from '../../../mastodon/selectors'; import Notification from '.'; import { deleteNotification } from '../../../mastodon/actions/notifications'; +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +State mapping: +-------------- + +The `mapStateToProps()` function maps various state properties to the +props of our component. We wrap this in `makeMapStateToProps()` so that +we only have to call `makeGetNotification()` once instead of every +time. + +*/ + const makeMapStateToProps = () => { const getNotification = makeGetNotification(); @@ -19,7 +51,20 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -const mapDispatchToProps = (dispatch) => ({ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Dispatch mapping: +----------------- + +The `mapDispatchToProps()` function maps dispatches to our store to the +various props of our component. We only need to provide a dispatch for +deleting notifications. + +*/ + +const mapDispatchToProps = dispatch => ({ onDeleteNotification (id) { dispatch(deleteNotification(id)); }, diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js new file mode 100644 index 000000000..26396478b --- /dev/null +++ b/app/javascript/glitch/components/notification/follow.js @@ -0,0 +1,171 @@ +/* + +`` +====================== + +This component renders a follow notification. + +__Props:__ + + - __`id` (`PropTypes.number.isRequired`) :__ + This is the id of the notification. + + - __`onDeleteNotification` (`PropTypes.func.isRequired`) :__ + The function to call when a notification should be + dismissed/deleted. + + - __`account` (`PropTypes.object.isRequired`) :__ + The account associated with the follow notification, ie the account + which followed the user. + + - __`intl` (`PropTypes.object.isRequired`) :__ + Our internationalization object, inserted by `@injectIntl`. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + +// Package imports // +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import escapeTextContentForBrowser from 'escape-html'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// Mastodon imports // +import emojify from '../../../mastodon/emoji'; +import Permalink from '../../../mastodon/components/permalink'; +import AccountContainer from '../../../mastodon/containers/account_container'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Inital setup: +------------- + +The `messages` constant is used to define any messages that we need +from inside props. + +*/ + +const messages = defineMessages({ + deleteNotification : + { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, +}); + +/* + +Implementation: +--------------- + +*/ + +@injectIntl +export default class NotificationFollow extends ImmutablePureComponent { + + static propTypes = { + id : PropTypes.number.isRequired, + onDeleteNotification : PropTypes.func.isRequired, + account : ImmutablePropTypes.map.isRequired, + intl : PropTypes.object.isRequired, + }; + +/* + +### `handleNotificationDeleteClick()` + +This function just calls our `onDeleteNotification()` prop with the +notification's `id`. + +*/ + + handleNotificationDeleteClick = () => { + this.props.onDeleteNotification(this.props.id); + } + +/* + +### `render()` + +This actually renders the component. + +*/ + + render () { + const { account, intl } = this.props; + +/* + +`dismiss` creates the notification dismissal button. Its title is given +by `dismissTitle`. + +*/ + + const dismissTitle = intl.formatMessage(messages.deleteNotification); + const dismiss = ( + + ); + +/* + +`link` is a container for the account's `displayName`, which links to +the account timeline using a ``. + +*/ + + const displayName = account.get('display_name') || account.get('username'); + const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const link = ( + + ); + +/* + +We can now render our component. + +*/ + + return ( +
+
+
+ +
+ + + + {dismiss} +
+ + +
+ ); + } + +} diff --git a/app/javascript/glitch/components/notification/follow_notification.js b/app/javascript/glitch/components/notification/follow_notification.js deleted file mode 100644 index 7cabd91f6..000000000 --- a/app/javascript/glitch/components/notification/follow_notification.js +++ /dev/null @@ -1,78 +0,0 @@ -// Package imports // -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import escapeTextContentForBrowser from 'escape-html'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Mastodon imports // -import emojify from '../../../mastodon/emoji'; -import Permalink from '../../../mastodon/components/permalink'; -import AccountContainer from '../../../mastodon/containers/account_container'; - -const messages = defineMessages({ - deleteNotification: { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, -}); - - -@injectIntl -export default class FollowNotification extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - notificationId: PropTypes.number.isRequired, - onDeleteNotification: PropTypes.func.isRequired, - account: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, - }; - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - updateOnProps = [ - 'account', - ] - - handleNotificationDeleteClick = () => { - this.props.onDeleteNotification(this.props.notificationId); - } - - render () { - const { account, intl } = this.props; - - const dismissTitle = intl.formatMessage(messages.deleteNotification); - const dismiss = ( - - ); - - const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = ; - return ( -
-
-
- -
- - - - {dismiss} -
- - -
- ); - } - -} diff --git a/app/javascript/glitch/components/notification/index.js b/app/javascript/glitch/components/notification/index.js index 0cdc03cbe..556d5aea8 100644 --- a/app/javascript/glitch/components/notification/index.js +++ b/app/javascript/glitch/components/notification/index.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; // Our imports // import StatusContainer from '../status/container'; -import FollowNotification from './follow_notification'; +import NotificationFollow from './follow'; export default class Notification extends ImmutablePureComponent { @@ -20,8 +20,8 @@ export default class Notification extends ImmutablePureComponent { renderFollow (notification) { return ( - diff --git a/app/javascript/glitch/reducers/local_settings.js b/app/javascript/glitch/reducers/local_settings.js index 776dcead7..35a8e065b 100644 --- a/app/javascript/glitch/reducers/local_settings.js +++ b/app/javascript/glitch/reducers/local_settings.js @@ -18,12 +18,12 @@ associated actions are: */ - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -Imports -------- +Imports: +-------- */ @@ -36,12 +36,12 @@ import { STORE_HYDRATE } from '../../mastodon/actions/store'; // Our imports // import { LOCAL_SETTING_CHANGE } from '../actions/local_settings'; - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -initialState ------------- +initialState: +------------- You can see the default values for all of our local settings here. These are only used if no previously-saved values exist. @@ -71,12 +71,12 @@ const initialState = ImmutableMap({ }), }); - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -Helper functions ----------------- +Helper functions: +----------------- ### `hydrate(state, localSettings)` @@ -89,12 +89,12 @@ from `localStorage`. const hydrate = (state, localSettings) => state.mergeDeep(localSettings); - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -`localSettings(state = initialState, action)` ---------------------------------------------- +`localSettings(state = initialState, action)`: +---------------------------------------------- This function holds our actual reducer. diff --git a/app/javascript/glitch/util/bio_metadata.js b/app/javascript/glitch/util/bio_metadata.js index c5e87f356..0c8195e9d 100644 --- a/app/javascript/glitch/util/bio_metadata.js +++ b/app/javascript/glitch/util/bio_metadata.js @@ -1,7 +1,7 @@ /* `util/bio_metadata` -======================== +=================== > For more information on the contents of this file, please contact: > @@ -26,7 +26,7 @@ functions are: */ - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /*********************************************************************\ -- cgit From 70c5eccc12558f27469ce09eee45762517dfe5fc Mon Sep 17 00:00:00 2001 From: kibigo! Date: Sun, 6 Aug 2017 15:08:13 -0700 Subject: Compatibility regex for user profiles --- app/javascript/glitch/util/bio_metadata.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'app/javascript/glitch/util') diff --git a/app/javascript/glitch/util/bio_metadata.js b/app/javascript/glitch/util/bio_metadata.js index 0c8195e9d..eb3ad01fe 100644 --- a/app/javascript/glitch/util/bio_metadata.js +++ b/app/javascript/glitch/util/bio_metadata.js @@ -74,17 +74,27 @@ functions are: \*********************************************************************/ +/* "u" FLAG COMPATABILITY */ + +let compat_mode = false; +try { + new RegExp('.', 'u'); +} catch (e) { + compat_mode = true; +} + /* CONVENIENCE FUNCTIONS */ -const unirex = str => new RegExp(str, 'u'); +const unirex = str => compat_mode ? new RegExp(str) : new RegExp(str, 'u'); const rexstr = exp => '(?:' + exp.source + ')'; /* CHARACTER CLASSES */ const DOCUMENT_START = /^/; const DOCUMENT_END = /$/; -const ALLOWED_CHAR = // `c-printable` in the YAML 1.2 spec. - /[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]/u; +const ALLOWED_CHAR = unirex( // `c-printable` in the YAML 1.2 spec. + compat_mode ? '[\t\n\r\x20-\x7e\x85\xa0-\ufffd]' : '[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]' + ); const WHITE_SPACE = /[ \t]/; const INDENTATION = / */; // Indentation must be only spaces. const LINE_BREAK = /\r?\n|\r|/; -- cgit From 21bafc65555d9ce82bf9e3c7841ba69564cc70fd Mon Sep 17 00:00:00 2001 From: kibigo! Date: Thu, 19 Oct 2017 16:11:53 -0700 Subject: Updates to bio metadata script --- app/javascript/glitch/util/bio_metadata.js | 225 +++++++++-------------------- 1 file changed, 68 insertions(+), 157 deletions(-) (limited to 'app/javascript/glitch/util') diff --git a/app/javascript/glitch/util/bio_metadata.js b/app/javascript/glitch/util/bio_metadata.js index eb3ad01fe..599ec20e2 100644 --- a/app/javascript/glitch/util/bio_metadata.js +++ b/app/javascript/glitch/util/bio_metadata.js @@ -69,6 +69,10 @@ functions are: easier to read and to maintain. I leave it to the future readers of this code to determine the extent of my successes in this endeavor. + UPDATE 19 Oct 2017: We no longer allow character escapes inside our + double-quoted strings for ease of processing. We now internally use + the name "ƔAML" in our code to clarify that this is Not Quite YAML. + Sending love + warmth eternal, - kibigo [@kibi@glitch.social] @@ -96,10 +100,7 @@ const ALLOWED_CHAR = unirex( // `c-printable` in the YAML 1.2 spec. compat_mode ? '[\t\n\r\x20-\x7e\x85\xa0-\ufffd]' : '[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]' ); const WHITE_SPACE = /[ \t]/; -const INDENTATION = / */; // Indentation must be only spaces. const LINE_BREAK = /\r?\n|\r|/; -const ESCAPE_CHAR = /[0abt\tnvfre "\/\\N_LP]/; -const HEXADECIMAL_CHARS = /[0-9a-fA-F]/; const INDICATOR = /[-?:,[\]{}&#*!|>'"%@`]/; const FLOW_CHAR = /[,[\]{}]/; @@ -121,7 +122,7 @@ const NEW_LINE = unirex( rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) ); const SOME_NEW_LINES = unirex( - '(?:' + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ')+' + '(?:' + rexstr(NEW_LINE) + ')+' ); const POSSIBLE_STARTS = unirex( rexstr(DOCUMENT_START) + rexstr(/]*>/) + '?' @@ -131,22 +132,13 @@ const POSSIBLE_ENDS = unirex( rexstr(DOCUMENT_END) + '|' + rexstr(/<\/p>/) ); -const CHARACTER_ESCAPE = unirex( - rexstr(/\\/) + - '(?:' + - rexstr(ESCAPE_CHAR) + '|' + - rexstr(/x/) + rexstr(HEXADECIMAL_CHARS) + '{2}' + '|' + - rexstr(/u/) + rexstr(HEXADECIMAL_CHARS) + '{4}' + '|' + - rexstr(/U/) + rexstr(HEXADECIMAL_CHARS) + '{8}' + - ')' +const QUOTE_CHAR = unirex( + '(?=' + rexstr(NOT_LINE_BREAK) + ')[^"]' ); -const ESCAPED_CHAR = unirex( - rexstr(/(?!["\\])/) + rexstr(NOT_LINE_BREAK) + '|' + - rexstr(CHARACTER_ESCAPE) -); -const ANY_ESCAPED_CHARS = unirex( - rexstr(ESCAPED_CHAR) + '*' +const ANY_QUOTE_CHAR = unirex( + rexstr(QUOTE_CHAR) + '*' ); + const ESCAPED_APOS = unirex( '(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/) ); @@ -190,120 +182,76 @@ const LATER_VALUE_CHAR = unirex( /* YAML CONSTRUCTS */ -const YAML_START = unirex( - rexstr(ANY_WHITE_SPACE) + rexstr(/---/) +const ƔAML_START = unirex( + rexstr(ANY_WHITE_SPACE) + '---' ); -const YAML_END = unirex( - rexstr(ANY_WHITE_SPACE) + rexstr(/(?:---|\.\.\.)/) +const ƔAML_END = unirex( + rexstr(ANY_WHITE_SPACE) + '(?:---|\.\.\.)' ); -const YAML_LOOKAHEAD = unirex( +const ƔAML_LOOKAHEAD = unirex( '(?=' + - rexstr(YAML_START) + + rexstr(ƔAML_START) + rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) + - rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) + + rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) + ')' ); -const YAML_DOUBLE_QUOTE = unirex( - rexstr(/"/) + rexstr(ANY_ESCAPED_CHARS) + rexstr(/"/) +const ƔAML_DOUBLE_QUOTE = unirex( + '"' + rexstr(ANY_QUOTE_CHAR) + '"' ); -const YAML_SINGLE_QUOTE = unirex( - rexstr(/'/) + rexstr(ANY_ESCAPED_APOS) + rexstr(/'/) +const ƔAML_SINGLE_QUOTE = unirex( + '\'' + rexstr(ANY_ESCAPED_APOS) + '\'' ); -const YAML_SIMPLE_KEY = unirex( +const ƔAML_SIMPLE_KEY = unirex( rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*' ); -const YAML_SIMPLE_VALUE = unirex( +const ƔAML_SIMPLE_VALUE = unirex( rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*' ); -const YAML_KEY = unirex( - rexstr(YAML_DOUBLE_QUOTE) + '|' + - rexstr(YAML_SINGLE_QUOTE) + '|' + - rexstr(YAML_SIMPLE_KEY) +const ƔAML_KEY = unirex( + rexstr(ƔAML_DOUBLE_QUOTE) + '|' + + rexstr(ƔAML_SINGLE_QUOTE) + '|' + + rexstr(ƔAML_SIMPLE_KEY) ); -const YAML_VALUE = unirex( - rexstr(YAML_DOUBLE_QUOTE) + '|' + - rexstr(YAML_SINGLE_QUOTE) + '|' + - rexstr(YAML_SIMPLE_VALUE) +const ƔAML_VALUE = unirex( + rexstr(ƔAML_DOUBLE_QUOTE) + '|' + + rexstr(ƔAML_SINGLE_QUOTE) + '|' + + rexstr(ƔAML_SIMPLE_VALUE) ); -const YAML_SEPARATOR = unirex( +const ƔAML_SEPARATOR = unirex( rexstr(ANY_WHITE_SPACE) + ':' + rexstr(WHITE_SPACE) + rexstr(ANY_WHITE_SPACE) ); -const YAML_LINE = unirex( - '(' + rexstr(YAML_KEY) + ')' + - rexstr(YAML_SEPARATOR) + - '(' + rexstr(YAML_VALUE) + ')' +const ƔAML_LINE = unirex( + '(' + rexstr(ƔAML_KEY) + ')' + + rexstr(ƔAML_SEPARATOR) + + '(' + rexstr(ƔAML_VALUE) + ')' ); /* FRONTMATTER REGEX */ -const YAML_FRONTMATTER = unirex( +const ƔAML_FRONTMATTER = unirex( rexstr(POSSIBLE_STARTS) + - rexstr(YAML_LOOKAHEAD) + - rexstr(YAML_START) + rexstr(SOME_NEW_LINES) + + rexstr(ƔAML_LOOKAHEAD) + + rexstr(ƔAML_START) + rexstr(SOME_NEW_LINES) + '(?:' + - '(' + rexstr(INDENTATION) + ')' + - rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) + - '(?:' + - '\\1' + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) + - '){0,4}' + - ')?' + - rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) + rexstr(SOME_NEW_LINES) + + '){0,5}' + + rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) ); /* SEARCHES */ -const FIND_YAML_LINES = unirex( - rexstr(NEW_LINE) + rexstr(INDENTATION) + rexstr(YAML_LINE) +const FIND_ƔAML_LINE = unirex( + rexstr(NEW_LINE) + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) ); /* STRING PROCESSING */ -function processString(str) { +function processString (str) { switch (str.charAt(0)) { case '"': - return str - .substring(1, str.length - 1) - .replace(/\\0/g, '\x00') - .replace(/\\a/g, '\x07') - .replace(/\\b/g, '\x08') - .replace(/\\t/g, '\x09') - .replace(/\\\x09/g, '\x09') - .replace(/\\n/g, '\x0a') - .replace(/\\v/g, '\x0b') - .replace(/\\f/g, '\x0c') - .replace(/\\r/g, '\x0d') - .replace(/\\e/g, '\x1b') - .replace(/\\ /g, '\x20') - .replace(/\\"/g, '\x22') - .replace(/\\\//g, '\x2f') - .replace(/\\\\/g, '\x5c') - .replace(/\\N/g, '\x85') - .replace(/\\_/g, '\xa0') - .replace(/\\L/g, '\u2028') - .replace(/\\P/g, '\u2029') - .replace( - new RegExp( - unirex( - rexstr(/\\x/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{2})' - ), 'gu' - ), (_, n) => String.fromCodePoint('0x' + n) - ) - .replace( - new RegExp( - unirex( - rexstr(/\\u/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{4})' - ), 'gu' - ), (_, n) => String.fromCodePoint('0x' + n) - ) - .replace( - new RegExp( - unirex( - rexstr(/\\U/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{8})' - ), 'gu' - ), (_, n) => String.fromCodePoint('0x' + n) - ); + return str.substring(1, str.length - 1); case '\'': return str .substring(1, str.length - 1) @@ -321,15 +269,18 @@ export function processBio(content) { text: content, metadata: [], }; - let yaml = content.match(YAML_FRONTMATTER); - if (!yaml) return result; - else yaml = yaml[0]; - let start = content.search(YAML_START); - let end = start + yaml.length - yaml.search(YAML_START); - result.text = content.substr(0, start) + content.substr(end); + let ɣaml = content.match(ƔAML_FRONTMATTER); + if (!ɣaml) { + return result; + } else { + ɣaml = ɣaml[0]; + } + const start = content.search(ƔAML_START); + const end = start + ɣaml.length - ɣaml.search(ƔAML_START); + result.text = content.substr(end); let metadata = null; - let query = new RegExp(FIND_YAML_LINES, 'g'); - while ((metadata = query.exec(yaml))) { + let query = new RegExp(rexstr(FIND_ƔAML_LINE), 'g'); // Some browsers don't allow flags unless both args are strings + while ((metadata = query.exec(ɣaml))) { result.metadata.push([ processString(metadata[1]), processString(metadata[2]), @@ -352,63 +303,23 @@ export function createBio(note, data) { let val = '' + data[i][1]; // Key processing - if (key === (key.match(YAML_SIMPLE_KEY) || [])[0]) /* do nothing */; - else if (key.indexOf('\'') === -1 && key === (key.match(ANY_ESCAPED_APOS) || [])[0]) key = '\'' + key + '\''; + if (key === (key.match(ƔAML_SIMPLE_KEY) || [])[0]) /* do nothing */; + else if (key === (key.match(ANY_QUOTE_CHAR) || [])[0]) key = '"' + key + '"'; else { key = key - .replace(/\x00/g, '\\0') - .replace(/\x07/g, '\\a') - .replace(/\x08/g, '\\b') - .replace(/\x0a/g, '\\n') - .replace(/\x0b/g, '\\v') - .replace(/\x0c/g, '\\f') - .replace(/\x0d/g, '\\r') - .replace(/\x1b/g, '\\e') - .replace(/\x22/g, '\\"') - .replace(/\x5c/g, '\\\\'); - let badchars = key.match( - new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu') - ) || []; - for (let j = 0; j < badchars.length; j++) { - key = key.replace( - badchars[i], - '\\u' + badchars[i].codePointAt(0).toLocaleString('en', { - useGrouping: false, - minimumIntegerDigits: 4, - }) - ); - } - key = '"' + key + '"'; + .replace(/'/g, '\'\'') + .replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '�'); + key = '\'' + key + '\''; } // Value processing - if (val === (val.match(YAML_SIMPLE_VALUE) || [])[0]) /* do nothing */; - else if (val.indexOf('\'') === -1 && val === (val.match(ANY_ESCAPED_APOS) || [])[0]) val = '\'' + val + '\''; + if (val === (val.match(ƔAML_SIMPLE_VALUE) || [])[0]) /* do nothing */; + else if (val === (val.match(ANY_QUOTE_CHAR) || [])[0]) val = '"' + val + '"'; else { - val = val - .replace(/\x00/g, '\\0') - .replace(/\x07/g, '\\a') - .replace(/\x08/g, '\\b') - .replace(/\x0a/g, '\\n') - .replace(/\x0b/g, '\\v') - .replace(/\x0c/g, '\\f') - .replace(/\x0d/g, '\\r') - .replace(/\x1b/g, '\\e') - .replace(/\x22/g, '\\"') - .replace(/\x5c/g, '\\\\'); - let badchars = val.match( - new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu') - ) || []; - for (let j = 0; j < badchars.length; j++) { - val = val.replace( - badchars[i], - '\\u' + badchars[i].codePointAt(0).toLocaleString('en', { - useGrouping: false, - minimumIntegerDigits: 4, - }) - ); - } - val = '"' + val + '"'; + key = key + .replace(/'/g, '\'\'') + .replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '�'); + key = '\'' + key + '\''; } frontmatter += key + ': ' + val + '\n'; -- cgit