From fe5edb41751deb752d8b9ded0638103cdfa7f25e Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 28 Jun 2017 00:27:44 -0700 Subject: [PATCH] Backend YAML Processing + Profile Metadata on Static Pages --- app/javascript/styles/accounts.scss | 141 ++++++++-------- app/lib/frontmatter_handler.rb | 242 +++++++++++++++++++++++++++ app/views/accounts/_header.html.haml | 45 ++--- 3 files changed, 341 insertions(+), 87 deletions(-) create mode 100644 app/lib/frontmatter_handler.rb diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/accounts.scss index 10f8bd2b9c..d346a6bb2c 100644 --- a/app/javascript/styles/accounts.scss +++ b/app/javascript/styles/accounts.scss @@ -1,29 +1,34 @@ .card { + display: flex; background: $ui-base-color; - background-size: cover; - background-position: center; - padding: 60px 0; - padding-bottom: 0; border-radius: 4px 4px 0 0; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); overflow: hidden; - position: relative; @media screen and (max-width: 700px) { border-radius: 0; box-shadow: none; } - &::after { - background: linear-gradient(rgba($base-shadow-color, 0.5), rgba($base-shadow-color, 0.8)); - display: block; - content: ""; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: 1; + .details { + position: relative; + padding: 60px 0 0; + background-size: cover; + background-position: center; + text-align: center; + flex: auto; + + &::after { + background: linear-gradient(rgba($base-shadow-color, 0.5), rgba($base-shadow-color, 0.8)); + display: block; + content: ""; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 1; + } } .name { @@ -66,59 +71,38 @@ z-index: 2; } - .details { - display: flex; - margin-top: 30px; - position: relative; - z-index: 2; - flex-direction: row; - } - .details-counters { - display: flex; + position: relative; + display: inline-flex; flex-direction: row; - order: 0; + margin: 15px 0; + z-index: 2; } .counter { width: 80px; color: $ui-primary-color; padding: 5px 10px 0; - margin-bottom: 10px; - border-right: 1px solid $ui-primary-color; cursor: default; position: relative; + & + .counter { + border-left: 1px solid $ui-primary-color; + } + + & > * { + opacity: .7; + transition: opacity .3s ease; + } + + &.active > *, &:hover > * { + opacity: 1; + } + a { display: block; } - &::after { - display: block; - content: ""; - position: absolute; - bottom: -10px; - left: 0; - width: 100%; - border-bottom: 4px solid $ui-primary-color; - opacity: 0.5; - transition: all 0.8s ease; - } - - &.active { - &::after { - border-bottom: 4px solid $ui-highlight-color; - opacity: 1; - } - } - - &:hover { - &::after { - opacity: 1; - transition-duration: 0.2s; - } - } - a { text-decoration: none; color: inherit; @@ -140,30 +124,51 @@ } .bio { - flex: 1; + position: relative; font-size: 14px; line-height: 18px; + margin: 15px 0; padding: 5px 10px; color: $ui-secondary-color; - order: 1; + z-index: 2; } - @media screen and (max-width: 480px) { - .details { - display: block; - } + .metadata { + max-width: 40%; + background: $ui-base-color; + color: $primary-text-color; + text-align: left; + overflow-y: auto; + white-space: pre-wrap; - .bio { - text-align: center; - margin-bottom: 20px; - } + .metadata-item { + border-bottom: 1px $ui-primary-color solid; + padding: 15px 10px; + font-size: 18px; + line-height: 24px; + overflow: hidden; + text-overflow: ellipsis; - .counter { - flex: 1 1 auto; - } + a { + color: $ui-highlight-color; + text-decoration: none; - .counter:last-child { - border-right: none; + &:hover { + text-decoration: underline; + } + } + + b { + display: block; + font-size: 12px; + line-height: 16px; + text-transform: uppercase; + color: $ui-primary-color; + + a { + color: $ui-primary-color; + } + } } } } diff --git a/app/lib/frontmatter_handler.rb b/app/lib/frontmatter_handler.rb new file mode 100644 index 0000000000..19007cf4e4 --- /dev/null +++ b/app/lib/frontmatter_handler.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +require 'singleton' + +# See also `app/javascript/features/account/util/bio_metadata.js`. + +class FrontmatterHandler + include Singleton + + # CONVENIENCE FUNCTIONS # + + def self.unirex(str) + Regexp.new str, false, 'um' + end + def self.rexstr(exp) + '(?:' + exp.source + ')' + end + + # CHARACTER CLASSES # + + DOCUMENT_START = /^/ + DOCUMENT_END = /$/ + ALLOWED_CHAR = # c-printable` in the YAML 1.2 spec. + /[\t\n\r\u{20}-\u{7e}\u{85}\u{a0}-\u{d7ff}\u{e000}-\u{fffd}\u{10000}-\u{10ffff}]/u + WHITE_SPACE = /[ \t]/ + INDENTATION = / */ + LINE_BREAK = /\r?\n|\r|/ + ESCAPE_CHAR = /[0abt\tnvfre "\/\\N_LP]/ + HEXADECIMAL_CHARS = /[0-9a-fA-F]/ + INDICATOR = /[-?:,\[\]{}&#*!|>'"%@`]/ + FLOW_CHAR = /[,\[\]{}]/ + + # NEGATED CHARACTER CLASSES # + + NOT_WHITE_SPACE = unirex '(?!' + rexstr(WHITE_SPACE) + ').' + NOT_LINE_BREAK = unirex '(?!' + rexstr(LINE_BREAK) + ').' + NOT_INDICATOR = unirex '(?!' + rexstr(INDICATOR) + ').' + NOT_FLOW_CHAR = unirex '(?!' + rexstr(FLOW_CHAR) + ').' + + # BASIC CONSTRUCTS # + + ANY_WHITE_SPACE = unirex rexstr(WHITE_SPACE) + '*' + ANY_ALLOWED_CHARS = unirex rexstr(ALLOWED_CHAR) + '*' + NEW_LINE = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ) + SOME_NEW_LINES = unirex( + '(?:' + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ')+' + ) + POSSIBLE_STARTS = unirex( + rexstr(DOCUMENT_START) + rexstr(/]*>/) + '?' + ) + POSSIBLE_ENDS = unirex( + rexstr(SOME_NEW_LINES) + '|' + + rexstr(DOCUMENT_END) + '|' + + rexstr(/<\/p>/) + ) + 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}' + + ')' + ) + ESCAPED_CHAR = unirex( + rexstr(/(?!["\\])/) + rexstr(NOT_LINE_BREAK) + '|' + + rexstr(CHARACTER_ESCAPE) + ) + ANY_ESCAPED_CHARS = unirex( + rexstr(ESCAPED_CHAR) + '*' + ) + ESCAPED_APOS = unirex( + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/) + ) + ANY_ESCAPED_APOS = unirex( + rexstr(ESCAPED_APOS) + '*' + ) + 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) + ')' + ) + 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. + ) + 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) + ')' + ) + 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 # + + YAML_START = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(/---/) + ) + YAML_END = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(/(?:---|\.\.\.)/) + ) + YAML_LOOKAHEAD = unirex( + '(?=' + + rexstr(YAML_START) + + rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) + + rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) + + ')' + ) + YAML_DOUBLE_QUOTE = unirex( + rexstr(/"/) + rexstr(ANY_ESCAPED_CHARS) + rexstr(/"/) + ) + YAML_SINGLE_QUOTE = unirex( + rexstr(/'/) + rexstr(ANY_ESCAPED_APOS) + rexstr(/'/) + ) + YAML_SIMPLE_KEY = unirex( + rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*' + ) + YAML_SIMPLE_VALUE = unirex( + rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*' + ) + YAML_KEY = unirex( + rexstr(YAML_DOUBLE_QUOTE) + '|' + + rexstr(YAML_SINGLE_QUOTE) + '|' + + rexstr(YAML_SIMPLE_KEY) + ) + YAML_VALUE = unirex( + rexstr(YAML_DOUBLE_QUOTE) + '|' + + rexstr(YAML_SINGLE_QUOTE) + '|' + + rexstr(YAML_SIMPLE_VALUE) + ) + YAML_SEPARATOR = unirex( + rexstr(ANY_WHITE_SPACE) + + ':' + rexstr(WHITE_SPACE) + + rexstr(ANY_WHITE_SPACE) + ) + YAML_LINE = unirex( + '(' + rexstr(YAML_KEY) + ')' + + rexstr(YAML_SEPARATOR) + + '(' + rexstr(YAML_VALUE) + ')' + ) + + # FRONTMATTER REGEX # + + 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 # + + FIND_YAML_LINES = unirex( + rexstr(NEW_LINE) + rexstr(INDENTATION) + rexstr(YAML_LINE) + ) + + # STRING PROCESSING # + + def process_string(str) + case str[0] + when '"' + str[1..-2] + .gsub(/\\0/, "\u{00}") + .gsub(/\\a/, "\u{07}") + .gsub(/\\b/, "\u{08}") + .gsub(/\\t/, "\u{09}") + .gsub(/\\n/, "\u{0a}") + .gsub(/\\v/, "\u{0b}") + .gsub(/\\f/, "\u{0c}") + .gsub(/\\r/, "\u{0d}") + .gsub(/\\e/, "\u{1b}") + .gsub(/\\ /, "\u{20}") + .gsub(/\\"/, "\u{22}") + .gsub(/\\\//, "\u{2f}") + .gsub(/\\\\/, "\u{5c}") + .gsub(/\\N/, "\u{85}") + .gsub(/\\_/, "\u{a0}") + .gsub(/\\L/, "\u{2028}") + .gsub(/\\P/, "\u{2029}") + .gsub(/\\x([0-9a-fA-F]{2})/mu) {|s| $1.to_i.chr Encoding::UTF_8} + .gsub(/\\u([0-9a-fA-F]{4})/mu) {|s| $1.to_i.chr Encoding::UTF_8} + .gsub(/\\U([0-9a-fA-F]{8})/mu) {|s| $1.to_i.chr Encoding::UTF_8} + when "'" + str[1..-2].gsub(/''/, "'") + else + str + end + end + + # BIO PROCESSING # + + def process_bio content + result = { + text: content.gsub(/"/, '"').gsub(/'/, "'"), + metadata: [] + } + yaml = YAML_FRONTMATTER.match(result[:text]) + return result unless yaml + yaml = yaml[0] + start = YAML_START =~ result[:text] + ending = start + yaml.length - (YAML_START =~ yaml) + result[:text][start..ending - 1] = '' + metadata = nil + index = 0 + while metadata = FIND_YAML_LINES.match(yaml, index) do + index = metadata.end(0) + result[:metadata].push [ + process_string(metadata[1]), process_string(metadata[2]) + ] + end + return result + end + +end diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 6451a5573d..ef6a2bcbea 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -1,23 +1,24 @@ -.card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" } - - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) - .controls - - if current_account.following?(account) - = link_to t('accounts.unfollow'), account_unfollow_path(account), data: { method: :post }, class: 'button' - - else - = link_to t('accounts.follow'), account_follow_path(account), data: { method: :post }, class: 'button' - - elsif !user_signed_in? - .controls - .remote-follow - = link_to t('accounts.remote_follow'), account_remote_follow_path(account), class: 'button' - .avatar= image_tag account.avatar.url(:original), class: 'u-photo' - %h1.name - %span.p-name.emojify= display_name(account) - %small - %span @#{account.username} - = fa_icon('lock') if account.locked? - .details +- processed_bio = FrontmatterHandler.instance.process_bio Formatter.instance.simplified_format account +.card.h-card.p-author + .details{ style: "background-image: url(#{account.header.url(:original)})" } + - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) + .controls + - if current_account.following?(account) + = link_to t('accounts.unfollow'), account_unfollow_path(account), data: { method: :post }, class: 'button' + - else + = link_to t('accounts.follow'), account_follow_path(account), data: { method: :post }, class: 'button' + - elsif !user_signed_in? + .controls + .remote-follow + = link_to t('accounts.remote_follow'), account_remote_follow_path(account), class: 'button' + .avatar= image_tag account.avatar.url(:original), class: 'u-photo' + %h1.name + %span.p-name.emojify= display_name(account) + %small + %span @#{account.username} + = fa_icon('lock') if account.locked? .bio - .account__header__content.p-note.emojify= Formatter.instance.simplified_format(account) + .account__header__content.p-note.emojify!=processed_bio[:text] .details-counters .counter{ class: active_nav_class(short_account_url(account)) } @@ -32,3 +33,9 @@ = link_to account_followers_url(account) do %span.counter-label= t('accounts.followers') %span.counter-number= number_with_delimiter account.followers_count + - if processed_bio[:metadata].length > 0 + .metadata< + - processed_bio[:metadata].each do |i| + .metadata-item>< + %b.emojify>!=i[0] + %span.emojify>!=i[1]