From 32cad0d651fd6e56081d29d9d7c5703092ff1823 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Mon, 9 Jul 2018 22:00:27 +0800 Subject: [PATCH] Add markdown backend. --- js/md.js | 127 +++++++++++++++++++++++++++++ matrique.pro | 3 +- qml/component/MessageDelegate.qml | 130 ++++++++++++++++++++++++++++++ qml/form/RoomForm.qml | 82 +++++++------------ res.qrc | 2 + 5 files changed, 289 insertions(+), 55 deletions(-) create mode 100644 js/md.js create mode 100644 qml/component/MessageDelegate.qml diff --git a/js/md.js b/js/md.js new file mode 100644 index 000000000..96893a6ca --- /dev/null +++ b/js/md.js @@ -0,0 +1,127 @@ +/* jshint browser: true, devel: true */ + +/** + * preg_replace (from PHP) in JavaScript! + * + * This is basically a pattern replace. You can use a regex pattern to search and + * another for the replace. For more information see the PHP docs on the original + * function (http://php.net/manual/en/function.preg-replace.php), and for more on + * JavaScript flavour regex visit http://www.regular-expressions.info/javascript.html + * + * NOTE: Unlike the PHP version, this function only deals with string inputs. No arrays. + * + * @author William Duyck + * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License 2.0 + * + * @param {String} pattern The pattern to search for. + * @param {String} replace The string to replace. + * @param {String} subject The string to search and replace. + * @param {Integer} limit The maximum possible replacements. + * @return {String} If matches are found, the new subject will be returned. + */ +var preg_replace=function(a,b,c,d){void 0===d&&(d=-1);var e=a.substr(a.lastIndexOf(a[0])+1),f=a.substr(1,a.lastIndexOf(a[0])-1),g=RegExp(f,e),i=[],j=0,k=0,l=c,m=[];if(-1===d){do m=g.exec(c),null!==m&&i.push(m);while(null!==m&&-1!==e.indexOf("g"))}else i.push(g.exec(c));for(j=i.length-1;j>-1;j--){for(m=b,k=i[j].length;k>-1;k--)m=m.replace("${"+k+"}",i[j][k]).replace("$"+k,i[j][k]).replace("\\"+k,i[j][k]);l=l.replace(i[j][0],m)}return l}; + +/** + * Basic Markdown Parser + * + * This function parses a small subset of the Markdown language as defined by + * [John Gruber](http://daringfireball.net/projects/markdown). It's very basic + * and needs to be refactored a little, and there are plans to add more support + * for the rest of the language in the near future. + * + * This implimentation is based loosely on + * [slimdown.php](https://gist.github.com/jbroadway/2836900) by Johnny Broadway. + * + * @version 0.1 + * @author William Duyck + * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License 2.0 + * + * @param {String} str A Markdown string to be converted to HTML. + * @return {String} The HTML for the given Markdown. + */ +var markdown_parser = function(str){ + + var rules = [ + // headers + ['/(#+)(.*)/g', function(chars, header){ + var level = chars.length; + return ''+header.trim()+''; + }], + // images + ['/\\!\\[([^\\[]+)\\]\\(([^\\(]+)\\)/g', '\"\\1\"'], + // link + ['/\\[([^\\[]+)\\]\\(([^\\(]+)\\)/g', '\\1'], + // bold + ['/(\\*\\*|__)(.*?)\\1/g', '\\2'], + // emphasis + ['/(\\*|_)(.*?)\\1/g', '\\2'], + // strike + ['/(\\~\\~)(.*?)\\1/g', '\\2'], + // quote + ['/\\:\\"(.*?)\\"\\:/g', '\\1'], + // unordered list + ['/\\n\\*(.*)/g', function(item){ + return '
    \n
  • '+item.trim()+'
  • \n
'; + }], + // ordered list + ['/\\n[0-9]+\\.(.*)/g', function(item){ + return '
    \n
  1. '+item.trim()+'
  2. \n
'; + }], + // blockquote + ['/\\n\\>(.*)/g', function(str){ + return '
'+str.trim()+'
'; + }], + // paragraphs + ['/\\n[^\\n]+\\n/g', function(line){ + line = line.trim(); + if(line[0] === '<'){ + return line; + } + return '\n

'+line+'

\n'; + }] + ], fixes = [ + ['/<\\/ul>\n
    /g', '\n'], + ['/<\\/ol>\n
      /g', '\n'], + ['/<\\/blockquote>\n
      /g', "\n"] + ]; + + var parse_line = function(str){ + str = "\n" + str.trim() + "\n"; + for(var i = 0, j = rules.length; i < j; i++){ + if(typeof rules[i][1] == 'function') { + var _flag = rules[i][0].substr(rules[i][0].lastIndexOf(rules[i][0][0])+1), + _pattern = rules[i][0].substr(1, rules[i][0].lastIndexOf(rules[i][0][0])-1), + reg = new RegExp(_pattern, _flag); + + var matches = reg.exec(str); + if(matches !== null){ + if(matches.length > 1){ + str = preg_replace(rules[i][0], rules[i][1](matches[1], matches[2]), str); + } + else + { + str = preg_replace(rules[i][0], rules[i][1](matches[0]), str); + } + } + } + else { + str = preg_replace(rules[i][0], rules[i][1], str); + } + } + return str.trim(); + }; + + str = str.split('\n'); + var rtn = []; + for(var i = 0, j = str.length; i < j; i++){ + rtn.push(parse_line(str[i])); + } + + rtn = rtn.join('\n'); + + for(i = 0, j = fixes.length; i < j; i++){ + rtn = preg_replace(fixes[i][0], fixes[i][1], rtn); + } + + return rtn; +}; diff --git a/matrique.pro b/matrique.pro index b15e48f36..7e3795ff5 100644 --- a/matrique.pro +++ b/matrique.pro @@ -47,7 +47,8 @@ DISTFILES += \ RoomListForm.qml \ RoomDetailForm.qml \ Room.qml \ - Setting.qml + Setting.qml \ + qml/js/md.js HEADERS += \ matrix/controller.h \ diff --git a/qml/component/MessageDelegate.qml b/qml/component/MessageDelegate.qml new file mode 100644 index 000000000..29c7c770b --- /dev/null +++ b/qml/component/MessageDelegate.qml @@ -0,0 +1,130 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Controls.Material 2.4 + +Item { + readonly property bool sentByMe: author === currentRoom.localUser + + anchors.right: messageRow.visible && sentByMe ? parent.right : undefined + anchors.horizontalCenter: stateText.visible ? parent.horizontalCenter : undefined + + width: { + if (messageRow.visible) return messageRow.width + if (stateText.visible) return stateText.width + } + height: { + if (messageRow.visible) return messageRow.height + if (stateText.visible) return stateText.height + } + + MouseArea { + anchors.fill: parent + + ToolTip.visible: pressed + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: time + } + + Row { + id: messageRow + visible: eventType === "message" || eventType === "image" || eventType === "notice" + + spacing: 6 + + ImageStatus { + id: avatar + + width: height + height: 40 + round: false + visible: !sentByMe + source: author.avatarUrl != "" ? "image://mxc/" + author.avatarUrl : null + displayText: author.displayName + + MouseArea { + id: mouseArea + + anchors.fill: parent + + ToolTip.visible: pressed + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: author.displayName + } + } + + Rectangle { + id: messageRect + + width: { + if (eventType === "image") return messageImage.width + 24 + if (eventType === "message") + return Math.min(messageText.implicitWidth + 24, messageListView.width - (!sentByMe ? avatar.width + messageRow.spacing : 0)) + if (eventType === "notice") + return Math.min(noticeText.implicitWidth + 24, messageListView.width - (!sentByMe ? avatar.width + messageRow.spacing : 0)) + } + height: { + if (eventType === "image") return messageImage.height + 24 + if (eventType === "message") return messageText.implicitHeight + 24 + if (eventType === "notice") return noticeText.implicitHeight + 24 + } + + color: noticeText.visible ? "transparent" : sentByMe ? "lightgrey" : Material.accent + border.color: Material.accent + border.width: noticeText.visible ? 2 : 0 + + Label { + id: messageText + visible: eventType === "message" + text: display + color: sentByMe ? "black" : "white" + anchors.fill: parent + anchors.margins: 12 + wrapMode: Label.Wrap + textFormat: Text.RichText + } + + Label { + id: noticeText + visible: eventType === "notice" + text: display + color: "black" + anchors.fill: parent + anchors.margins: 12 + wrapMode: Label.Wrap + textFormat: Text.RichText + } + + Image { + id: messageImage + anchors.centerIn: parent + visible: eventType === "image" + source: visible? "image://mxc/" + content.url : "" + + MouseArea { + anchors.fill: parent + + ToolTip.visible: pressed + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: visible ? content.body : "" + } + } + } + } + + Label { + id: stateText + visible: eventType === "state" || eventType === "emote" + width: Math.min(implicitWidth, messageListView.width) + height: implicitHeight + padding: 12 + text: author.displayName + " " + display + color: eventType === "state" ? "black" : "white" + wrapMode: Label.Wrap + textFormat: Text.StyledText + + background: Rectangle { + anchors.fill: parent + color: eventType === "state" ? "lightgrey" : Material.accent + } + } +} diff --git a/qml/form/RoomForm.qml b/qml/form/RoomForm.qml index a65812512..9d7873d9c 100644 --- a/qml/form/RoomForm.qml +++ b/qml/form/RoomForm.qml @@ -5,6 +5,7 @@ import QtQuick.Controls.Material 2.4 import QtGraphicalEffects 1.0 import Matrique 0.1 import "qrc:/qml/component" +import "qrc:/js/md.js" as Markdown Item { id: item @@ -97,50 +98,8 @@ Item { onRoomChanged: if (room.timelineSize === 0) room.getPreviousContent(50) } - delegate: Row { - readonly property bool sentByMe: author === currentRoom.localUser + delegate: MessageDelegate { - id: messageRow - - anchors.right: sentByMe ? parent.right : undefined - spacing: 6 - - ImageStatus { - id: avatar - width: height - height: 40 - round: false - visible: !sentByMe - source: author.avatarUrl != "" ? "image://mxc/" + author.avatarUrl : null - displayText: author.displayName - - MouseArea { - id: mouseArea - anchors.fill: parent - - ToolTip.visible: pressed - ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval - ToolTip.text: author.displayName - } - } - - Rectangle { - width: Math.min(messageText.implicitWidth + 24, - messageListView.width - (!sentByMe ? avatar.width + messageRow.spacing : 0)) - height: messageText.implicitHeight + 24 - color: sentByMe ? "lightgrey" : Material.accent - - Label { - id: messageText - text: display - color: sentByMe ? "black" : "white" - linkColor: sentByMe ? Material.accent : "white" - anchors.fill: parent - anchors.margins: 12 - wrapMode: Label.Wrap - textFormat: Text.RichText - } - } } onAtYBeginningChanged: if (atYBeginning && currentRoom) currentRoom.getPreviousContent(50) @@ -154,12 +113,11 @@ Item { RoundButton { id: goTopFab width: height - height: !parent.atYEnd ? 64 : 0 + height: 64 + visible: !parent.atYEnd - anchors.verticalCenter: parent.bottom - anchors.verticalCenterOffset: -48 - anchors.horizontalCenter: parent.right - anchors.horizontalCenterOffset: -48 + anchors.right: parent.right + anchors.bottom: parent.bottom contentItem: MaterialIcon { anchors.fill: parent @@ -167,16 +125,13 @@ Item { color: "white" } - opacity: hovered ? 1 : 0.5 + opacity: hovered ? 0.7 : 0.4 Material.background: Qt.lighter(Material.accent) onClicked: parent.positionViewAtBeginning() - Behavior on height { - PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 } - } Behavior on opacity { - PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 } + PropertyAnimation { easing.type: Easing.Linear; duration: 200 } } } } @@ -214,9 +169,28 @@ Item { } Keys.onReturnPressed: { - currentRoom.postMessage("text", inputField.text) + postMessage(inputField.text) inputField.text = "" } + + function postMessage(text) { + if (text.trim().length === 0) { + return + } + if(!currentRoom) { + return + } + + var type = "m.text" + var PREFIX_ME = '/me ' + if (text.indexOf(PREFIX_ME) === 0) { + text = text.substr(PREFIX_ME.length) + type = "m.emote" + } + +// var parsedText = Markdown.markdown_parser(text) + currentRoom.postMessage(type, text) + } } ItemDelegate { diff --git a/res.qrc b/res.qrc index 1cd0ac50d..1c2d3abcd 100644 --- a/res.qrc +++ b/res.qrc @@ -16,5 +16,7 @@ qml/component/SideNavButton.qml qml/component/MaterialIcon.qml asset/img/icon.png + js/md.js + qml/component/MessageDelegate.qml