diff --git a/.appveyor.yml b/.appveyor.yml index f1298a9..f7e237e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -3,13 +3,13 @@ image: Visual Studio 2017 environment: DEPLOY_DIR: Spectral-%APPVEYOR_BUILD_VERSION% matrix: - - QTDIR: C:\Qt\5.11\msvc2017_64 + - QTDIR: C:\Qt\5.12.1\msvc2017_64 VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" PLATFORM: init: - call "%QTDIR%\bin\qtenv2.bat" - - set PATH=C:\Qt\Tools\QtCreator\bin;%PATH% + - set PATH=%PATH%;C:\Qt\Tools\QtCreator\bin - call "%VCVARS%" %platform% - cd /D "%APPVEYOR_BUILD_FOLDER%" @@ -17,14 +17,14 @@ before_build: - git submodule update --init --recursive build_script: - - qmake spectral.pro CONFIG+=debug CONFIG+=qml_debug PREFIX="%DEPLOY_DIR%" + - qmake spectral.pro CONFIG+=release CONFIG+=qtquickcompiler PREFIX="%DEPLOY_DIR%" - nmake after_build: - nmake install - - windeployqt --debug --qmldir qml --qmldir imports "%DEPLOY_DIR%\spectral.exe" + - windeployqt --release --qmldir qml --qmldir imports "%DEPLOY_DIR%\spectral.exe" - 7z a spectral.zip "%DEPLOY_DIR%\" artifacts: - path: spectral.zip - name: portable \ No newline at end of file + name: portable diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7c22588..9ef3ac9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,11 +22,11 @@ build-appimage: before_script: - git submodule update --init --recursive script: - - /opt/qt511/bin/qt511-env.sh - - /opt/qt511/bin/qmake CONFIG+=debug CONFIG+=qml_debug PREFIX=/usr + - /opt/qt512/bin/qt512-env.sh + - /opt/qt512/bin/qmake CONFIG+=debug CONFIG+=qml_debug PREFIX=/usr - make - make INSTALL_ROOT=appdir install - - /usr/bin/linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/org.eu.encom.spectral.desktop -appimage -qmldir=qml -qmldir=imports -qmake=/opt/qt511/bin/qmake + - /usr/bin/linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/org.eu.encom.spectral.desktop -appimage -qmldir=qml -qmldir=imports -qmake=/opt/qt512/bin/qmake artifacts: paths: - - Spectral-x86_64.AppImage \ No newline at end of file + - Spectral*.AppImage diff --git a/assets/font/roboto.ttf b/assets/font/roboto.ttf new file mode 100644 index 0000000..2c97eea Binary files /dev/null and b/assets/font/roboto.ttf differ diff --git a/assets/font/twemoji.ttf b/assets/font/twemoji.ttf new file mode 100644 index 0000000..b8a19d0 Binary files /dev/null and b/assets/font/twemoji.ttf differ diff --git a/assets/img/avatar.png b/assets/img/avatar.png deleted file mode 100644 index 6d355bb..0000000 Binary files a/assets/img/avatar.png and /dev/null differ diff --git a/assets/img/background.jpg b/assets/img/background.jpg deleted file mode 100644 index de559fd..0000000 Binary files a/assets/img/background.jpg and /dev/null differ diff --git a/assets/img/matrix.svg b/assets/img/matrix.svg new file mode 100644 index 0000000..eff00f2 --- /dev/null +++ b/assets/img/matrix.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/img/roompanel-dark.svg b/assets/img/roompanel-dark.svg new file mode 100644 index 0000000..d240920 --- /dev/null +++ b/assets/img/roompanel-dark.svg @@ -0,0 +1,219 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/roompanel.svg b/assets/img/roompanel.svg new file mode 100644 index 0000000..d01c669 --- /dev/null +++ b/assets/img/roompanel.svg @@ -0,0 +1,219 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flatpak/org.eu.encom.spectral.yaml b/flatpak/org.eu.encom.spectral.yaml index 574c14a..644b4b5 100644 --- a/flatpak/org.eu.encom.spectral.yaml +++ b/flatpak/org.eu.encom.spectral.yaml @@ -1,6 +1,6 @@ id: org.eu.encom.spectral runtime: org.kde.Platform -runtime-version: 5.11 +runtime-version: 5.12 sdk: org.kde.Sdk command: spectral finish-args: @@ -15,6 +15,8 @@ finish-args: modules: - name: spectral buildsystem: qmake + config-opts: + - "BUNDLE_FONT=true" sources: - type: dir path: ../ diff --git a/font.qrc b/font.qrc new file mode 100644 index 0000000..cfda4ec --- /dev/null +++ b/font.qrc @@ -0,0 +1,6 @@ + + + assets/font/roboto.ttf + assets/font/twemoji.ttf + + diff --git a/imports/Spectral/Component/AutoListView.qml b/imports/Spectral/Component/AutoListView.qml index 2b6bddb..1fca551 100644 --- a/imports/Spectral/Component/AutoListView.qml +++ b/imports/Spectral/Component/AutoListView.qml @@ -1,4 +1,4 @@ -import QtQuick 2.9 +import QtQuick 2.12 ListView { ScrollHelper { diff --git a/imports/Spectral/Component/AutoMouseArea.qml b/imports/Spectral/Component/AutoMouseArea.qml index ceaf84d..124625b 100644 --- a/imports/Spectral/Component/AutoMouseArea.qml +++ b/imports/Spectral/Component/AutoMouseArea.qml @@ -1,4 +1,4 @@ -import QtQuick 2.9 +import QtQuick 2.12 import Spectral.Setting 0.1 diff --git a/imports/Spectral/Component/AutoTextField.qml b/imports/Spectral/Component/AutoTextField.qml index 8bd6bac..274b236 100644 --- a/imports/Spectral/Component/AutoTextField.qml +++ b/imports/Spectral/Component/AutoTextField.qml @@ -1,5 +1,5 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 TextField { selectByMouse: true diff --git a/imports/Spectral/Component/Avatar.qml b/imports/Spectral/Component/Avatar.qml new file mode 100644 index 0000000..399f465 --- /dev/null +++ b/imports/Spectral/Component/Avatar.qml @@ -0,0 +1,64 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.0 + +Item { + property string hint: "H" + property string source: "" + readonly property url realSource: source ? "image://mxc/" + source : "" + + id: root + + Image { + anchors.fill: parent + + id: image + visible: realSource + source: width < 1 ? "" : realSource + sourceSize.width: width + sourceSize.height: width + fillMode: Image.PreserveAspectCrop + mipmap: true + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: image.width + height: image.width + + radius: width / 2 + } + } + } + + Rectangle { + anchors.fill: parent + + visible: !realSource || image.status != Image.Ready + + radius: height / 2 + + color: stringToColor(hint) + + Label { + anchors.centerIn: parent + + color: "white" + text: hint[0].toUpperCase() + font.pixelSize: root.width / 2 + font.bold: true + } + } + + function stringToColor(str) { + var hash = 0; + for (var i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + var colour = '#'; + for (var j = 0; j < 3; j++) { + var value = (hash >> (j * 8)) & 0xFF; + colour += ('00' + value.toString(16)).substr(-2); + } + return colour; + } +} diff --git a/imports/Spectral/Component/Emoji/EmojiPicker.qml b/imports/Spectral/Component/Emoji/EmojiPicker.qml index e7674f5..019357f 100644 --- a/imports/Spectral/Component/Emoji/EmojiPicker.qml +++ b/imports/Spectral/Component/Emoji/EmojiPicker.qml @@ -1,94 +1,107 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 + +import Spectral.Component 2.0 import Spectral 0.1 +import Spectral.Setting 0.1 -Popup { - property var emojiModel - property var textArea +ColumnLayout { property string emojiCategory: "people" + property var textArea + property var emojiModel - ColumnLayout { - anchors.fill: parent + spacing: 0 - GridView { - Layout.fillWidth: true - Layout.fillHeight: true + ListView { + Layout.fillWidth: true + Layout.preferredHeight: 48 + Layout.leftMargin: 24 + Layout.rightMargin: 24 - cellWidth: 36 - cellHeight: 36 + boundsBehavior: Flickable.DragOverBounds - boundsBehavior: Flickable.DragOverBounds + clip: true - clip: true + orientation: ListView.Horizontal - model: emojiModel.model[emojiCategory] - - delegate: ItemDelegate { - width: 36 - height: 36 - - contentItem: Text { - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - font.pointSize: 20 - font.family: "Emoji" - text: modelData.unicode - } - - hoverEnabled: true - ToolTip.text: modelData.shortname - ToolTip.visible: hovered - - onClicked: textArea.insert(textArea.cursorPosition, modelData.unicode) - } - - ScrollBar.vertical: ScrollBar {} + model: ListModel { + ListElement { label: "😏"; category: "people" } + ListElement { label: "🌲"; category: "nature" } + ListElement { label: "🍛"; category: "food"} + ListElement { label: "🚁"; category: "activity" } + ListElement { label: "🚅"; category: "travel" } + ListElement { label: "💡"; category: "objects" } + ListElement { label: "🔣"; category: "symbols" } + ListElement { label: "🏁"; category: "flags" } } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 2 + delegate: ItemDelegate { + width: 64 + height: 48 - color: Material.accent - } + contentItem: Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter - Row { - Repeater { - model: ListModel { - ListElement { label: "😏"; category: "people" } - ListElement { label: "🌲"; category: "nature" } - ListElement { label: "🍛"; category: "food"} - ListElement { label: "🚁"; category: "activity" } - ListElement { label: "🚅"; category: "travel" } - ListElement { label: "💡"; category: "objects" } - ListElement { label: "🔣"; category: "symbols" } - ListElement { label: "🏁"; category: "flags" } - } - - delegate: ItemDelegate { - width: 36 - height: 36 - - contentItem: Text { - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - font.pointSize: 20 - font.family: "Emoji" - text: label - } - - hoverEnabled: true - ToolTip.text: category - ToolTip.visible: hovered - - onClicked: emojiCategory = category - } + font.pixelSize: 24 + text: label } + + Rectangle { + anchors.bottom: parent.bottom + + width: parent.width + height: 2 + + visible: emojiCategory === category + + color: Material.accent + } + + onClicked: emojiCategory = category } } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + Layout.leftMargin: 12 + Layout.rightMargin: 12 + + color: MSettings.darkTheme ? "#424242" : "#e7ebeb" + } + + GridView { + Layout.fillWidth: true + Layout.preferredHeight: 180 + + cellWidth: 48 + cellHeight: 48 + + boundsBehavior: Flickable.DragOverBounds + + clip: true + + model: emojiModel.model[emojiCategory] + + delegate: ItemDelegate { + width: 48 + height: 48 + + contentItem: Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + font.pixelSize: 32 + text: modelData.unicode + } + + onClicked: textArea.insert(textArea.cursorPosition, modelData.unicode) + } + + ScrollBar.vertical: ScrollBar {} + } } diff --git a/imports/Spectral/Component/MaterialIcon.qml b/imports/Spectral/Component/MaterialIcon.qml index 5f94994..83c4d7e 100644 --- a/imports/Spectral/Component/MaterialIcon.qml +++ b/imports/Spectral/Component/MaterialIcon.qml @@ -1,6 +1,6 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 import Spectral.Setting 0.1 import Spectral.Font 0.1 @@ -10,8 +10,8 @@ Text { id: materialLabel - color: MSettings.darkTheme ? "white" : "dark" - font.pointSize: 16 + color: MPalette.foreground + font.pixelSize: 24 font.family: MaterialFont.name horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/imports/Spectral/Component/ScrollHelper.qml b/imports/Spectral/Component/ScrollHelper.qml index 923a141..89425d6 100644 --- a/imports/Spectral/Component/ScrollHelper.qml +++ b/imports/Spectral/Component/ScrollHelper.qml @@ -1,5 +1,5 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 MouseArea { id: root @@ -66,14 +66,13 @@ MouseArea { } onWheel: { - var newPos = calculateNewPosition(flickable, wheel); // console.warn("Delta: ", wheel.pixelDelta.y); // console.warn("Old position: ", flickable.contentY); // console.warn("New position: ", newPos); // Show the scrollbars flickable.flick(0, 0); - flickable.contentY = newPos; + flickable.contentY = calculateNewPosition(flickable, wheel); cancelFlickStateTimer.start() } diff --git a/imports/Spectral/Component/SideNavButton.qml b/imports/Spectral/Component/SideNavButton.qml deleted file mode 100644 index d2fb7e3..0000000 --- a/imports/Spectral/Component/SideNavButton.qml +++ /dev/null @@ -1,25 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 - -import "qrc:/js/util.js" as Util - -ItemDelegate { - property var page - property bool selected: stackView.currentItem === page - property color highlightColor: Material.accent - - Rectangle { - width: selected ? 4 : 0 - height: parent.height - - color: highlightColor - - Behavior on width { - PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 } - } - } - - onClicked: Util.pushToStack(stackView, page) -} diff --git a/imports/Spectral/Component/SplitView.qml b/imports/Spectral/Component/SplitView.qml index fc553f9..a29e14e 100644 --- a/imports/Spectral/Component/SplitView.qml +++ b/imports/Spectral/Component/SplitView.qml @@ -37,10 +37,11 @@ ** ****************************************************************************/ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 import QtQuick.Window 2.1 +import Spectral.Setting 0.1 Item { id: root @@ -64,7 +65,7 @@ Item { property Component handleDelegate: Rectangle { width: 1 height: 1 - visible: false + color: MSettings.darkTheme ? "#424242" : "#E1E1E1" } /*! @@ -88,7 +89,7 @@ Item { /*! \qmlmethod void SplitView::addItem(Item item) Add an item to the end of the view. - \since QtQuick.Controls 1.3 */ + \since QtQuick.Controls 1.12 */ function addItem(item) { d.updateLayoutGuard = true d.addItem_impl(item) diff --git a/imports/Spectral/Component/Timeline/DownloadableContent.qml b/imports/Spectral/Component/Timeline/DownloadableContent.qml index ba586d4..60be43e 100644 --- a/imports/Spectral/Component/Timeline/DownloadableContent.qml +++ b/imports/Spectral/Component/Timeline/DownloadableContent.qml @@ -1,6 +1,6 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Controls.Material 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.Material 2.12 import Qt.labs.platform 1.0 Item { diff --git a/imports/Spectral/Component/Timeline/FileDelegate.qml b/imports/Spectral/Component/Timeline/FileDelegate.qml new file mode 100644 index 0000000..87186b6 --- /dev/null +++ b/imports/Spectral/Component/Timeline/FileDelegate.qml @@ -0,0 +1,174 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 +import QtGraphicalEffects 1.0 +import Qt.labs.platform 1.0 as Platform + +import Spectral 0.1 +import Spectral.Setting 0.1 + +import Spectral.Component 2.0 +import Spectral.Font 0.1 + +ColumnLayout { + readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other") + readonly property bool sentByMe: author === currentRoom.localUser + + property bool openOnFinished: false + readonly property bool downloaded: progressInfo && progressInfo.completed + + id: root + + spacing: 0 + + onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile() + + Label { + Layout.leftMargin: 48 + + text: author.displayName + + visible: avatarVisible + + font.pixelSize: 13 + verticalAlignment: Text.AlignVCenter + } + + RowLayout { + Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft + + z: -5 + + id: messageRow + + spacing: 4 + + Avatar { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + visible: avatarVisible + hint: author.displayName + source: author.avatarMediaId + } + + Label { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + visible: !(sentByMe || avatarVisible) + + text: Qt.formatDateTime(time, "hh:mm") + color: "#5B7480" + + font.pixelSize: 10 + horizontalAlignment: Label.AlignHCenter + verticalAlignment: Label.AlignVCenter + } + + Control { + Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48 + + padding: 12 + + contentItem: RowLayout { + ToolButton { + contentItem: MaterialIcon { + icon: progressInfo.completed ? "\ue5ca" : "\ue2c4" + } + + onClicked: progressInfo.completed ? openSavedFile() : saveFileAs() + } + + ColumnLayout { + Label { + Layout.alignment: Qt.AlignVCenter + + text: display + font.pixelSize: 18 + font.weight: Font.Medium + font.capitalization: Font.AllUppercase + } + + Label { + text: progressInfo.active ? (progressInfo.progress + "/" + progressInfo.total) : content.info.size + color: MPalette.lighter + } + } + } + + background: Rectangle { + color: MPalette.banner + radius: 18 + + AutoMouseArea { + anchors.fill: parent + + id: messageMouseArea + + onSecondaryClicked: messageContextMenu.popup() + + Menu { + id: messageContextMenu + + MenuItem { + text: "View Source" + + onTriggered: { + sourceDialog.sourceText = toolTip + sourceDialog.open() + } + } + MenuItem { + text: "Open Externally" + + onTriggered: downloadAndOpen() + } + MenuItem { + text: "Save As" + + onTriggered: saveFileAs() + } + MenuItem { + text: "Reply" + + onTriggered: { + roomPanelInput.replyUser = author + roomPanelInput.replyEventID = eventId + roomPanelInput.replyContent = message + roomPanelInput.isReply = true + roomPanelInput.focus() + } + } + MenuItem { + text: "Redact" + + onTriggered: currentRoom.redactEvent(eventId) + } + } + } + } + } + } + + function saveFileAs() { currentRoom.saveFileAs(eventId) } + + function downloadAndOpen() + { + if (downloaded) openSavedFile() + else + { + openOnFinished = true + currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_") + (message || ".tmp")) + } + } + + function openSavedFile() + { + if (Qt.openUrlExternally(progressInfo.localPath)) return; + if (Qt.openUrlExternally(progressInfo.localDir)) return; + } +} diff --git a/imports/Spectral/Component/Timeline/GenericBubble.qml b/imports/Spectral/Component/Timeline/GenericBubble.qml deleted file mode 100644 index cc33245..0000000 --- a/imports/Spectral/Component/Timeline/GenericBubble.qml +++ /dev/null @@ -1,37 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Controls.Material 2.2 - -import Spectral.Component 2.0 -import Spectral.Effect 2.0 - -import Spectral.Setting 0.1 - -Control { - property bool highlighted: false - property bool colored: false - - readonly property bool darkBackground: highlighted ? true : MSettings.darkTheme - - padding: 12 - - AutoMouseArea { - anchors.fill: parent - - onSecondaryClicked: { - messageContextMenu.row = messageRow - messageContextMenu.model = model - messageContextMenu.selectedText = contentLabel.selectedText - messageContextMenu.popup() - } - } - - background: Rectangle { - color: colored ? Material.accent : highlighted ? Material.primary : Material.background - - layer.enabled: true - layer.effect: ElevationEffect { - elevation: 1 - } - } -} diff --git a/imports/Spectral/Component/Timeline/ImageDelegate.qml b/imports/Spectral/Component/Timeline/ImageDelegate.qml new file mode 100644 index 0000000..c077842 --- /dev/null +++ b/imports/Spectral/Component/Timeline/ImageDelegate.qml @@ -0,0 +1,168 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 +import QtGraphicalEffects 1.0 +import Qt.labs.platform 1.0 as Platform + +import Spectral 0.1 +import Spectral.Setting 0.1 + +import Spectral.Component 2.0 +import Spectral.Font 0.1 + +ColumnLayout { + readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other") + readonly property bool sentByMe: author === currentRoom.localUser + + property bool openOnFinished: false + readonly property bool downloaded: progressInfo && progressInfo.completed + + id: root + + spacing: 0 + + onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile() + + Label { + Layout.leftMargin: 48 + + text: author.displayName + + visible: avatarVisible + + font.pixelSize: 13 + verticalAlignment: Text.AlignVCenter + } + + RowLayout { + Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft + + z: -5 + + id: messageRow + + spacing: 4 + + Avatar { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + visible: avatarVisible + hint: author.displayName + source: author.avatarMediaId + } + + Label { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + visible: !(sentByMe || avatarVisible) + + text: Qt.formatDateTime(time, "hh:mm") + color: "#5B7480" + + font.pixelSize: 10 + horizontalAlignment: Label.AlignHCenter + verticalAlignment: Label.AlignVCenter + } + + Image { + Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48 + + id: img + + source: downloaded ? progressInfo.localPath : "image://mxc/" + + (content.info && content.info.thumbnail_info ? + content.thumbnailMediaId : content.mediaId) + sourceSize.width: 200 + sourceSize.height: 200 + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: img.width + height: img.height + radius: 24 + } + } + + Rectangle { + anchors.fill: parent + + color: "transparent" + radius: 24 + + border.width: 2 + border.color: MPalette.banner + } + + AutoMouseArea { + anchors.fill: parent + + id: messageMouseArea + + onSecondaryClicked: messageContextMenu.popup() + + Menu { + id: messageContextMenu + + MenuItem { + text: "View Source" + + onTriggered: { + sourceDialog.sourceText = toolTip + sourceDialog.open() + } + } + MenuItem { + text: "Open Externally" + + onTriggered: downloadAndOpen() + } + MenuItem { + text: "Save As" + + onTriggered: saveFileAs() + } + MenuItem { + text: "Reply" + + onTriggered: { + roomPanelInput.replyUser = author + roomPanelInput.replyEventID = eventId + roomPanelInput.replyContent = message + roomPanelInput.isReply = true + roomPanelInput.focus() + } + } + MenuItem { + text: "Redact" + + onTriggered: currentRoom.redactEvent(eventId) + } + } + } + } + } + + function saveFileAs() { currentRoom.saveFileAs(eventId) } + + function downloadAndOpen() + { + if (downloaded) openSavedFile() + else + { + openOnFinished = true + currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_") + (message || ".tmp")) + } + } + + function openSavedFile() + { + if (Qt.openUrlExternally(progressInfo.localPath)) return; + if (Qt.openUrlExternally(progressInfo.localDir)) return; + } +} diff --git a/imports/Spectral/Component/Timeline/MessageDelegate.qml b/imports/Spectral/Component/Timeline/MessageDelegate.qml index fe4cb3c..a881937 100644 --- a/imports/Spectral/Component/Timeline/MessageDelegate.qml +++ b/imports/Spectral/Component/Timeline/MessageDelegate.qml @@ -1,254 +1,217 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 import Spectral 0.1 import Spectral.Setting 0.1 import Spectral.Component 2.0 +import Spectral.Font 0.1 -RowLayout { +ColumnLayout { readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other") - readonly property bool highlighted: !(sentByMe || eventType === "notice" ) readonly property bool sentByMe: author === currentRoom.localUser - readonly property bool isText: eventType === "notice" || eventType === "message" signal saveFileAs() signal openExternally() - z: -5 - - id: messageRow - Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft - spacing: 6 + id: root - ImageItem { - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 - Layout.alignment: Qt.AlignTop + spacing: 0 + + Label { + Layout.leftMargin: 48 + + text: author.displayName - round: false visible: avatarVisible - hint: author.displayName - source: author.paintable + + font.pixelSize: 13 + verticalAlignment: Text.AlignVCenter } - Rectangle { - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 - Layout.alignment: Qt.AlignTop + RowLayout { + z: -5 - color: "transparent" - visible: !(sentByMe || avatarVisible) - } + id: messageRow - GenericBubble { - Layout.maximumWidth: messageListView.width - (!sentByMe ? 40 + messageRow.spacing : 0) + spacing: 4 - id: genericBubble + Avatar { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop - highlighted: messageRow.highlighted - colored: highlighted && (eventType === "notice" || highlight) + visible: avatarVisible + hint: author.displayName + source: author.avatarMediaId + } - contentItem: ColumnLayout { - id: messageColumn + Label { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop - spacing: 0 + visible: !(sentByMe || avatarVisible) - TimelineLabel { - Layout.fillWidth: true + text: Qt.formatDateTime(time, "hh:mm") + color: MPalette.lighter - id: authorLabel + font.pixelSize: 10 + horizontalAlignment: Label.AlignHCenter + verticalAlignment: Label.AlignVCenter + } - visible: messageRow.avatarVisible - text: author.displayName - Material.foreground: Material.accent - coloredBackground: highlighted - font.bold: true + Control { + Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48 - MouseArea { + verticalPadding: 8 + horizontalPadding: 16 + + background: Rectangle { + color: sentByMe ? "#009DC2" : eventType === "notice" ? "#4285F4" : "#673AB7" + radius: 18 + + AutoMouseArea { anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: roomPanelInput.insert(author.displayName) - } - } - TextEdit { - Layout.fillWidth: true + id: messageMouseArea - id: contentLabel + onSecondaryClicked: messageContextMenu.popup() - text: (highlighted ? "" : "") + display + Menu { + readonly property string selectedText: contentLabel.selectedText - visible: isText - color: highlighted ? "white": Material.foreground + id: messageContextMenu - font.family: authorLabel.font.family - font.pointSize: 10 - selectByMouse: true - readOnly: true - wrapMode: Label.Wrap - selectedTextColor: highlighted ? Material.accent : "white" - selectionColor: highlighted ? "white" : Material.accent - textFormat: Text.RichText + MenuItem { + text: "View Source" - onLinkActivated: Qt.openUrlExternally(link) + onTriggered: { + sourceDialog.sourceText = toolTip + sourceDialog.open() + } + } + MenuItem { + text: "Reply" - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor - } - } + onTriggered: { + roomPanelInput.replyUser = author + roomPanelInput.replyEventID = eventId + roomPanelInput.replyContent = messageContextMenu.selectedText || message + roomPanelInput.isReply = true + roomPanelInput.focus() + } + } + MenuItem { + text: "Redact" - Loader { - sourceComponent: { - switch (eventType) { - case "image": - return imageComponent - case "file": - return fileComponent - case "audio": - return audioComponent + onTriggered: currentRoom.redactEvent(eventId) + } } } - - active: eventType === "image" || eventType === "file" || eventType === "audio" } - Row { - Layout.alignment: Qt.AlignRight + contentItem: ColumnLayout { + Control { + Layout.fillWidth: true - spacing: 4 + visible: replyEventId || "" - TimelineLabel { - visible: userMarker.length > 5 - text: userMarker.length - 5 + "+" - coloredBackground: highlighted - Material.foreground: "grey" - font.pointSize: 8 - } + padding: 8 - Repeater { - model: userMarker.length > 5 ? userMarker.slice(0, 5) : userMarker - - ImageItem { - width: parent.height - height: parent.height - - hint: modelData.displayName - source: modelData.paintable + background: Item { + Rectangle { + anchors.leftMargin: 0 + width: 2 + height: parent.height + color: "white" + } MouseArea { anchors.fill: parent - cursorShape: Qt.PointingHandCursor + onClicked: goToEvent(replyEventId) + } + } - onClicked: { - readMarkerDialog.listModel = userMarker - readMarkerDialog.open() + contentItem: RowLayout { + spacing: 8 + + Avatar { + Layout.preferredWidth: 36 + Layout.preferredHeight: 36 + Layout.alignment: Qt.AlignTop + + source: replyAuthor ? replyAuthor.avatarMediaId : "" + hint: replyAuthor ? replyAuthor.displayName : "H" + } + + ColumnLayout { + Layout.fillWidth: true + + spacing: 0 + + Label { + Layout.fillWidth: true + + color: "white" + text: replyAuthor ? replyAuthor.displayName : "" + + font.pixelSize: 13 + font.weight: Font.Medium + } + + Label { + Layout.fillWidth: true + + color: "white" + text: replyDisplay || "" + + wrapMode: Label.Wrap + textFormat: Label.RichText } } } } - TimelineLabel { - id: timeLabel + TextEdit { + Layout.fillWidth: true - visible: Math.abs(time - aboveTime) > 600000 || index == 0 - text: Qt.formatTime(time, "hh:mm") - coloredBackground: highlighted - Material.foreground: "grey" - font.pointSize: 8 - } - } - } + id: contentLabel - Component { - id: imageComponent + text: "" + display - DownloadableContent { - width: messageImage.width - height: messageImage.height + color: "white" - id: downloadable + font.family: CommonFont.font.family + font.pixelSize: 14 + selectByMouse: true + readOnly: true + wrapMode: Label.Wrap + selectedTextColor: Material.accent + selectionColor: "white" + textFormat: Text.RichText - TimelineImage { - z: -4 - - id: messageImage - - sourceSize: 128 - source: "image://mxc/" + (content.thumbnail_url ? content.thumbnail_url : content.url) - - onClicked: downloadAndOpen() - } - - Component.onCompleted: { - messageRow.saveFileAs.connect(saveFileAs) - messageRow.openExternally.connect(downloadAndOpen) - } - } - } - - Component { - id: fileComponent - - TimelineLabel { - Layout.fillWidth: true - - id: downloadDelegate - - text: "File: " + content.body - coloredBackground: highlighted - - background: DownloadableContent { - id: downloadable - - Component.onCompleted: { - messageRow.saveFileAs.connect(saveFileAs) - messageRow.openExternally.connect(downloadAndOpen) - } - } - } - } - - Component { - id: audioComponent - - TimelineLabel { - id: downloadDelegate - - text: content.info.duration / 1000 + '"' - coloredBackground: highlighted - - MouseArea { - anchors.fill: parent - - propagateComposedEvents: true - - onClicked: { - if (downloadable.downloaded) - spectralController.playAudio(progressInfo.localPath) - else - { - playOnFinished = true - currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_") + ".tmp") + onLinkActivated: { + if (link.startsWith("https://matrix.to/")) { + var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)") + if (result.length < 3) return + if (result[1] != currentRoom.id) return + if (!result[2]) return + goToEvent(result[2]) + } else { + Qt.openUrlExternally(link) } } - } - background: DownloadableContent { - id: downloadable - - onDownloadedChanged: downloaded && playOnFinished ? spectralController.playAudio(progressInfo.localPath) : {} - - Component.onCompleted: { - messageRow.saveFileAs.connect(saveFileAs) - messageRow.openExternally.connect(downloadAndOpen) + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } } } diff --git a/imports/Spectral/Component/Timeline/SectionDelegate.qml b/imports/Spectral/Component/Timeline/SectionDelegate.qml new file mode 100644 index 0000000..e84be60 --- /dev/null +++ b/imports/Spectral/Component/Timeline/SectionDelegate.qml @@ -0,0 +1,12 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import Spectral.Setting 0.1 + +Label { + text: section + " • " + Qt.formatTime(time, "hh:mm") + color: MPalette.foreground + font.pixelSize: 13 + font.weight: Font.Medium + font.capitalization: Font.AllUppercase + verticalAlignment: Text.AlignVCenter +} diff --git a/imports/Spectral/Component/Timeline/StateDelegate.qml b/imports/Spectral/Component/Timeline/StateDelegate.qml index ab7272b..9e6fef7 100644 --- a/imports/Spectral/Component/Timeline/StateDelegate.qml +++ b/imports/Spectral/Component/Timeline/StateDelegate.qml @@ -1,24 +1,27 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 import Spectral.Setting 0.1 Label { - Layout.alignment: Qt.AlignHCenter - text: "" + author.displayName + " " + display - color: "white" + color: MPalette.foreground + font.pixelSize: 13 + font.weight: Font.Medium - padding: 8 + topPadding: 8 + bottomPadding: 8 + leftPadding: 24 + rightPadding: 24 wrapMode: Label.Wrap - linkColor: "white" textFormat: MSettings.richText ? Text.RichText : Text.StyledText onLinkActivated: Qt.openUrlExternally(link) background: Rectangle { - color: MSettings.darkTheme ? "#484848" : "grey" + color: MPalette.banner + radius: 4 } } diff --git a/imports/Spectral/Component/Timeline/TimelineImage.qml b/imports/Spectral/Component/Timeline/TimelineImage.qml deleted file mode 100644 index c7050de..0000000 --- a/imports/Spectral/Component/Timeline/TimelineImage.qml +++ /dev/null @@ -1,34 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 - -Item { - property alias source: baseImage.source - property alias sourceSize: baseImage.sourceSize.width - - readonly property bool loading: baseImage.status == Image.Loading - - signal clicked() - - width: loading ? 128 : baseImage.implicitWidth - height: loading ? progressBar.height : baseImage.implicitHeight - - id: rekt - - Image { id: baseImage } - - ProgressBar { - width: parent.width - visible: loading - - id: progressBar - - indeterminate: true - } - - MouseArea { - anchors.fill: parent - propagateComposedEvents: true - - onClicked: rekt.clicked() - } -} diff --git a/imports/Spectral/Component/Timeline/TimelineLabel.qml b/imports/Spectral/Component/Timeline/TimelineLabel.qml deleted file mode 100644 index f64751f..0000000 --- a/imports/Spectral/Component/Timeline/TimelineLabel.qml +++ /dev/null @@ -1,17 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Controls.Material 2.2 - -import Spectral.Setting 0.1 - -Label { - property bool coloredBackground - - color: coloredBackground ? "white": Material.foreground - - wrapMode: Label.Wrap - linkColor: coloredBackground ? "white" : Material.accent - textFormat: Text.RichText - - onLinkActivated: Qt.openUrlExternally(link) -} diff --git a/imports/Spectral/Component/Timeline/qmldir b/imports/Spectral/Component/Timeline/qmldir index 815bdd5..527b337 100644 --- a/imports/Spectral/Component/Timeline/qmldir +++ b/imports/Spectral/Component/Timeline/qmldir @@ -1,4 +1,6 @@ module Spectral.Component.Timeline MessageDelegate 2.0 MessageDelegate.qml StateDelegate 2.0 StateDelegate.qml - +SectionDelegate 2.0 SectionDelegate.qml +ImageDelegate 2.0 ImageDelegate.qml +FileDelegate 2.0 FileDelegate.qml diff --git a/imports/Spectral/Component/qmldir b/imports/Spectral/Component/qmldir index e051620..be035c4 100644 --- a/imports/Spectral/Component/qmldir +++ b/imports/Spectral/Component/qmldir @@ -6,3 +6,4 @@ ScrollHelper 2.0 ScrollHelper.qml AutoListView 2.0 AutoListView.qml AutoTextField 2.0 AutoTextField.qml SplitView 2.0 SplitView.qml +Avatar 2.0 Avatar.qml diff --git a/imports/Spectral/Effect/CircleMask.qml b/imports/Spectral/Effect/CircleMask.qml new file mode 100644 index 0000000..184861e --- /dev/null +++ b/imports/Spectral/Effect/CircleMask.qml @@ -0,0 +1,29 @@ +import QtQuick 2.12 +import QtGraphicalEffects 1.0 + +Item { + id: item + + property alias source: mask.source + + Rectangle { + id: circleMask + + width: parent.width + height: parent.height + + smooth: true + visible: false + + radius: Math.max(width/2, height/2) + } + + OpacityMask { + id: mask + + width: parent.width + height: parent.height + + maskSource: circleMask + } +} diff --git a/imports/Spectral/Effect/ElevationEffect.qml b/imports/Spectral/Effect/ElevationEffect.qml index ca2a4c6..0299fd5 100644 --- a/imports/Spectral/Effect/ElevationEffect.qml +++ b/imports/Spectral/Effect/ElevationEffect.qml @@ -1,4 +1,4 @@ -import QtQuick 2.9 +import QtQuick 2.12 import QtGraphicalEffects 1.0 /*! @@ -7,7 +7,8 @@ import QtGraphicalEffects 1.0 Item { id: effect - property variant source + property var source + readonly property Item sourceItem: source.sourceItem property int elevation: 0 @@ -134,7 +135,7 @@ Item { glowRadius: modelData.blur/2 spread: 0.05 color: _shadowColors[index] - cornerRadius: modelData.blur + (effect.source.radius || 0) + cornerRadius: modelData.blur + (effect.sourceItem.radius || 0) } } diff --git a/imports/Spectral/Effect/RippleEffect.qml b/imports/Spectral/Effect/RippleEffect.qml new file mode 100644 index 0000000..33a002f --- /dev/null +++ b/imports/Spectral/Effect/RippleEffect.qml @@ -0,0 +1,239 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.0 + +import Spectral.Component 2.0 +import Spectral.Setting 0.1 + +AutoMouseArea { + id: ripple + + property color color: MSettings.darkTheme ? Qt.rgba(255, 255, 255, 0.16) : Qt.rgba(0, 0, 0, 0.08) + property bool circular: false + property bool centered: false + property bool focused + property color focusColor: "transparent" + property int focusWidth: width - 32 + property Item control + + clip: true + + Connections { + target: control + + onPressedChanged: { + if (!control.pressed) + __private.removeLastCircle() + } + } + + onPressed: { + __private.createTapCircle(mouse.x, mouse.y) + + if (control) + mouse.accepted = false + } + + onReleased: __private.removeLastCircle() + onCanceled: __private.removeLastCircle() + + QtObject { + id: __private + + property int startRadius: 0 + property int endRadius + property bool showFocus: true + + property Item lastCircle + + function createTapCircle(x, y) { + endRadius = centered ? width/2 : radius(x, y) + 5 + showFocus = false + + lastCircle = tapCircle.createObject(ripple, { + "circleX": centered ? width/2 : x, + "circleY": centered ? height/2 : y + }) + } + + function removeLastCircle() { + if (lastCircle) + lastCircle.removeCircle() + } + + function radius(x, y) { + var dist1 = Math.max(dist(x, y, 0, 0), dist(x, y, width, height)) + var dist2 = Math.max(dist(x, y, width, 0), dist(x, y, 0, height)) + + return Math.max(dist1, dist2) + } + + function dist(x1, y1, x2, y2) { + var distX = x2 - x1 + var distY = y2 - y1 + + return Math.sqrt(distX * distX + distY * distY) + } + } + + Rectangle { + id: focusBackground + objectName: "focusBackground" + + width: parent.width + height: parent.height + + color: Qt.rgba(0,0,0,0.2) + + opacity: __private.showFocus && focused ? 1 : 0 + + Behavior on opacity { + NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } + } + } + + Rectangle { + id: focusCircle + objectName: "focusRipple" + + property bool focusedState + + x: (parent.width - width)/2 + y: (parent.height - height)/2 + + width: focused + ? focusedState ? focusWidth + : Math.min(parent.width - 8, focusWidth + 12) + : parent.width/5 + height: width + + radius: width/2 + + opacity: __private.showFocus && focused ? 1 : 0 + + color: focusColor.a === 0 ? Qt.rgba(1,1,1,0.4) : focusColor + + Behavior on opacity { + NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } + } + + Behavior on width { + NumberAnimation { duration: focusTimer.interval; } + } + + Timer { + id: focusTimer + running: focused + repeat: true + interval: 800 + + onTriggered: focusCircle.focusedState = !focusCircle.focusedState + } + } + + Component { + id: tapCircle + + Item { + id: circleItem + objectName: "tapRipple" + + property bool done + + property real circleX + property real circleY + + property bool closed + + width: parent.width + height: parent.height + + function removeCircle() { + done = true + + if (fillSizeAnimation.running) { + fillOpacityAnimation.stop() + closeAnimation.start() + + circleItem.destroy(500); + } else { + __private.showFocus = true + fadeAnimation.start(); + + circleItem.destroy(300); + } + } + + Item { + id: circleParent + + width: parent.width + height: parent.height + + visible: !circular + + Rectangle { + id: circleRectangle + + x: circleItem.circleX - radius + y: circleItem.circleY - radius + + width: radius * 2 + height: radius * 2 + + opacity: 0 + color: ripple.color + + NumberAnimation { + id: fillSizeAnimation + running: true + + target: circleRectangle; property: "radius"; duration: 500; + from: __private.startRadius; to: __private.endRadius; + easing.type: Easing.InOutQuad + + onStopped: { + if (done) + __private.showFocus = true + } + } + + NumberAnimation { + id: fillOpacityAnimation + running: true + + target: circleRectangle; property: "opacity"; duration: 300; + from: 0; to: 1; easing.type: Easing.InOutQuad + } + + NumberAnimation { + id: fadeAnimation + + target: circleRectangle; property: "opacity"; duration: 300; + from: 1; to: 0; easing.type: Easing.InOutQuad + } + + SequentialAnimation { + id: closeAnimation + + NumberAnimation { + target: circleRectangle; property: "opacity"; duration: 250; + to: 1; easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: circleRectangle; property: "opacity"; duration: 250; + from: 1; to: 0; easing.type: Easing.InOutQuad + } + } + } + } + + CircleMask { + anchors.fill: parent + source: circleParent + visible: circular + } + } + } +} diff --git a/imports/Spectral/Effect/qmldir b/imports/Spectral/Effect/qmldir index b4c470e..89f10e4 100644 --- a/imports/Spectral/Effect/qmldir +++ b/imports/Spectral/Effect/qmldir @@ -1,2 +1,3 @@ module Spectral.Effect ElevationEffect 2.0 ElevationEffect.qml +RippleEffect 2.0 RippleEffect.qml diff --git a/imports/Spectral/Font/CommonFont.qml b/imports/Spectral/Font/CommonFont.qml new file mode 100644 index 0000000..6205c2e --- /dev/null +++ b/imports/Spectral/Font/CommonFont.qml @@ -0,0 +1,5 @@ +pragma Singleton +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Label {} diff --git a/imports/Spectral/Font/MaterialFont.qml b/imports/Spectral/Font/MaterialFont.qml index 312fe23..c1a3d66 100644 --- a/imports/Spectral/Font/MaterialFont.qml +++ b/imports/Spectral/Font/MaterialFont.qml @@ -1,5 +1,5 @@ pragma Singleton -import QtQuick 2.9 +import QtQuick 2.12 FontLoader { source: "qrc:/assets/font/material.ttf" diff --git a/imports/Spectral/Font/qmldir b/imports/Spectral/Font/qmldir index 57fb41f..bdd2daa 100644 --- a/imports/Spectral/Font/qmldir +++ b/imports/Spectral/Font/qmldir @@ -1,3 +1,3 @@ module Spectral.Font singleton MaterialFont 0.1 MaterialFont.qml - +singleton CommonFont 0.1 CommonFont.qml diff --git a/imports/Spectral/Menu/MessageContextMenu.qml b/imports/Spectral/Menu/MessageContextMenu.qml deleted file mode 100644 index 973e428..0000000 --- a/imports/Spectral/Menu/MessageContextMenu.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 - -Menu { - property var row: null - property var model: null - property string selectedText - - readonly property bool isFile: model && (model.eventType === "video" || model.eventType === "audio" || model.eventType === "file" || model.eventType === "image") - - id: messageContextMenu - - MenuItem { - text: "View Source" - - onTriggered: { - sourceDialog.sourceText = model.toolTip - sourceDialog.open() - } - } - MenuItem { - visible: isFile - height: visible ? undefined : 0 - text: "Open Externally" - - onTriggered: row.openExternally() - } - MenuItem { - visible: isFile - height: visible ? undefined : 0 - text: "Save As" - - onTriggered: row.saveFileAs() - } - MenuItem { - height: visible ? undefined : 0 - text: "Reply" - - onTriggered: { - roomPanelInput.isReply = true - roomPanelInput.replyUserID = model.author.id - roomPanelInput.replyEventID = model.eventId - roomPanelInput.replyContent = selectedText != "" ? selectedText : model.message - } - } - MenuItem { - visible: model && model.author === currentRoom.localUser - height: visible ? undefined : 0 - text: "Redact" - - onTriggered: currentRoom.redactEvent(model.eventId) - } -} diff --git a/imports/Spectral/Menu/RoomContextMenu.qml b/imports/Spectral/Menu/RoomContextMenu.qml deleted file mode 100644 index 8c5b1a6..0000000 --- a/imports/Spectral/Menu/RoomContextMenu.qml +++ /dev/null @@ -1,35 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import Spectral 0.1 - -Menu { - property var model: null - - id: roomListMenu - - MenuItem { - text: "Favourite" - checkable: true - checked: model && model.category === RoomType.Favorite - - onTriggered: model.category === RoomType.Favorite ? model.currentRoom.removeTag("m.favourite") : model.currentRoom.addTag("m.favourite", 1.0) - } - MenuItem { - text: "Deprioritize" - checkable: true - checked: model && model.category === RoomType.Deprioritized - - onTriggered: model.category === RoomType.Deprioritized ? model.currentRoom.removeTag("m.lowpriority") : model.currentRoom.addTag("m.lowpriority", 1.0) - } - MenuSeparator {} - MenuItem { - text: "Mark as Read" - - onTriggered: model.currentRoom.markAllMessagesAsRead() - } - MenuItem { - text: "Leave Room" - - onTriggered: model.currentRoom.forget() - } -} diff --git a/imports/Spectral/Menu/qmldir b/imports/Spectral/Menu/qmldir deleted file mode 100644 index 6892ba8..0000000 --- a/imports/Spectral/Menu/qmldir +++ /dev/null @@ -1,3 +0,0 @@ -module Spectral.Menu -MessageContextMenu 2.0 MessageContextMenu.qml -RoomContextMenu 2.0 RoomContextMenu.qml diff --git a/imports/Spectral/Page/Login.qml b/imports/Spectral/Page/Login.qml deleted file mode 100644 index fbf5a06..0000000 --- a/imports/Spectral/Page/Login.qml +++ /dev/null @@ -1,27 +0,0 @@ -import QtQuick 2.9 - -LoginForm { - loginButton.onClicked: doLogin() - - Shortcut { - sequence: "Return" - onActivated: doLogin() - } - - function doLogin() { - if (!(serverField.text.startsWith("http") && serverField.text.includes("://"))) { - loginButtonTooltip.text = "Server address should start with http(s)://" - loginButtonTooltip.open() - return - } - - loginButton.text = "Logging in..." - loginButton.enabled = false - controller.loginWithCredentials(serverField.text, usernameField.text, passwordField.text) - - controller.connectionAdded.connect(function(conn) { - stackView.pop() - accountListView.currentConnection = conn - }) - } -} diff --git a/imports/Spectral/Page/LoginForm.ui.qml b/imports/Spectral/Page/LoginForm.ui.qml deleted file mode 100644 index 2099ebf..0000000 --- a/imports/Spectral/Page/LoginForm.ui.qml +++ /dev/null @@ -1,156 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 -import QtQuick.Controls 2.2 -import QtQuick.Controls.Material 2.2 - -import Spectral.Component 2.0 - -import Spectral.Setting 0.1 - -Page { - property var controller - - property alias loginButton: loginButton - property alias serverField: serverField - property alias usernameField: usernameField - property alias passwordField: passwordField - property alias loginButtonTooltip: loginButtonTooltip - - Row { - anchors.fill: parent - - Pane { - width: parent.width / 2 - height: parent.height - - background: Item { - Image { - id: background - anchors.fill: parent - source: "qrc:/assets/img/background.jpg" - fillMode: Image.PreserveAspectCrop - cache: false - } - - ColorOverlay { - anchors.fill: background - source: background - color: Material.accent - opacity: 0.7 - } - } - - Column { - x: 32 - anchors.verticalCenter: parent.verticalCenter - - Label { - text: "MATRIX LOGIN." - font.pointSize: 28 - font.bold: true - color: "white" - } - - Label { - text: "A NEW METHOD OF MESSAGING" - font.pointSize: 12 - color: "white" - } - } - } - - Pane { - width: parent.width / 2 - height: parent.height - - padding: 64 - - ColumnLayout { - width: parent.width - - id: mainCol - - AutoTextField { - Layout.fillWidth: true - - id: serverField - - leftPadding: 16 - topPadding: 0 - bottomPadding: 0 - - text: "https://matrix.org" - placeholderText: "Server" - - background: Rectangle { - implicitHeight: 48 - - color: MSettings.darkTheme ? "#242424" : "#eaeaea" - border.color: parent.activeFocus ? Material.accent : "transparent" - border.width: 2 - } - } - - AutoTextField { - Layout.fillWidth: true - - id: usernameField - - leftPadding: 16 - topPadding: 0 - bottomPadding: 0 - - placeholderText: "Username" - - background: Rectangle { - implicitHeight: 48 - - color: MSettings.darkTheme ? "#242424" : "#eaeaea" - border.color: parent.activeFocus ? Material.accent : "transparent" - border.width: 2 - } - } - - AutoTextField { - Layout.fillWidth: true - - id: passwordField - - leftPadding: 16 - topPadding: 0 - bottomPadding: 0 - - placeholderText: "Password" - echoMode: TextInput.Password - - background: Rectangle { - implicitHeight: 48 - - color: MSettings.darkTheme ? "#242424" : "#eaeaea" - border.color: parent.activeFocus ? Material.accent : "transparent" - border.width: 2 - } - } - - Button { - Layout.fillWidth: true - - id: loginButton - - text: "LOGIN" - highlighted: true - - ToolTip { - id: loginButtonTooltip - } - } - } - } - } -} - -/*##^## Designer { - D{i:0;autoSize:true;height:480;width:640} -} - ##^##*/ diff --git a/imports/Spectral/Page/Room.qml b/imports/Spectral/Page/Room.qml deleted file mode 100644 index 033555c..0000000 --- a/imports/Spectral/Page/Room.qml +++ /dev/null @@ -1,67 +0,0 @@ -import QtQuick 2.9 - -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 - -import Spectral.Panel 2.0 -import Spectral.Component 2.0 -import Spectral.Effect 2.0 - -import Spectral 0.1 -import Spectral.Setting 0.1 - -Page { - property alias connection: roomListModel.connection - property alias enteredRoom: roomListForm.enteredRoom - property alias filter: roomListForm.filter - - id: page - - RoomListModel { - id: roomListModel - - onNewMessage: if (!window.active) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon, iconPath) - } - - SplitView { - anchors.fill: parent - - RoomListPanel { - width: page.width * 0.35 - Layout.minimumWidth: 64 - - id: roomListForm - - listModel: roomListModel - - onWidthChanged: { - if (width < 240) width = 64 - } - - ElevationEffect { - anchors.fill: source - z: source.z - 1 - - source: parent - elevation: 4 - } - - onLeaveRoom: roomForm.saveReadMarker(room) - } - - RoomPanel { - Layout.fillWidth: true - Layout.minimumWidth: 480 - - id: roomForm - - currentRoom: roomListForm.enteredRoom - } - } - - function goToEvent(eventID) { - roomForm.goToEvent(eventID) - } -} diff --git a/imports/Spectral/Page/Setting.qml b/imports/Spectral/Page/Setting.qml deleted file mode 100644 index 0385eaf..0000000 --- a/imports/Spectral/Page/Setting.qml +++ /dev/null @@ -1,5 +0,0 @@ -import QtQuick 2.9 - -SettingForm { - addAccountButton.onClicked: stackView.push(loginPage) -} diff --git a/imports/Spectral/Page/SettingAccountDelegate.qml b/imports/Spectral/Page/SettingAccountDelegate.qml deleted file mode 100644 index 62441a3..0000000 --- a/imports/Spectral/Page/SettingAccountDelegate.qml +++ /dev/null @@ -1,135 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 - -import Spectral.Component 2.0 - -import Spectral 0.1 -import Spectral.Setting 0.1 - -Column { - property bool expanded: false - - spacing: 8 - - ItemDelegate { - width: accountSettingsListView.width - height: 64 - - Row { - anchors.fill: parent - anchors.margins: 8 - - spacing: 8 - - ImageItem { - width: parent.height - height: parent.height - - hint: user.displayName - source: user.paintable - } - - ColumnLayout { - Label { - text: user.displayName - } - Label { - text: user.id - } - } - } - - onClicked: expanded = !expanded - } - - ColumnLayout { - width: parent.width - 32 - height: expanded ? implicitHeight : 0 - anchors.horizontalCenter: parent.horizontalCenter - - spacing: 0 - - clip: true - - AutoListView { - Layout.fillWidth: true - Layout.preferredHeight: 24 - - orientation: ListView.Horizontal - - spacing: 8 - - model: ["#498882", "#42a5f5", "#5c6bc0", "#7e57c2", "#ab47bc", "#ff7043"] - - delegate: Rectangle { - width: parent.height - height: parent.height - radius: width / 2 - - color: modelData - - MouseArea { - anchors.fill: parent - - onClicked: spectralController.setColor(connection.localUserId, modelData) - } - } - } - - RowLayout { - Layout.fillWidth: true - - Label { - text: "Homeserver:" - } - AutoTextField { - Layout.fillWidth: true - - text: connection.homeserver - readOnly: true - } - } - - RowLayout { - Layout.fillWidth: true - - spacing: 16 - - Label { - text: "Device ID:" - } - AutoTextField { - Layout.fillWidth: true - - text: connection.deviceId - readOnly: true - } - } - - RowLayout { - Layout.fillWidth: true - - spacing: 16 - - Label { - text: "Access Token:" - } - AutoTextField { - Layout.fillWidth: true - - text: connection.accessToken - readOnly: true - } - } - - Button { - Layout.fillWidth: true - - highlighted: true - text: "Logout" - - onClicked: spectralController.logout(connection) - } - } -} diff --git a/imports/Spectral/Page/SettingCategoryDelegate.qml b/imports/Spectral/Page/SettingCategoryDelegate.qml deleted file mode 100644 index e78bfbb..0000000 --- a/imports/Spectral/Page/SettingCategoryDelegate.qml +++ /dev/null @@ -1,11 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 - -ItemDelegate { - text: category - - onClicked: { - settingStackView.clear() - settingStackView.push([accountForm, generalForm, appearanceForm, aboutForm][form]) - } -} diff --git a/imports/Spectral/Page/SettingForm.ui.qml b/imports/Spectral/Page/SettingForm.ui.qml deleted file mode 100644 index 5768451..0000000 --- a/imports/Spectral/Page/SettingForm.ui.qml +++ /dev/null @@ -1,185 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Controls.Material 2.2 -import QtQuick.Layouts 1.3 - -import Spectral.Component 2.0 -import Spectral.Effect 2.0 - -import Spectral 0.1 -import Spectral.Setting 0.1 - -Page { - property alias listModel: accountSettingsListView.model - - property alias addAccountButton: addAccountButton - - implicitWidth: 400 - implicitHeight: 300 - - Page { - id: accountForm - - parent: null - - padding: 64 - - ColumnLayout { - anchors.fill: parent - - AutoListView { - Layout.fillWidth: true - Layout.fillHeight: true - - id: accountSettingsListView - - boundsBehavior: Flickable.DragOverBounds - - clip: true - - delegate: SettingAccountDelegate {} - } - - Button { - Layout.fillWidth: true - - id: addAccountButton - - text: "Add Account" - flat: true - highlighted: true - } - } - } - - Page { - id: generalForm - - parent: null - - padding: 64 - - Column { - Switch { - text: "Use press and hold instead of right click" - checked: MSettings.pressAndHold - - onCheckedChanged: MSettings.pressAndHold = checked - } - - Switch { - text: "Show tray icon" - checked: MSettings.showTray - - onCheckedChanged: MSettings.showTray = checked - } - - Switch { - text: "Confirm on Exit" - checked: MSettings.confirmOnExit - - onCheckedChanged: MSettings.confirmOnExit = checked - } - } - } - - Page { - id: appearanceForm - - parent: null - - padding: 64 - - Column { - Switch { - text: "Dark theme" - checked: MSettings.darkTheme - - onCheckedChanged: MSettings.darkTheme = checked - } - } - } - - Page { - id: aboutForm - - parent: null - - padding: 64 - - ColumnLayout { - spacing: 16 - Image { - Layout.preferredWidth: 64 - Layout.preferredHeight: 64 - - source: "qrc:/assets/img/icon.png" - } - Label { - text: "Spectral, an IM client for the Matrix protocol." - } - Label { - text: "Released under GNU General Public License, version 3." - } - } - } - - Rectangle { - width: 240 - height: parent.height - z: 10 - - id: settingDrawer - - color: MSettings.darkTheme ? "#323232" : "#f3f3f3" - - layer.enabled: true - layer.effect: ElevationEffect { - elevation: 4 - } - - Column { - anchors.fill: parent - - Repeater { - model: ListModel { - ListElement { - category: "Accounts" - form: 0 - } - ListElement { - category: "General" - form: 1 - } - ListElement { - category: "Appearance" - form: 2 - } - ListElement { - category: "About" - form: 3 - } - } - - delegate: SettingCategoryDelegate { - width: parent.width - } - } - } - } - - StackView { - anchors.fill: parent - anchors.leftMargin: settingDrawer.width - - id: settingStackView - - initialItem: aboutForm - } -} - - -/*##^## Designer { - D{i:0;autoSize:true;height:480;width:640} -} - ##^##*/ diff --git a/imports/Spectral/Panel/RoomDrawer.qml b/imports/Spectral/Panel/RoomDrawer.qml index dc07bbd..475c5f2 100644 --- a/imports/Spectral/Panel/RoomDrawer.qml +++ b/imports/Spectral/Panel/RoomDrawer.qml @@ -1,39 +1,30 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Controls.Material 2.2 -import QtQuick.Layouts 1.3 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.Material 2.12 +import QtQuick.Layouts 1.12 import Spectral.Component 2.0 import Spectral 0.1 -import "qrc:/js/util.js" as Util - Drawer { property var room id: roomDrawer edge: Qt.RightEdge - interactive: false - - ToolButton { - contentItem: MaterialIcon { icon: "\ue5c4" } - - onClicked: roomDrawer.close() - } ColumnLayout { anchors.fill: parent anchors.margins: 32 - ImageItem { + Avatar { Layout.preferredWidth: 96 Layout.preferredHeight: 96 Layout.alignment: Qt.AlignHCenter hint: room ? room.displayName : "No name" - source: room ? room.paintable : null + source: room ? room.avatarMediaId : null } Label { @@ -57,7 +48,7 @@ Drawer { wrapMode: Label.Wrap horizontalAlignment: Text.AlignHCenter - text: room ? room.memberCount + " Members" : "No Member Count" + text: room ? room.totalMemberCount + " Members" : "No Member Count" } RowLayout { @@ -124,11 +115,11 @@ Drawer { anchors.margins: 8 spacing: 12 - ImageItem { + Avatar { Layout.preferredWidth: height Layout.fillHeight: true - source: paintable + source: avatar hint: name } diff --git a/imports/Spectral/Panel/RoomHeader.qml b/imports/Spectral/Panel/RoomHeader.qml index a0a13b1..1933426 100644 --- a/imports/Spectral/Panel/RoomHeader.qml +++ b/imports/Spectral/Panel/RoomHeader.qml @@ -1,80 +1,81 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 import Spectral 0.1 +import Spectral.Effect 2.0 +import Spectral.Component 2.0 +import Spectral.Setting 0.1 -Rectangle { - property alias paintable: headerImage.source +Control { + property alias avatar: headerImage.source property alias topic: headerTopicLabel.text + property bool atTop: false signal clicked() id: header - color: Material.accent + background: Rectangle { + color: Material.background - ItemDelegate { + opacity: atTop ? 0 : 1 + + layer.enabled: true + layer.effect: ElevationEffect { + elevation: 2 + } + } + + RowLayout { anchors.fill: parent + anchors.margins: 12 - id: roomHeader + spacing: 12 - onClicked: header.clicked() + Avatar { + Layout.preferredWidth: height + Layout.fillHeight: true - RowLayout { - anchors.fill: parent - anchors.margins: 12 + id: headerImage - spacing: 12 + source: currentRoom.avatarMediaId + hint: currentRoom ? currentRoom.displayName : "No name" + } - ImageItem { - Layout.preferredWidth: height - Layout.fillHeight: true + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true - id: headerImage + visible: parent.width > 64 - source: currentRoom.paintable - hint: currentRoom ? currentRoom.displayName : "No name" - } - - ColumnLayout { + Label { Layout.fillWidth: true Layout.fillHeight: true - visible: parent.width > 64 + text: currentRoom ? currentRoom.displayName : "" + color: MPalette.foreground + font.pixelSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } - Label { - Layout.fillWidth: true - Layout.fillHeight: true + Label { + Layout.fillWidth: true + Layout.fillHeight: true - text: currentRoom ? currentRoom.displayName : "" - color: "white" - font.pointSize: 12 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } + id: headerTopicLabel - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - id: headerTopicLabel - - color: "white" - elide: Text.ElideRight - wrapMode: Text.NoWrap - } + color: MPalette.lighter + elide: Text.ElideRight + wrapMode: Text.NoWrap } } } - ProgressBar { - width: parent.width - z: 10 - anchors.bottom: parent.bottom + RippleEffect { + anchors.fill: parent - Material.accent: "white" - visible: currentRoom && currentRoom.busy - indeterminate: true + onClicked: header.clicked() } } diff --git a/imports/Spectral/Panel/RoomListDelegate.qml b/imports/Spectral/Panel/RoomListDelegate.qml deleted file mode 100644 index 8268e5d..0000000 --- a/imports/Spectral/Panel/RoomListDelegate.qml +++ /dev/null @@ -1,100 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 - -import Spectral 0.1 -import Spectral.Setting 0.1 - -import Spectral.Component 2.0 - -Rectangle { - color: MSettings.darkTheme ? "#303030" : "#fafafa" - - AutoMouseArea { - anchors.fill: parent - - hoverEnabled: miniMode - - onSecondaryClicked: { - roomContextMenu.model = model - roomContextMenu.popup() - } - onPrimaryClicked: { - if (category === RoomType.Invited) { - inviteDialog.currentRoom = currentRoom - inviteDialog.open() - } else { - leaveRoom(enteredRoom) - enterRoom(currentRoom) - enteredRoom = currentRoom - } - } - - ToolTip.visible: miniMode && containsMouse - ToolTip.text: name - } - - Rectangle { - anchors.fill: parent - - visible: highlightCount > 0 || currentRoom === enteredRoom - color: Material.accent - opacity: 0.1 - } - - Rectangle { - width: unreadCount > 0 ? 4 : 0 - height: parent.height - - color: Material.accent - - Behavior on width { - PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 } - } - } - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - - spacing: 12 - - ImageItem { - id: imageItem - - Layout.preferredWidth: height - Layout.fillHeight: true - - source: paintable - hint: name || "No Name" - } - - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignHCenter - - visible: parent.width > 64 - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: name || "No Name" - font.pointSize: 12 - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - - text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm,"") - elide: Text.ElideRight - wrapMode: Text.NoWrap - } - } - } -} diff --git a/imports/Spectral/Panel/RoomListPanel.qml b/imports/Spectral/Panel/RoomListPanel.qml index 6472f4a..2cf0025 100644 --- a/imports/Spectral/Panel/RoomListPanel.qml +++ b/imports/Spectral/Panel/RoomListPanel.qml @@ -1,15 +1,42 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 + +import Spectral.Component 2.0 +import Spectral.Effect 2.0 + +import Spectral 0.1 +import Spectral.Setting 0.1 import SortFilterProxyModel 0.2 -RoomListPanelForm { - model: sortedRoomListModel +Item { + property var controller: null + readonly property var user: controller.connection ? controller.connection.localUser : null + + property int filter: 0 + property var enteredRoom: null + property alias errorControl: errorControl + + signal enterRoom(var room) + signal leaveRoom(var room) + + id: root + + RoomListModel { + id: roomListModel + + connection: controller.connection + + onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon) + } SortFilterProxyModel { id: sortedRoomListModel - sourceModel: listModel + sourceModel: roomListModel proxyRoles: ExpressionRole { name: "display" @@ -53,9 +80,841 @@ RoomListPanelForm { ] } - Shortcut { - sequence: StandardKey.Find - onActivated: searchField.forceActiveFocus() + Drawer { + width: Math.max(root.width, 400) + height: root.height + + id: drawer + + edge: Qt.LeftEdge + + Component { + id: mainPage + + ColumnLayout { + readonly property string title: "Main" + + id: mainColumn + + spacing: 0 + + Control { + Layout.fillWidth: true + Layout.preferredHeight: 330 + + padding: 24 + + contentItem: ColumnLayout { + spacing: 4 + + Avatar { + Layout.preferredWidth: 200 + Layout.preferredHeight: 200 + Layout.margins: 12 + Layout.alignment: Qt.AlignHCenter + + source: root.user ? root.user.avatarMediaId : null + hint: root.user ? root.user.displayName : "?" + } + + Label { + Layout.alignment: Qt.AlignHCenter + + text: root.user ? root.user.displayName : "No Name" + color: "white" + font.pixelSize: 22 + } + + Label { + Layout.alignment: Qt.AlignHCenter + + text: root.user ? root.user.id : "@example:matrix.org" + color: "white" + opacity: 0.7 + font.pixelSize: 13 + } + } + + background: Rectangle { color: Material.primary } + + RippleEffect { + anchors.fill: parent + + onClicked: stackView.push(userPage) + } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ColumnLayout { + width: mainColumn.width + spacing: 0 + + Repeater { + model: AccountListModel { + controller: spectralController + } + + delegate: ItemDelegate { + Layout.fillWidth: true + + text: user.displayName + + onClicked: { + controller.connection = connection + drawer.close() + } + } + } + + ItemDelegate { + Layout.fillWidth: true + + text: "Add Account" + + onClicked: loginDialog.open() + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + + color: MSettings.darkTheme ? "#424242" : "#e7ebeb" + } + + ItemDelegate { + Layout.fillWidth: true + + text: "Settings" + + onClicked: stackView.push(settingsPage) + } + + ItemDelegate { + Layout.fillWidth: true + + text: "Logout" + + onClicked: controller.logout(controller.connection) + } + + ItemDelegate { + Layout.fillWidth: true + + text: "Exit" + + onClicked: Qt.quit() + } + } + } + } + } + + Component { + id: userPage + + ScrollView { + readonly property string title: "User Info" + + id: main + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ColumnLayout { + width: main.width + spacing: 0 + + ItemDelegate { + Layout.fillWidth: true + + padding: 24 + + contentItem: ColumnLayout { + spacing: 0 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: "Matrix ID" + font.pixelSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: root.user.id + color: "#5B7480" + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + + ItemDelegate { + Layout.fillWidth: true + + padding: 24 + + contentItem: ColumnLayout { + spacing: 0 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: "Name" + font.pixelSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: root.user.name + color: "#5B7480" + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + + ItemDelegate { + Layout.fillWidth: true + + padding: 24 + + contentItem: ColumnLayout { + spacing: 0 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: "Avatar" + font.pixelSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: root.user.avatarMediaId + color: "#5B7480" + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + + ItemDelegate { + Layout.fillWidth: true + + padding: 24 + + contentItem: ColumnLayout { + spacing: 0 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: "Server" + font.pixelSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: root.controller.connection.accessToken + color: "#5B7480" + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + + ItemDelegate { + Layout.fillWidth: true + + padding: 24 + + contentItem: ColumnLayout { + spacing: 0 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: "Device" + font.pixelSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: root.controller.connection.deviceId + color: "#5B7480" + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + + ItemDelegate { + Layout.fillWidth: true + + padding: 24 + + contentItem: ColumnLayout { + spacing: 0 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: "Token" + font.pixelSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: root.controller.connection.accessToken + color: "#5B7480" + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + } + } + } + + Component { + id: settingsPage + + ScrollView { + readonly property string title: "Settings" + + id: main + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + padding: 32 + + ColumnLayout { + width: main.width - 64 + spacing: 0 + + Switch { + text: "Dark theme" + checked: MSettings.darkTheme + + onCheckedChanged: MSettings.darkTheme = checked + } + + Switch { + text: "Show notifications" + checked: MSettings.showNotification + + onCheckedChanged: MSettings.showNotification = checked + } + + Switch { + text: "Use press and hold instead of right click" + checked: MSettings.pressAndHold + + onCheckedChanged: MSettings.pressAndHold = checked + } + + Switch { + text: "Show tray icon" + checked: MSettings.showTray + + onCheckedChanged: MSettings.showTray = checked + } + + Switch { + text: "Enable timeline background" + checked: MSettings.enableTimelineBackground + + onCheckedChanged: MSettings.enableTimelineBackground = checked + } + + RowLayout { + Layout.fillWidth: true + + Label { + text: "DPI" + } + + Slider { + Layout.fillWidth: true + + value: controller.dpi() + from: 100 + to: 300 + stepSize: 25 + snapMode: Slider.SnapAlways + + ToolTip.visible: pressed + ToolTip.text: value + + onMoved: controller.setDpi(value) + } + } + } + } + } + + ColumnLayout { + anchors.fill: parent + + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 64 + + visible: stackView.depth > 1 + + color: Material.primary + + RowLayout { + anchors.fill: parent + anchors.margins: 4 + + ToolButton { + Layout.preferredWidth: height + Layout.fillHeight: true + + contentItem: MaterialIcon { + icon: "\ue5c4" + color: "white" + } + + onClicked: stackView.pop() + } + Label { + Layout.fillWidth: true + + text: stackView.currentItem.title + color: "white" + font.pixelSize: 18 + elide: Label.ElideRight + } + } + } + + StackView { + Layout.fillWidth: true + Layout.fillHeight: true + + id: stackView + + clip: true + + initialItem: mainPage + } + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Control { + Layout.fillWidth: true + Layout.preferredHeight: 64 + + id: roomListHeader + + topPadding: 12 + bottomPadding: 12 + leftPadding: 12 + rightPadding: 18 + + contentItem: RowLayout { + ItemDelegate { + Layout.preferredWidth: height + Layout.fillHeight: true + + visible: !searchField.active + + contentItem: MaterialIcon { + icon: { + switch (filter) { + case 0: return "\ue8b6" + case 1: return "\ue7f5" + case 2: return "\ue7ff" + case 3: return "\ue7fc" + } + } + } + + Menu { + id: filterMenu + + MenuItem { + text: "All" + + onClicked: filter = 0 + } + + MenuSeparator {} + + MenuItem { + text: "New" + + onClicked: filter = 1 + } + + MenuItem { + text: "People" + + onClicked: filter = 2 + } + + MenuItem { + text: "Group" + + onClicked: filter = 3 + } + } + + onClicked: filterMenu.popup() + } + + ItemDelegate { + Layout.preferredWidth: height + Layout.fillHeight: true + + visible: searchField.active + + contentItem: MaterialIcon { icon: "\ue5cd" } + + onClicked: searchField.clear() + } + + AutoTextField { + readonly property bool active: text + readonly property bool isRoom: text.match(/#.*:.*\..*/g) || text.match(/!.*:.*\..*/g) + readonly property bool isUser: text.match(/@.*:.*\..*/g) + + Layout.fillWidth: true + Layout.fillHeight: true + + id: searchField + + topPadding: 0 + bottomPadding: 0 + placeholderText: "Search..." + color: MPalette.lighter + + background: Item {} + } + + ItemDelegate { + Layout.preferredWidth: height + Layout.fillHeight: true + + visible: searchField.isRoom || searchField.isUser + + contentItem: MaterialIcon { icon: "\ue145" } + + onClicked: { + if (searchField.isRoom) { + controller.joinRoom(controller.connection, searchField.text) + return + } + if (searchField.isUser) { + controller.createDirectChat(controller.connection, searchField.text) + return + } + } + } + + Avatar { + Layout.preferredWidth: height + Layout.fillHeight: true + Layout.alignment: Qt.AlignRight + + visible: !searchField.active + + source: root.user ? root.user.avatarMediaId : null + hint: root.user ? root.user.displayName : "?" + + MouseArea { + anchors.fill: parent + onClicked: drawer.open() + } + } + } + + background: Rectangle { + color: Material.background + + opacity: listView.atYBeginning ? 0 : 1 + + layer.enabled: true + layer.effect: ElevationEffect { + elevation: 2 + } + } + } + + Control { + property string error: "" + property string detail: "" + + Layout.fillWidth: true + + id: errorControl + + visible: false + + topPadding: 16 + bottomPadding: 16 + leftPadding: 24 + rightPadding: 24 + + contentItem: ColumnLayout { + Label { + Layout.fillWidth: true + + text: errorControl.error + font.pixelSize: 16 + color: "white" + wrapMode: Text.Wrap + } + Label { + Layout.fillWidth: true + + text: errorControl.detail + font.pixelSize: 14 + color: "white" + opacity: 0.6 + wrapMode: Text.Wrap + } + } + + background: Rectangle { + color: "#273338" + } + + RippleEffect { + anchors.fill: parent + + onClicked: errorControl.visible = false + } + } + + AutoListView { + Layout.fillWidth: true + Layout.fillHeight: true + + id: listView + + spacing: 0 + clip: true + + model: sortedRoomListModel + + boundsBehavior: Flickable.DragOverBounds + + ScrollBar.vertical: ScrollBar {} + + delegate: Item { + width: listView.width + height: 64 + + Rectangle { + anchors.fill: parent + + visible: currentRoom === enteredRoom + color: Material.accent + opacity: 0.1 + } + + Rectangle { + width: unreadCount > 0 ? 4 : 0 + height: parent.height + + color: Material.accent + + Behavior on width { + PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 } + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + + spacing: 12 + + Avatar { + Layout.preferredWidth: height + Layout.fillHeight: true + + source: avatar + hint: name || "No Name" + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: name || "No Name" + color: MPalette.foreground + font.pixelSize: 16 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm,"") + color: MPalette.lighter + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + + Label { + visible: notificationCount > 0 && highlightCount == 0 + color: "white" + text: notificationCount + leftPadding: 12 + rightPadding: 12 + topPadding: 4 + bottomPadding: 4 + font.bold: true + + background: Rectangle { + radius: height / 2 + color: MPalette.lighter + } + } + + Label { + visible: highlightCount > 0 + color: "white" + text: highlightCount + leftPadding: 12 + rightPadding: 12 + topPadding: 4 + bottomPadding: 4 + font.bold: true + + background: Rectangle { + radius: height / 2 + color: MPalette.accent + } + } + } + + RippleEffect { + anchors.fill: parent + + onSecondaryClicked: roomContextMenu.popup() + onPrimaryClicked: { + if (category === RoomType.Invited) { + inviteDialog.currentRoom = currentRoom + inviteDialog.open() + } else { + if (enteredRoom) { + enteredRoom.displayed = false + leaveRoom(enteredRoom) + } + currentRoom.displayed = true + enterRoom(currentRoom) + enteredRoom = currentRoom + } + } + } + + Menu { + id: roomContextMenu + + MenuItem { + text: "Favourite" + checkable: true + checked: category === RoomType.Favorite + + onTriggered: category === RoomType.Favorite ? currentRoom.removeTag("m.favourite") : currentRoom.addTag("m.favourite", 1.0) + } + MenuItem { + text: "Deprioritize" + checkable: true + checked: category === RoomType.Deprioritized + + onTriggered: category === RoomType.Deprioritized ? currentRoom.removeTag("m.lowpriority") : currentRoom.addTag("m.lowpriority", 1.0) + } + MenuSeparator {} + MenuItem { + text: "Mark as Read" + + onTriggered: currentRoom.markAllMessagesAsRead() + } + MenuItem { + text: "Leave Room" + + onTriggered: currentRoom.forget() + } + } + } + + section.property: "display" + section.criteria: ViewSection.FullString + section.delegate: Label { + width: parent.width + height: 24 + + text: section + color: MPalette.lighter + leftPadding: 16 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + } } Dialog { @@ -70,11 +929,30 @@ RoomListPanelForm { title: "Action Required" modal: true - standardButtons: Dialog.Ok | Dialog.Cancel contentItem: Label { text: "Accept this invitation?" } - onAccepted: currentRoom.acceptInvitation() - onRejected: currentRoom.forget() + footer: DialogButtonBox { + Button { + text: "Accept" + flat: true + + onClicked: currentRoom.acceptInvitation() + } + + Button { + text: "Reject" + flat: true + + onClicked: currentRoom.forget() + } + + Button { + text: "Cancel" + flat: true + + onClicked: inviteDialog.close() + } + } } } diff --git a/imports/Spectral/Panel/RoomListPanelForm.ui.qml b/imports/Spectral/Panel/RoomListPanelForm.ui.qml deleted file mode 100644 index b0b6307..0000000 --- a/imports/Spectral/Panel/RoomListPanelForm.ui.qml +++ /dev/null @@ -1,138 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 -import QtQuick.Controls.Material 2.2 -import QtQml.Models 2.3 - -import Spectral.Component 2.0 -import Spectral.Menu 2.0 -import Spectral.Effect 2.0 - -import Spectral 0.1 -import Spectral.Setting 0.1 -import SortFilterProxyModel 0.2 - -import "qrc:/js/util.js" as Util - -Rectangle { - property var listModel - property int filter: 0 - property var enteredRoom: null - - property alias searchField: searchField - property alias model: listView.model - - property bool miniMode: width == 64 - - signal enterRoom(var room) - signal leaveRoom(var room) - - color: MSettings.darkTheme ? "#323232" : "#f3f3f3" - - Label { - text: miniMode ? "Empty" : "Here? No, not here." - anchors.centerIn: parent - visible: listView.count === 0 - } - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 - Layout.margins: 12 - - color: MSettings.darkTheme ? "#303030" : "#fafafa" - - RowLayout { - anchors.fill: parent - - spacing: 0 - - MaterialIcon { - Layout.preferredWidth: height - Layout.fillHeight: true - - visible: !miniMode && !searchField.text - - icon: "\ue8b6" - color: "grey" - } - - ItemDelegate { - Layout.preferredWidth: height - Layout.fillHeight: true - - visible: !miniMode && searchField.text - - contentItem: MaterialIcon { - icon: "\ue5cd" - color: "grey" - } - - onClicked: searchField.text = "" - } - - AutoTextField { - Layout.fillWidth: true - Layout.fillHeight: true - - id: searchField - - topPadding: 0 - bottomPadding: 0 - placeholderText: "Search..." - - background: Item { - } - } - } - } - - AutoListView { - Layout.fillWidth: true - Layout.fillHeight: true - - id: listView - - spacing: 1 - clip: true - - boundsBehavior: Flickable.DragOverBounds - - ScrollBar.vertical: ScrollBar { - } - - delegate: RoomListDelegate { - width: parent.width - height: 64 - } - - section.property: "display" - section.criteria: ViewSection.FullString - section.delegate: Label { - width: parent.width - height: 24 - - text: section - color: "grey" - leftPadding: miniMode ? undefined : 16 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - horizontalAlignment: miniMode ? Text.AlignHCenter : undefined - } - - RoomContextMenu { - id: roomContextMenu - } - } - } -} - -/*##^## Designer { - D{i:0;autoSize:true;height:480;width:640} -} - ##^##*/ diff --git a/imports/Spectral/Panel/RoomPanel.qml b/imports/Spectral/Panel/RoomPanel.qml index 658d4d3..62e09b3 100644 --- a/imports/Spectral/Panel/RoomPanel.qml +++ b/imports/Spectral/Panel/RoomPanel.qml @@ -1,50 +1,425 @@ -import QtQuick 2.9 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 +import Qt.labs.qmlmodels 1.0 -RoomPanelForm { - roomHeader.paintable: currentRoom ? currentRoom.paintable : null - roomHeader.topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : "" - roomHeader.onClicked: roomDrawer.open() +import Spectral.Component 2.0 +import Spectral.Component.Emoji 2.0 +import Spectral.Component.Timeline 2.0 +import Spectral.Effect 2.0 - sortedMessageEventModel.onModelReset: { - if (currentRoom) { - var lastScrollPosition = sortedMessageEventModel.mapFromSource(currentRoom.savedTopVisibleIndex()) - messageListView.currentIndex = lastScrollPosition - if (messageListView.contentY < messageListView.originY + 10 || currentRoom.timelineSize < 20) - currentRoom.getPreviousContent(100) - } +import Spectral 0.1 +import Spectral.Setting 0.1 +import SortFilterProxyModel 0.2 + +Item { + property var currentRoom: null + + id: root + + MessageEventModel { + id: messageEventModel + room: currentRoom } - messageListView { - property int largestVisibleIndex: messageListView.count > 0 ? messageListView.indexAt(messageListView.contentX, messageListView.contentY + messageListView.height - 1) : -1 + RoomDrawer { + width: Math.min(root.width * 0.7, 480) + height: root.height - onContentYChanged: { - if(currentRoom && messageListView.contentY - 5000 < messageListView.originY) - currentRoom.getPreviousContent(50); + id: roomDrawer + + room: currentRoom + } + + Label { + anchors.centerIn: parent + visible: !currentRoom + text: "Please choose a room." + } + + Image { + anchors.fill: parent + + visible: currentRoom && MSettings.enableTimelineBackground + + source: MSettings.timelineBackground || MSettings.darkTheme ? "qrc:/assets/img/roompanel-dark.svg" : "qrc:/assets/img/roompanel.svg" + fillMode: Image.PreserveAspectCrop + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + visible: currentRoom + + RoomHeader { + Layout.fillWidth: true + Layout.preferredHeight: 64 + z: 10 + + id: roomHeader + + avatar: currentRoom ? currentRoom.avatarMediaId : "" + topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : "" + atTop: messageListView.atYBeginning + + onClicked: roomDrawer.open() } - onMovementEnded: currentRoom.saveViewport(sortedMessageEventModel.mapToSource(messageListView.indexAt(messageListView.contentX, messageListView.contentY)), sortedMessageEventModel.mapToSource(largestVisibleIndex)) + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumWidth: 960 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + Layout.alignment: Qt.AlignHCenter - displaced: Transition { - NumberAnimation { - property: "y"; duration: 200 - easing.type: Easing.OutQuad + spacing: 16 + + AutoListView { + Layout.fillWidth: true + Layout.fillHeight: true + + id: messageListView + + spacing: 4 + + displayMarginBeginning: 100 + displayMarginEnd: 100 + verticalLayoutDirection: ListView.BottomToTop + highlightMoveDuration: 500 + + boundsBehavior: Flickable.DragOverBounds + + model: SortFilterProxyModel { + id: sortedMessageEventModel + + sourceModel: messageEventModel + + filters: ExpressionFilter { + expression: marks !== 0x10 && eventType !== "other" + } + + onModelReset: { + if (currentRoom) { + var lastScrollPosition = sortedMessageEventModel.mapFromSource(currentRoom.savedTopVisibleIndex()) + messageListView.currentIndex = lastScrollPosition + if (messageListView.contentY < messageListView.originY + 10 || currentRoom.timelineSize < 20) + currentRoom.getPreviousContent(50) + } + } + } + + property int largestVisibleIndex: count > 0 ? indexAt(contentX, contentY + height - 1) : -1 + + onContentYChanged: { + if(currentRoom && contentY - 5000 < originY) + currentRoom.getPreviousContent(20); + } + + displaced: Transition { + NumberAnimation { + property: "y"; duration: 200 + easing.type: Easing.OutQuad + } + } + + delegate: DelegateChooser { + role: "eventType" + + DelegateChoice { + roleValue: "state" + delegate: ColumnLayout { + width: messageListView.width + spacing: 4 + + SectionDelegate { + Layout.alignment: Qt.AlignHCenter + Layout.margins: 16 + + visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 + } + + StateDelegate { + Layout.maximumWidth: parent.width + Layout.alignment: Qt.AlignHCenter + } + } + } + + DelegateChoice { + roleValue: "emote" + delegate: ColumnLayout { + width: messageListView.width + spacing: 4 + + SectionDelegate { + Layout.alignment: Qt.AlignHCenter + Layout.margins: 16 + + visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 + } + + StateDelegate { + Layout.maximumWidth: parent.width + Layout.alignment: Qt.AlignHCenter + } + } + } + + DelegateChoice { + roleValue: "message" + delegate: ColumnLayout { + width: messageListView.width + spacing: 4 + + SectionDelegate { + Layout.alignment: Qt.AlignHCenter + Layout.margins: 16 + + visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 + } + + MessageDelegate { + } + } + } + + DelegateChoice { + roleValue: "notice" + delegate: ColumnLayout { + width: messageListView.width + spacing: 4 + + SectionDelegate { + Layout.alignment: Qt.AlignHCenter + Layout.margins: 16 + + visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 + } + + MessageDelegate { + } + } + } + + DelegateChoice { + roleValue: "image" + delegate: ColumnLayout { + width: messageListView.width + spacing: 4 + + SectionDelegate { + Layout.alignment: Qt.AlignHCenter + Layout.margins: 16 + + visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 + } + + ImageDelegate { + Layout.maximumWidth: parent.width + } + } + } + + DelegateChoice { + roleValue: "file" + delegate: ColumnLayout { + width: messageListView.width + spacing: 4 + + SectionDelegate { + Layout.alignment: Qt.AlignHCenter + Layout.margins: 16 + + visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 + } + + FileDelegate { + Layout.maximumWidth: parent.width + } + } + } + } + + RoundButton { + width: 64 + height: 64 + anchors.right: parent.right + anchors.top: parent.top + + id: goBottomFab + + visible: currentRoom && currentRoom.hasUnreadMessages + + contentItem: MaterialIcon { + anchors.fill: parent + + icon: "\ue316" + color: "white" + } + + Material.background: Material.accent + + onClicked: goToEvent(currentRoom.readMarkerEventId) + } + + RoundButton { + width: 64 + height: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + + id: goTopFab + + visible: !messageListView.atYEnd + + contentItem: MaterialIcon { + anchors.fill: parent + + icon: "\ue313" + color: "white" + } + + Material.background: Material.accent + + onClicked: messageListView.positionViewAtBeginning() + } + + Popup { + property string sourceText + + anchors.centerIn: parent + width: 480 + + id: sourceDialog + + parent: ApplicationWindow.overlay + + padding: 16 + + closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside + + contentItem: ScrollView { + clip: true + TextArea { + readOnly: true + selectByMouse: true + + text: sourceDialog.sourceText + } + } + } + + Popup { + property alias listModel: readMarkerListView.model + + x: (window.width - width) / 2 + y: (window.height - height) / 2 + width: 320 + + id: readMarkerDialog + + parent: ApplicationWindow.overlay + + modal: true + padding: 16 + + closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside + + contentItem: AutoListView { + implicitHeight: Math.min(window.height - 64, + readMarkerListView.contentHeight) + + id: readMarkerListView + + clip: true + boundsBehavior: Flickable.DragOverBounds + + delegate: ItemDelegate { + width: parent.width + height: 48 + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 12 + + Avatar { + Layout.preferredWidth: height + Layout.fillHeight: true + + source: modelData.avatar + hint: modelData.displayName + } + + Label { + Layout.fillWidth: true + + text: modelData.displayName + } + } + } + + ScrollBar.vertical: ScrollBar {} + } + } + } + + Control { + Layout.maximumWidth: parent.width * 0.8 + + visible: currentRoom && currentRoom.hasUsersTyping + padding: 8 + + contentItem: RowLayout { + spacing: 8 + + Repeater { + model: currentRoom && currentRoom.hasUsersTyping ? currentRoom.usersTyping : null + + delegate: Avatar { + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + + source: modelData.avatarMediaId + hint: modelData.displayName + } + } + + BusyIndicator { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + } + } + + background: Rectangle { + color: MPalette.banner + radius: height / 2 + } + } + + RoomPanelInput { + Layout.fillWidth: true + + id: roomPanelInput } } } - goBottomFab.onClicked: goToEvent(currentRoom.readMarkerEventId) - goTopFab.onClicked: messageListView.positionViewAtBeginning() - function goToEvent(eventID) { var index = messageEventModel.eventIDToIndex(eventID) if (index === -1) return - messageListView.currentIndex = -1 - messageListView.currentIndex = sortedMessageEventModel.mapFromSource(index) + // messageListView.currentIndex = sortedMessageEventModel.mapFromSource(index) + messageListView.positionViewAtIndex(sortedMessageEventModel.mapFromSource(index), ListView.Contain) } function saveReadMarker(room) { var readMarker = sortedMessageEventModel.get(messageListView.largestVisibleIndex).eventId if (!readMarker) return room.readMarkerEventId = readMarker + currentRoom.saveViewport(sortedMessageEventModel.mapToSource(messageListView.indexAt(messageListView.contentX, messageListView.contentY)), sortedMessageEventModel.mapToSource(messageListView.largestVisibleIndex)) } } diff --git a/imports/Spectral/Panel/RoomPanelForm.ui.qml b/imports/Spectral/Panel/RoomPanelForm.ui.qml deleted file mode 100644 index c970db7..0000000 --- a/imports/Spectral/Panel/RoomPanelForm.ui.qml +++ /dev/null @@ -1,322 +0,0 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 - -import Spectral.Component 2.0 -import Spectral.Component.Emoji 2.0 -import Spectral.Component.Timeline 2.0 -import Spectral.Menu 2.0 -import Spectral.Effect 2.0 - -import Spectral 0.1 -import Spectral.Setting 0.1 -import SortFilterProxyModel 0.2 - -import "qrc:/js/md.js" as Markdown -import "qrc:/js/util.js" as Util - -Item { - property var currentRoom: null - - property alias roomHeader: roomHeader - property alias messageListView: messageListView - property alias goTopFab: goTopFab - property alias goBottomFab: goBottomFab - property alias messageEventModel: messageEventModel - property alias sortedMessageEventModel: sortedMessageEventModel - property alias roomDrawer: roomDrawer - - id: root - - MessageEventModel { - id: messageEventModel - room: currentRoom - } - - RoomDrawer { - width: Math.min(root.width * 0.7, 480) - height: root.height - - id: roomDrawer - - room: currentRoom - } - - Label { - anchors.centerIn: parent - visible: !currentRoom - text: "Please choose a room." - } - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - visible: currentRoom - - RoomHeader { - Layout.fillWidth: true - Layout.preferredHeight: 64 - z: 10 - - id: roomHeader - } - - AutoListView { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - id: messageListView - - displayMarginBeginning: 40 - displayMarginEnd: 40 - verticalLayoutDirection: ListView.BottomToTop - spacing: 8 - - boundsBehavior: Flickable.DragOverBounds - - model: SortFilterProxyModel { - id: sortedMessageEventModel - - sourceModel: messageEventModel - - filters: ExpressionFilter { - expression: marks !== 0x08 && marks !== 0x10 - } - } - - delegate: ColumnLayout { - width: parent.width - - id: delegateColumn - - spacing: 8 - - Label { - Layout.alignment: Qt.AlignHCenter - - visible: section !== aboveSection - - text: section - color: "white" - verticalAlignment: Text.AlignVCenter - leftPadding: 8 - rightPadding: 8 - topPadding: 4 - bottomPadding: 4 - - background: Rectangle { - color: MSettings.darkTheme ? "#484848" : "grey" - } - } - - MessageDelegate { - visible: eventType === "notice" || eventType === "message" - || eventType === "image" || eventType === "video" - || eventType === "audio" || eventType === "file" - } - - StateDelegate { - Layout.maximumWidth: messageListView.width * 0.8 - - visible: eventType === "emote" || eventType === "state" - } - - Label { - Layout.alignment: Qt.AlignHCenter - - visible: eventType === "other" - - text: display - color: "grey" - font.italic: true - } - - RowLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - - visible: readMarker === true - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 2 - - color: Material.accent - } - - Label { - text: "And Now" - color: Material.accent - verticalAlignment: Text.AlignVCenter - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 2 - - color: Material.accent - } - } - } - - RoundButton { - width: 64 - height: 64 - anchors.right: parent.right - anchors.top: parent.top - - id: goBottomFab - - visible: currentRoom && currentRoom.hasUnreadMessages - - contentItem: MaterialIcon { - anchors.fill: parent - - icon: "\ue316" - color: "white" - } - - Material.background: Material.accent - } - - RoundButton { - width: 64 - height: 64 - anchors.right: parent.right - anchors.bottom: parent.bottom - - id: goTopFab - - visible: !messageListView.atYEnd - - contentItem: MaterialIcon { - anchors.fill: parent - - icon: "\ue313" - color: "white" - } - - Material.background: Material.accent - } - - MessageContextMenu { - id: messageContextMenu - } - - Popup { - property string sourceText - - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 480 - - id: sourceDialog - - parent: ApplicationWindow.overlay - - modal: true - - padding: 16 - - closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside - - contentItem: ScrollView { - TextArea { - readOnly: true - selectByMouse: true - - text: sourceDialog.sourceText - } - } - } - - Popup { - property alias listModel: readMarkerListView.model - - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 320 - - id: readMarkerDialog - - parent: ApplicationWindow.overlay - - modal: true - padding: 16 - - closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside - - contentItem: AutoListView { - implicitHeight: Math.min(window.height - 64, - readMarkerListView.contentHeight) - - id: readMarkerListView - - clip: true - boundsBehavior: Flickable.DragOverBounds - - delegate: ItemDelegate { - width: parent.width - height: 48 - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 12 - - ImageItem { - Layout.preferredWidth: height - Layout.fillHeight: true - - source: modelData.paintable - hint: modelData.displayName - } - - Label { - Layout.fillWidth: true - - text: modelData.displayName - } - } - } - - ScrollBar.vertical: ScrollBar { - } - } - } - } - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 40 - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 40 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - color: Material.background - - RoomPanelInput { - anchors.verticalCenter: parent.top - - id: roomPanelInput - - width: parent.width - height: 48 - } - } - } -} - - -/*##^## Designer { - D{i:0;autoSize:true;height:480;width:640} -} - ##^##*/ diff --git a/imports/Spectral/Panel/RoomPanelInput.qml b/imports/Spectral/Panel/RoomPanelInput.qml index 3c875c1..90a0b28 100644 --- a/imports/Spectral/Panel/RoomPanelInput.qml +++ b/imports/Spectral/Panel/RoomPanelInput.qml @@ -1,7 +1,7 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 import Spectral.Component 2.0 import Spectral.Component.Emoji 2.0 @@ -10,167 +10,222 @@ import Spectral.Setting 0.1 import Spectral 0.1 -import "qrc:/js/md.js" as Markdown - -Rectangle { - property bool isReply - property string replyUserID +Control { + property alias isReply: replyItem.visible + property var replyUser property string replyEventID property string replyContent - property bool isAutoCompleting + property alias isAutoCompleting: autoCompleteListView.visible property var autoCompleteModel property int autoCompleteBeginPosition property int autoCompleteEndPosition - color: MSettings.darkTheme ? "#303030" : "#fafafa" + id: root - layer.enabled: true - layer.effect: ElevationEffect { - elevation: 2 + padding: 0 + + background: Rectangle { + color: MSettings.darkTheme ? "#303030" : "#fafafa" + radius: 24 + + layer.enabled: true + layer.effect: ElevationEffect { + elevation: 2 + } } - Popup { - x: 0 - y: -height - 10 - width: Math.min(autoCompleteListView.contentWidth, parent.width) - height: 36 - padding: 0 + contentItem: ColumnLayout { + spacing: 0 - Material.elevation: 2 + RowLayout { + Layout.fillWidth: true + Layout.margins: 8 - id: autoComplete + id: replyItem - visible: isAutoCompleting && autoCompleteModel.length !== 0 + visible: false + + spacing: 8 + + Avatar { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + + source: replyUser ? replyUser.avatarMediaId : "" + hint: replyUser ? replyUser.displayName : "No name" + } + + Label { + Layout.fillWidth: true + + text: replyContent + font.pixelSize: 16 + + wrapMode: Label.Wrap + } + } + + EmojiPicker { + Layout.fillWidth: true + + id: emojiPicker + + visible: false + + textArea: inputField + emojiModel: EmojiModel { id: emojiModel } + } + + ListView { + Layout.fillWidth: true + Layout.preferredHeight: 36 + Layout.margins: 8 - contentItem: ListView { id: autoCompleteListView + visible: false + model: autoCompleteModel clip: true + spacing: 4 orientation: ListView.Horizontal highlightFollowsCurrentItem: true keyNavigationWraps: true - highlight: Rectangle { - color: Material.accent - opacity: 0.4 - } - - delegate: ItemDelegate { + delegate: Control { property string autoCompleteText: modelData.displayName || modelData.unicode property bool isEmoji: modelData.unicode != null + readonly property bool highlighted: autoCompleteListView.currentIndex === index - height: parent.height - padding: 4 + height: 36 + padding: 8 + + background: Rectangle { + visible: !isEmoji + color: highlighted ? Material.accent : "transparent" + border.color: Material.accent + border.width: 2 + radius: height / 2 + } contentItem: Row { - spacing: 8 + spacing: 4 Text { - width: parent.height - height: parent.height + width: 20 + height: 20 visible: isEmoji text: autoCompleteText - font.pointSize: 16 + font.pixelSize: 24 font.family: "Emoji" verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } - ImageItem { - width: parent.height - height: parent.height + Avatar { + width: 20 + height: 20 visible: !isEmoji - source: modelData.paintable || null + source: modelData.avatarMediaId || null } Label { height: parent.height visible: !isEmoji text: autoCompleteText + color: highlighted ? "white" : Material.accent verticalAlignment: Text.AlignVCenter } } - onClicked: { - autoCompleteListView.currentIndex = index - inputField.replaceAutoComplete(autoCompleteText) + MouseArea { + anchors.fill: parent + onClicked: { + autoCompleteListView.currentIndex = index + inputField.replaceAutoComplete(autoCompleteText) + } } } } - } - Rectangle { - width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0 - height: parent.height - - opacity: 0.2 - color: Material.accent - } - - RowLayout { - anchors.fill: parent - - spacing: 0 - - ItemDelegate { - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 - - id: uploadButton - visible: !isReply - - contentItem: MaterialIcon { - icon: "\ue226" - } - - onClicked: currentRoom.chooseAndUploadFile() - - BusyIndicator { - anchors.fill: parent - - running: currentRoom && currentRoom.hasFileUploading - } - } - - ItemDelegate { - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 - - id: cancelReplyButton - visible: isReply - - contentItem: MaterialIcon { - icon: "\ue5cd" - } - - onClicked: clearReply() - } - - ScrollView { + Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 48 + Layout.preferredHeight: 1 + Layout.leftMargin: 12 + Layout.rightMargin: 12 - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + visible: emojiPicker.visible || replyItem.visible || autoCompleteListView.visible - clip: true + color: MSettings.darkTheme ? "#424242" : "#e7ebeb" + } + + RowLayout { + Layout.fillWidth: true + + spacing: 0 + + ToolButton { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignBottom + + id: uploadButton + visible: !isReply + + contentItem: MaterialIcon { + icon: "\ue226" + } + + onClicked: currentRoom.chooseAndUploadFile() + + BusyIndicator { + anchors.fill: parent + + running: currentRoom && currentRoom.hasFileUploading + } + } + + ToolButton { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignBottom + + id: cancelReplyButton + + visible: isReply + + contentItem: MaterialIcon { + icon: "\ue5cd" + } + + onClicked: clearReply() + } TextArea { property real progress: 0 + Layout.fillWidth: true + Layout.minimumHeight: 48 + id: inputField wrapMode: Text.Wrap - placeholderText: isReply ? "Reply to " + replyUserID : "Send a Message" - leftPadding: 16 + placeholderText: "Send a Message" topPadding: 0 bottomPadding: 0 selectByMouse: true verticalAlignment: TextEdit.AlignVCenter - text: currentRoom ? currentRoom.cachedInput : "" + text: currentRoom != null ? currentRoom.cachedInput : "" - background: Item { + background: Item {} + + Rectangle { + width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0 + height: parent.height + + opacity: 0.2 + color: Material.accent } Timer { @@ -193,19 +248,18 @@ Rectangle { onTriggered: currentRoom.sendTypingNotification(true) } - ToolTip.visible: currentRoom - && currentRoom.hasUsersTyping - ToolTip.text: currentRoom ? currentRoom.usersTyping : "" - Keys.onReturnPressed: { if (event.modifiers & Qt.ShiftModifier) { insert(cursorPosition, "\n") - } else { + } else if (text) { postMessage(text) text = "" + closeAll() } } + Keys.onEscapePressed: closeAll() + Keys.onBacktabPressed: if (isAutoCompleting) autoCompleteListView.decrementCurrentIndex() Keys.onTabPressed: { @@ -259,8 +313,7 @@ Rectangle { var PREFIX_MARKDOWN = '/md ' if (isReply) { - currentRoom.sendReply(replyUserID, replyEventID, replyContent, text) - clearReply() + currentRoom.sendReply(replyUser.id, replyEventID, replyContent, text) return } @@ -294,44 +347,26 @@ Rectangle { } if (text.indexOf(PREFIX_MARKDOWN) === 0) { text = text.substr(PREFIX_MARKDOWN.length) - var parsedText = Markdown.markdown_parser(text) - currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text) + currentRoom.postMarkdownText(text) return } currentRoom.postPlainText(text) } } - } - ItemDelegate { - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 + ToolButton { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignBottom - id: emojiButton + id: emojiButton - contentItem: MaterialIcon { - icon: "\ue24e" - } - - onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open() - - EmojiPicker { - x: -width + parent.width - y: -height - 16 - - width: 360 - height: 320 - - id: emojiPicker - - emojiModel: EmojiModel { - id: emojiModel + contentItem: MaterialIcon { + icon: "\ue24e" } - Material.elevation: 2 - - textArea: inputField + onClicked: emojiPicker.visible = !emojiPicker.visible } } } @@ -346,8 +381,18 @@ Rectangle { function clearReply() { isReply = false - replyUserID = "" + replyUser = null replyEventID = "" replyContent = "" } + + function focus() { + inputField.forceActiveFocus() + } + + function closeAll() { + replyItem.visible = false + autoCompleteListView.visible = false + emojiPicker.visible = false + } } diff --git a/imports/Spectral/Setting/Palette.qml b/imports/Spectral/Setting/Palette.qml new file mode 100644 index 0000000..b959905 --- /dev/null +++ b/imports/Spectral/Setting/Palette.qml @@ -0,0 +1,14 @@ +pragma Singleton +import QtQuick 2.12 +import QtQuick.Controls.Material 2.12 + +QtObject { + readonly property int theme: MSettings.darkTheme ? Material.Dark : Material.Light + + readonly property color primary: "#344955" + readonly property color accent: "#673AB7" + readonly property color foreground: MSettings.darkTheme ? "#FFFFFF" : "#1D333E" + readonly property color background: MSettings.darkTheme ? "#303030" : "#FFFFFF" + readonly property color lighter: MSettings.darkTheme ? "#FFFFFF" : "#5B7480" + readonly property color banner: MSettings.darkTheme ? "#404040" : "#F2F3F4" +} diff --git a/imports/Spectral/Setting/Setting.qml b/imports/Spectral/Setting/Setting.qml index c95c500..fb7a966 100644 --- a/imports/Spectral/Setting/Setting.qml +++ b/imports/Spectral/Setting/Setting.qml @@ -1,11 +1,15 @@ pragma Singleton -import QtQuick 2.9 +import QtQuick 2.12 import Qt.labs.settings 1.0 Settings { + property bool showNotification: true + property bool pressAndHold property bool showTray: true - property bool confirmOnExit: true property bool darkTheme + + property bool enableTimelineBackground: true + property string timelineBackground } diff --git a/imports/Spectral/Setting/qmldir b/imports/Spectral/Setting/qmldir index 5e1c2a7..2c923cd 100644 --- a/imports/Spectral/Setting/qmldir +++ b/imports/Spectral/Setting/qmldir @@ -1,3 +1,3 @@ module Spectral.Setting singleton MSettings 0.1 Setting.qml - +singleton MPalette 0.1 Palette.qml diff --git a/include/hoedown/autolink.c b/include/hoedown/autolink.c new file mode 100644 index 0000000..e7019fd --- /dev/null +++ b/include/hoedown/autolink.c @@ -0,0 +1,281 @@ +#include "autolink.h" + +#include +#include +#include +#include + +#ifndef _MSC_VER +#include +#else +#define strncasecmp _strnicmp +#endif + +int +hoedown_autolink_is_safe(const uint8_t *data, size_t size) +{ + static const size_t valid_uris_count = 6; + static const char *valid_uris[] = { + "http://", "https://", "/", "#", "ftp://", "mailto:" + }; + static const size_t valid_uris_size[] = { 7, 8, 1, 1, 6, 7 }; + size_t i; + + for (i = 0; i < valid_uris_count; ++i) { + size_t len = valid_uris_size[i]; + + if (size > len && + strncasecmp((char *)data, valid_uris[i], len) == 0 && + isalnum(data[len])) + return 1; + } + + return 0; +} + +static size_t +autolink_delim(uint8_t *data, size_t link_end, size_t max_rewind, size_t size) +{ + uint8_t cclose, copen = 0; + size_t i; + + for (i = 0; i < link_end; ++i) + if (data[i] == '<') { + link_end = i; + break; + } + + while (link_end > 0) { + if (strchr("?!.,:", data[link_end - 1]) != NULL) + link_end--; + + else if (data[link_end - 1] == ';') { + size_t new_end = link_end - 2; + + while (new_end > 0 && isalpha(data[new_end])) + new_end--; + + if (new_end < link_end - 2 && data[new_end] == '&') + link_end = new_end; + else + link_end--; + } + else break; + } + + if (link_end == 0) + return 0; + + cclose = data[link_end - 1]; + + switch (cclose) { + case '"': copen = '"'; break; + case '\'': copen = '\''; break; + case ')': copen = '('; break; + case ']': copen = '['; break; + case '}': copen = '{'; break; + } + + if (copen != 0) { + size_t closing = 0; + size_t opening = 0; + size_t i = 0; + + /* Try to close the final punctuation sign in this same line; + * if we managed to close it outside of the URL, that means that it's + * not part of the URL. If it closes inside the URL, that means it + * is part of the URL. + * + * Examples: + * + * foo http://www.pokemon.com/Pikachu_(Electric) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo (http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric)) + * + * (foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => foo http://www.pokemon.com/Pikachu_(Electric) + */ + + while (i < link_end) { + if (data[i] == copen) + opening++; + else if (data[i] == cclose) + closing++; + + i++; + } + + if (closing != opening) + link_end--; + } + + return link_end; +} + +static size_t +check_domain(uint8_t *data, size_t size, int allow_short) +{ + size_t i, np = 0; + + if (!isalnum(data[0])) + return 0; + + for (i = 1; i < size - 1; ++i) { + if (strchr(".:", data[i]) != NULL) np++; + else if (!isalnum(data[i]) && data[i] != '-') break; + } + + if (allow_short) { + /* We don't need a valid domain in the strict sense (with + * least one dot; so just make sure it's composed of valid + * domain characters and return the length of the the valid + * sequence. */ + return i; + } else { + /* a valid domain needs to have at least a dot. + * that's as far as we get */ + return np ? i : 0; + } +} + +size_t +hoedown_autolink__www( + size_t *rewind_p, + hoedown_buffer *link, + uint8_t *data, + size_t max_rewind, + size_t size, + unsigned int flags) +{ + size_t link_end; + + if (max_rewind > 0 && !ispunct(data[-1]) && !isspace(data[-1])) + return 0; + + if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0) + return 0; + + link_end = check_domain(data, size, 0); + + if (link_end == 0) + return 0; + + while (link_end < size && !isspace(data[link_end])) + link_end++; + + link_end = autolink_delim(data, link_end, max_rewind, size); + + if (link_end == 0) + return 0; + + hoedown_buffer_put(link, data, link_end); + *rewind_p = 0; + + return (int)link_end; +} + +size_t +hoedown_autolink__email( + size_t *rewind_p, + hoedown_buffer *link, + uint8_t *data, + size_t max_rewind, + size_t size, + unsigned int flags) +{ + size_t link_end, rewind; + int nb = 0, np = 0; + + for (rewind = 0; rewind < max_rewind; ++rewind) { + uint8_t c = data[-1 - rewind]; + + if (isalnum(c)) + continue; + + if (strchr(".+-_", c) != NULL) + continue; + + break; + } + + if (rewind == 0) + return 0; + + for (link_end = 0; link_end < size; ++link_end) { + uint8_t c = data[link_end]; + + if (isalnum(c)) + continue; + + if (c == '@') + nb++; + else if (c == '.' && link_end < size - 1) + np++; + else if (c != '-' && c != '_') + break; + } + + if (link_end < 2 || nb != 1 || np == 0 || + !isalpha(data[link_end - 1])) + return 0; + + link_end = autolink_delim(data, link_end, max_rewind, size); + + if (link_end == 0) + return 0; + + hoedown_buffer_put(link, data - rewind, link_end + rewind); + *rewind_p = rewind; + + return link_end; +} + +size_t +hoedown_autolink__url( + size_t *rewind_p, + hoedown_buffer *link, + uint8_t *data, + size_t max_rewind, + size_t size, + unsigned int flags) +{ + size_t link_end, rewind = 0, domain_len; + + if (size < 4 || data[1] != '/' || data[2] != '/') + return 0; + + while (rewind < max_rewind && isalpha(data[-1 - rewind])) + rewind++; + + if (!hoedown_autolink_is_safe(data - rewind, size + rewind)) + return 0; + + link_end = strlen("://"); + + domain_len = check_domain( + data + link_end, + size - link_end, + flags & HOEDOWN_AUTOLINK_SHORT_DOMAINS); + + if (domain_len == 0) + return 0; + + link_end += domain_len; + while (link_end < size && !isspace(data[link_end])) + link_end++; + + link_end = autolink_delim(data, link_end, max_rewind, size); + + if (link_end == 0) + return 0; + + hoedown_buffer_put(link, data - rewind, link_end + rewind); + *rewind_p = rewind; + + return link_end; +} diff --git a/include/hoedown/autolink.h b/include/hoedown/autolink.h new file mode 100644 index 0000000..528885c --- /dev/null +++ b/include/hoedown/autolink.h @@ -0,0 +1,46 @@ +/* autolink.h - versatile autolinker */ + +#ifndef HOEDOWN_AUTOLINK_H +#define HOEDOWN_AUTOLINK_H + +#include "buffer.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * CONSTANTS * + *************/ + +typedef enum hoedown_autolink_flags { + HOEDOWN_AUTOLINK_SHORT_DOMAINS = (1 << 0) +} hoedown_autolink_flags; + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_autolink_is_safe: verify that a URL has a safe protocol */ +int hoedown_autolink_is_safe(const uint8_t *data, size_t size); + +/* hoedown_autolink__www: search for the next www link in data */ +size_t hoedown_autolink__www(size_t *rewind_p, hoedown_buffer *link, + uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); + +/* hoedown_autolink__email: search for the next email in data */ +size_t hoedown_autolink__email(size_t *rewind_p, hoedown_buffer *link, + uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); + +/* hoedown_autolink__url: search for the next URL in data */ +size_t hoedown_autolink__url(size_t *rewind_p, hoedown_buffer *link, + uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_AUTOLINK_H **/ diff --git a/include/hoedown/buffer.c b/include/hoedown/buffer.c new file mode 100644 index 0000000..af77b70 --- /dev/null +++ b/include/hoedown/buffer.c @@ -0,0 +1,308 @@ +#include "buffer.h" + +#include +#include +#include +#include + +void * +hoedown_malloc(size_t size) +{ + void *ret = malloc(size); + + if (!ret) { + fprintf(stderr, "Allocation failed.\n"); + abort(); + } + + return ret; +} + +void * +hoedown_calloc(size_t nmemb, size_t size) +{ + void *ret = calloc(nmemb, size); + + if (!ret) { + fprintf(stderr, "Allocation failed.\n"); + abort(); + } + + return ret; +} + +void * +hoedown_realloc(void *ptr, size_t size) +{ + void *ret = realloc(ptr, size); + + if (!ret) { + fprintf(stderr, "Allocation failed.\n"); + abort(); + } + + return ret; +} + +void +hoedown_buffer_init( + hoedown_buffer *buf, + size_t unit, + hoedown_realloc_callback data_realloc, + hoedown_free_callback data_free, + hoedown_free_callback buffer_free) +{ + assert(buf); + + buf->data = NULL; + buf->size = buf->asize = 0; + buf->unit = unit; + buf->data_realloc = data_realloc; + buf->data_free = data_free; + buf->buffer_free = buffer_free; +} + +void +hoedown_buffer_uninit(hoedown_buffer *buf) +{ + assert(buf && buf->unit); + buf->data_free(buf->data); +} + +hoedown_buffer * +hoedown_buffer_new(size_t unit) +{ + hoedown_buffer *ret = hoedown_malloc(sizeof (hoedown_buffer)); + hoedown_buffer_init(ret, unit, hoedown_realloc, free, free); + return ret; +} + +void +hoedown_buffer_free(hoedown_buffer *buf) +{ + if (!buf) return; + assert(buf && buf->unit); + + buf->data_free(buf->data); + + if (buf->buffer_free) + buf->buffer_free(buf); +} + +void +hoedown_buffer_reset(hoedown_buffer *buf) +{ + assert(buf && buf->unit); + + buf->data_free(buf->data); + buf->data = NULL; + buf->size = buf->asize = 0; +} + +void +hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz) +{ + size_t neoasz; + assert(buf && buf->unit); + + if (buf->asize >= neosz) + return; + + neoasz = buf->asize + buf->unit; + while (neoasz < neosz) + neoasz += buf->unit; + + buf->data = buf->data_realloc(buf->data, neoasz); + buf->asize = neoasz; +} + +void +hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size) +{ + assert(buf && buf->unit); + + if (buf->size + size > buf->asize) + hoedown_buffer_grow(buf, buf->size + size); + + memcpy(buf->data + buf->size, data, size); + buf->size += size; +} + +void +hoedown_buffer_puts(hoedown_buffer *buf, const char *str) +{ + hoedown_buffer_put(buf, (const uint8_t *)str, strlen(str)); +} + +void +hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c) +{ + assert(buf && buf->unit); + + if (buf->size >= buf->asize) + hoedown_buffer_grow(buf, buf->size + 1); + + buf->data[buf->size] = c; + buf->size += 1; +} + +int +hoedown_buffer_putf(hoedown_buffer *buf, FILE *file) +{ + assert(buf && buf->unit); + + while (!(feof(file) || ferror(file))) { + hoedown_buffer_grow(buf, buf->size + buf->unit); + buf->size += fread(buf->data + buf->size, 1, buf->unit, file); + } + + return ferror(file); +} + +void +hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size) +{ + assert(buf && buf->unit); + + if (size > buf->asize) + hoedown_buffer_grow(buf, size); + + memcpy(buf->data, data, size); + buf->size = size; +} + +void +hoedown_buffer_sets(hoedown_buffer *buf, const char *str) +{ + hoedown_buffer_set(buf, (const uint8_t *)str, strlen(str)); +} + +int +hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size) +{ + if (buf->size != size) return 0; + return memcmp(buf->data, data, size) == 0; +} + +int +hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str) +{ + return hoedown_buffer_eq(buf, (const uint8_t *)str, strlen(str)); +} + +int +hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix) +{ + size_t i; + + for (i = 0; i < buf->size; ++i) { + if (prefix[i] == 0) + return 0; + + if (buf->data[i] != prefix[i]) + return buf->data[i] - prefix[i]; + } + + return 0; +} + +void +hoedown_buffer_slurp(hoedown_buffer *buf, size_t size) +{ + assert(buf && buf->unit); + + if (size >= buf->size) { + buf->size = 0; + return; + } + + buf->size -= size; + memmove(buf->data, buf->data + size, buf->size); +} + +const char * +hoedown_buffer_cstr(hoedown_buffer *buf) +{ + assert(buf && buf->unit); + + if (buf->size < buf->asize && buf->data[buf->size] == 0) + return (char *)buf->data; + + hoedown_buffer_grow(buf, buf->size + 1); + buf->data[buf->size] = 0; + + return (char *)buf->data; +} + +void +hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...) +{ + va_list ap; + int n; + + assert(buf && buf->unit); + + if (buf->size >= buf->asize) + hoedown_buffer_grow(buf, buf->size + 1); + + va_start(ap, fmt); + n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap); + va_end(ap); + + if (n < 0) { +#ifndef _MSC_VER + return; +#else + va_start(ap, fmt); + n = _vscprintf(fmt, ap); + va_end(ap); +#endif + } + + if ((size_t)n >= buf->asize - buf->size) { + hoedown_buffer_grow(buf, buf->size + n + 1); + + va_start(ap, fmt); + n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap); + va_end(ap); + } + + if (n < 0) + return; + + buf->size += n; +} + +void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int c) { + unsigned char unichar[4]; + + assert(buf && buf->unit); + + if (c < 0x80) { + hoedown_buffer_putc(buf, c); + } + else if (c < 0x800) { + unichar[0] = 192 + (c / 64); + unichar[1] = 128 + (c % 64); + hoedown_buffer_put(buf, unichar, 2); + } + else if (c - 0xd800u < 0x800) { + HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd"); + } + else if (c < 0x10000) { + unichar[0] = 224 + (c / 4096); + unichar[1] = 128 + (c / 64) % 64; + unichar[2] = 128 + (c % 64); + hoedown_buffer_put(buf, unichar, 3); + } + else if (c < 0x110000) { + unichar[0] = 240 + (c / 262144); + unichar[1] = 128 + (c / 4096) % 64; + unichar[2] = 128 + (c / 64) % 64; + unichar[3] = 128 + (c % 64); + hoedown_buffer_put(buf, unichar, 4); + } + else { + HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd"); + } +} diff --git a/include/hoedown/buffer.h b/include/hoedown/buffer.h new file mode 100644 index 0000000..d7703f8 --- /dev/null +++ b/include/hoedown/buffer.h @@ -0,0 +1,134 @@ +/* buffer.h - simple, fast buffers */ + +#ifndef HOEDOWN_BUFFER_H +#define HOEDOWN_BUFFER_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_MSC_VER) +#define __attribute__(x) +#define inline __inline +#define __builtin_expect(x,n) x +#endif + + +/********* + * TYPES * + *********/ + +typedef void *(*hoedown_realloc_callback)(void *, size_t); +typedef void (*hoedown_free_callback)(void *); + +struct hoedown_buffer { + uint8_t *data; /* actual character data */ + size_t size; /* size of the string */ + size_t asize; /* allocated size (0 = volatile buffer) */ + size_t unit; /* reallocation unit size (0 = read-only buffer) */ + + hoedown_realloc_callback data_realloc; + hoedown_free_callback data_free; + hoedown_free_callback buffer_free; +}; + +typedef struct hoedown_buffer hoedown_buffer; + + +/************* + * FUNCTIONS * + *************/ + +/* allocation wrappers */ +void *hoedown_malloc(size_t size) __attribute__ ((malloc)); +void *hoedown_calloc(size_t nmemb, size_t size) __attribute__ ((malloc)); +void *hoedown_realloc(void *ptr, size_t size) __attribute__ ((malloc)); + +/* hoedown_buffer_init: initialize a buffer with custom allocators */ +void hoedown_buffer_init( + hoedown_buffer *buffer, + size_t unit, + hoedown_realloc_callback data_realloc, + hoedown_free_callback data_free, + hoedown_free_callback buffer_free +); + +/* hoedown_buffer_uninit: uninitialize an existing buffer */ +void hoedown_buffer_uninit(hoedown_buffer *buf); + +/* hoedown_buffer_new: allocate a new buffer */ +hoedown_buffer *hoedown_buffer_new(size_t unit) __attribute__ ((malloc)); + +/* hoedown_buffer_reset: free internal data of the buffer */ +void hoedown_buffer_reset(hoedown_buffer *buf); + +/* hoedown_buffer_grow: increase the allocated size to the given value */ +void hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz); + +/* hoedown_buffer_put: append raw data to a buffer */ +void hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size); + +/* hoedown_buffer_puts: append a NUL-terminated string to a buffer */ +void hoedown_buffer_puts(hoedown_buffer *buf, const char *str); + +/* hoedown_buffer_putc: append a single char to a buffer */ +void hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c); + +/* hoedown_buffer_putf: read from a file and append to a buffer, until EOF or error */ +int hoedown_buffer_putf(hoedown_buffer *buf, FILE* file); + +/* hoedown_buffer_set: replace the buffer's contents with raw data */ +void hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size); + +/* hoedown_buffer_sets: replace the buffer's contents with a NUL-terminated string */ +void hoedown_buffer_sets(hoedown_buffer *buf, const char *str); + +/* hoedown_buffer_eq: compare a buffer's data with other data for equality */ +int hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size); + +/* hoedown_buffer_eq: compare a buffer's data with NUL-terminated string for equality */ +int hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str); + +/* hoedown_buffer_prefix: compare the beginning of a buffer with a string */ +int hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix); + +/* hoedown_buffer_slurp: remove a given number of bytes from the head of the buffer */ +void hoedown_buffer_slurp(hoedown_buffer *buf, size_t size); + +/* hoedown_buffer_cstr: NUL-termination of the string array (making a C-string) */ +const char *hoedown_buffer_cstr(hoedown_buffer *buf); + +/* hoedown_buffer_printf: formatted printing to a buffer */ +void hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...) __attribute__ ((format (printf, 2, 3))); + +/* hoedown_buffer_put_utf8: put a Unicode character encoded as UTF-8 */ +void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int codepoint); + +/* hoedown_buffer_free: free the buffer */ +void hoedown_buffer_free(hoedown_buffer *buf); + + +/* HOEDOWN_BUFPUTSL: optimized hoedown_buffer_puts of a string literal */ +#define HOEDOWN_BUFPUTSL(output, literal) \ + hoedown_buffer_put(output, (const uint8_t *)literal, sizeof(literal) - 1) + +/* HOEDOWN_BUFSETSL: optimized hoedown_buffer_sets of a string literal */ +#define HOEDOWN_BUFSETSL(output, literal) \ + hoedown_buffer_set(output, (const uint8_t *)literal, sizeof(literal) - 1) + +/* HOEDOWN_BUFEQSL: optimized hoedown_buffer_eqs of a string literal */ +#define HOEDOWN_BUFEQSL(output, literal) \ + hoedown_buffer_eq(output, (const uint8_t *)literal, sizeof(literal) - 1) + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_BUFFER_H **/ diff --git a/include/hoedown/document.c b/include/hoedown/document.c new file mode 100644 index 0000000..a9cb42c --- /dev/null +++ b/include/hoedown/document.c @@ -0,0 +1,3012 @@ +#include "document.h" + +#include +#include +#include +#include + +#include "stack.h" + +#ifndef _MSC_VER +#include +#else +#define strncasecmp _strnicmp +#endif + +#define REF_TABLE_SIZE 8 + +#define BUFFER_BLOCK 0 +#define BUFFER_SPAN 1 + +#define HOEDOWN_LI_END 8 /* internal list flag */ + +const char *hoedown_find_block_tag(const char *str, unsigned int len); + +/*************** + * LOCAL TYPES * + ***************/ + +/* link_ref: reference to a link */ +struct link_ref { + unsigned int id; + + hoedown_buffer *link; + hoedown_buffer *title; + + struct link_ref *next; +}; + +/* footnote_ref: reference to a footnote */ +struct footnote_ref { + unsigned int id; + + int is_used; + unsigned int num; + + hoedown_buffer *contents; +}; + +/* footnote_item: an item in a footnote_list */ +struct footnote_item { + struct footnote_ref *ref; + struct footnote_item *next; +}; + +/* footnote_list: linked list of footnote_item */ +struct footnote_list { + unsigned int count; + struct footnote_item *head; + struct footnote_item *tail; +}; + +/* char_trigger: function pointer to render active chars */ +/* returns the number of chars taken care of */ +/* data is the pointer of the beginning of the span */ +/* offset is the number of valid chars before data */ +typedef size_t +(*char_trigger)(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); + +static size_t char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_image(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); + +enum markdown_char_t { + MD_CHAR_NONE = 0, + MD_CHAR_EMPHASIS, + MD_CHAR_CODESPAN, + MD_CHAR_LINEBREAK, + MD_CHAR_LINK, + MD_CHAR_IMAGE, + MD_CHAR_LANGLE, + MD_CHAR_ESCAPE, + MD_CHAR_ENTITY, + MD_CHAR_AUTOLINK_URL, + MD_CHAR_AUTOLINK_EMAIL, + MD_CHAR_AUTOLINK_WWW, + MD_CHAR_SUPERSCRIPT, + MD_CHAR_QUOTE, + MD_CHAR_MATH +}; + +static char_trigger markdown_char_ptrs[] = { + NULL, + &char_emphasis, + &char_codespan, + &char_linebreak, + &char_link, + &char_image, + &char_langle_tag, + &char_escape, + &char_entity, + &char_autolink_url, + &char_autolink_email, + &char_autolink_www, + &char_superscript, + &char_quote, + &char_math +}; + +struct hoedown_document { + hoedown_renderer md; + hoedown_renderer_data data; + + struct link_ref *refs[REF_TABLE_SIZE]; + struct footnote_list footnotes_found; + struct footnote_list footnotes_used; + uint8_t active_char[256]; + hoedown_stack work_bufs[2]; + hoedown_extensions ext_flags; + size_t max_nesting; + int in_link_body; +}; + +/*************************** + * HELPER FUNCTIONS * + ***************************/ + +static hoedown_buffer * +newbuf(hoedown_document *doc, int type) +{ + static const size_t buf_size[2] = {256, 64}; + hoedown_buffer *work = NULL; + hoedown_stack *pool = &doc->work_bufs[type]; + + if (pool->size < pool->asize && + pool->item[pool->size] != NULL) { + work = pool->item[pool->size++]; + work->size = 0; + } else { + work = hoedown_buffer_new(buf_size[type]); + hoedown_stack_push(pool, work); + } + + return work; +} + +static void +popbuf(hoedown_document *doc, int type) +{ + doc->work_bufs[type].size--; +} + +static void +unscape_text(hoedown_buffer *ob, hoedown_buffer *src) +{ + size_t i = 0, org; + while (i < src->size) { + org = i; + while (i < src->size && src->data[i] != '\\') + i++; + + if (i > org) + hoedown_buffer_put(ob, src->data + org, i - org); + + if (i + 1 >= src->size) + break; + + hoedown_buffer_putc(ob, src->data[i + 1]); + i += 2; + } +} + +static unsigned int +hash_link_ref(const uint8_t *link_ref, size_t length) +{ + size_t i; + unsigned int hash = 0; + + for (i = 0; i < length; ++i) + hash = tolower(link_ref[i]) + (hash << 6) + (hash << 16) - hash; + + return hash; +} + +static struct link_ref * +add_link_ref( + struct link_ref **references, + const uint8_t *name, size_t name_size) +{ + struct link_ref *ref = hoedown_calloc(1, sizeof(struct link_ref)); + + ref->id = hash_link_ref(name, name_size); + ref->next = references[ref->id % REF_TABLE_SIZE]; + + references[ref->id % REF_TABLE_SIZE] = ref; + return ref; +} + +static struct link_ref * +find_link_ref(struct link_ref **references, uint8_t *name, size_t length) +{ + unsigned int hash = hash_link_ref(name, length); + struct link_ref *ref = NULL; + + ref = references[hash % REF_TABLE_SIZE]; + + while (ref != NULL) { + if (ref->id == hash) + return ref; + + ref = ref->next; + } + + return NULL; +} + +static void +free_link_refs(struct link_ref **references) +{ + size_t i; + + for (i = 0; i < REF_TABLE_SIZE; ++i) { + struct link_ref *r = references[i]; + struct link_ref *next; + + while (r) { + next = r->next; + hoedown_buffer_free(r->link); + hoedown_buffer_free(r->title); + free(r); + r = next; + } + } +} + +static struct footnote_ref * +create_footnote_ref(struct footnote_list *list, const uint8_t *name, size_t name_size) +{ + struct footnote_ref *ref = hoedown_calloc(1, sizeof(struct footnote_ref)); + + ref->id = hash_link_ref(name, name_size); + + return ref; +} + +static int +add_footnote_ref(struct footnote_list *list, struct footnote_ref *ref) +{ + struct footnote_item *item = hoedown_calloc(1, sizeof(struct footnote_item)); + if (!item) + return 0; + item->ref = ref; + + if (list->head == NULL) { + list->head = list->tail = item; + } else { + list->tail->next = item; + list->tail = item; + } + list->count++; + + return 1; +} + +static struct footnote_ref * +find_footnote_ref(struct footnote_list *list, uint8_t *name, size_t length) +{ + unsigned int hash = hash_link_ref(name, length); + struct footnote_item *item = NULL; + + item = list->head; + + while (item != NULL) { + if (item->ref->id == hash) + return item->ref; + item = item->next; + } + + return NULL; +} + +static void +free_footnote_ref(struct footnote_ref *ref) +{ + hoedown_buffer_free(ref->contents); + free(ref); +} + +static void +free_footnote_list(struct footnote_list *list, int free_refs) +{ + struct footnote_item *item = list->head; + struct footnote_item *next; + + while (item) { + next = item->next; + if (free_refs) + free_footnote_ref(item->ref); + free(item); + item = next; + } +} + + +/* + * Check whether a char is a Markdown spacing char. + + * Right now we only consider spaces the actual + * space and a newline: tabs and carriage returns + * are filtered out during the preprocessing phase. + * + * If we wanted to actually be UTF-8 compliant, we + * should instead extract an Unicode codepoint from + * this character and check for space properties. + */ +static int +_isspace(int c) +{ + return c == ' ' || c == '\n'; +} + +/* is_empty_all: verify that all the data is spacing */ +static int +is_empty_all(const uint8_t *data, size_t size) +{ + size_t i = 0; + while (i < size && _isspace(data[i])) i++; + return i == size; +} + +/* + * Replace all spacing characters in data with spaces. As a special + * case, this collapses a newline with the previous space, if possible. + */ +static void +replace_spacing(hoedown_buffer *ob, const uint8_t *data, size_t size) +{ + size_t i = 0, mark; + hoedown_buffer_grow(ob, size); + while (1) { + mark = i; + while (i < size && data[i] != '\n') i++; + hoedown_buffer_put(ob, data + mark, i - mark); + + if (i >= size) break; + + if (!(i > 0 && data[i-1] == ' ')) + hoedown_buffer_putc(ob, ' '); + i++; + } +} + +/**************************** + * INLINE PARSING FUNCTIONS * + ****************************/ + +/* is_mail_autolink • looks for the address part of a mail autolink and '>' */ +/* this is less strict than the original markdown e-mail address matching */ +static size_t +is_mail_autolink(uint8_t *data, size_t size) +{ + size_t i = 0, nb = 0; + + /* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */ + for (i = 0; i < size; ++i) { + if (isalnum(data[i])) + continue; + + switch (data[i]) { + case '@': + nb++; + + case '-': + case '.': + case '_': + break; + + case '>': + return (nb == 1) ? i + 1 : 0; + + default: + return 0; + } + } + + return 0; +} + +/* tag_length • returns the length of the given tag, or 0 is it's not valid */ +static size_t +tag_length(uint8_t *data, size_t size, hoedown_autolink_type *autolink) +{ + size_t i, j; + + /* a valid tag can't be shorter than 3 chars */ + if (size < 3) return 0; + + if (data[0] != '<') return 0; + + /* HTML comment, laxist form */ + if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') { + i = 5; + + while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>')) + i++; + + i++; + + if (i <= size) + return i; + } + + /* begins with a '<' optionally followed by '/', followed by letter or number */ + i = (data[1] == '/') ? 2 : 1; + + if (!isalnum(data[i])) + return 0; + + /* scheme test */ + *autolink = HOEDOWN_AUTOLINK_NONE; + + /* try to find the beginning of an URI */ + while (i < size && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-')) + i++; + + if (i > 1 && data[i] == '@') { + if ((j = is_mail_autolink(data + i, size - i)) != 0) { + *autolink = HOEDOWN_AUTOLINK_EMAIL; + return i + j; + } + } + + if (i > 2 && data[i] == ':') { + *autolink = HOEDOWN_AUTOLINK_NORMAL; + i++; + } + + /* completing autolink test: no spacing or ' or " */ + if (i >= size) + *autolink = HOEDOWN_AUTOLINK_NONE; + + else if (*autolink) { + j = i; + + while (i < size) { + if (data[i] == '\\') i += 2; + else if (data[i] == '>' || data[i] == '\'' || + data[i] == '"' || data[i] == ' ' || data[i] == '\n') + break; + else i++; + } + + if (i >= size) return 0; + if (i > j && data[i] == '>') return i + 1; + /* one of the forbidden chars has been found */ + *autolink = HOEDOWN_AUTOLINK_NONE; + } + + /* looking for something looking like a tag end */ + while (i < size && data[i] != '>') i++; + if (i >= size) return 0; + return i + 1; +} + +/* parse_inline • parses inline markdown elements */ +static void +parse_inline(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t i = 0, end = 0, consumed = 0; + hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; + uint8_t *active_char = doc->active_char; + + if (doc->work_bufs[BUFFER_SPAN].size + + doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting) + return; + + while (i < size) { + /* copying inactive chars into the output */ + while (end < size && active_char[data[end]] == 0) + end++; + + if (doc->md.normal_text) { + work.data = data + i; + work.size = end - i; + doc->md.normal_text(ob, &work, &doc->data); + } + else + hoedown_buffer_put(ob, data + i, end - i); + + if (end >= size) break; + i = end; + + end = markdown_char_ptrs[ (int)active_char[data[end]] ](ob, doc, data + i, i - consumed, size - i); + if (!end) /* no action from the callback */ + end = i + 1; + else { + i += end; + end = i; + consumed = i; + } + } +} + +/* is_escaped • returns whether special char at data[loc] is escaped by '\\' */ +static int +is_escaped(uint8_t *data, size_t loc) +{ + size_t i = loc; + while (i >= 1 && data[i - 1] == '\\') + i--; + + /* odd numbers of backslashes escapes data[loc] */ + return (loc - i) % 2; +} + +/* find_emph_char • looks for the next emph uint8_t, skipping other constructs */ +static size_t +find_emph_char(uint8_t *data, size_t size, uint8_t c) +{ + size_t i = 0; + + while (i < size) { + while (i < size && data[i] != c && data[i] != '[' && data[i] != '`') + i++; + + if (i == size) + return 0; + + /* not counting escaped chars */ + if (is_escaped(data, i)) { + i++; continue; + } + + if (data[i] == c) + return i; + + /* skipping a codespan */ + if (data[i] == '`') { + size_t span_nb = 0, bt; + size_t tmp_i = 0; + + /* counting the number of opening backticks */ + while (i < size && data[i] == '`') { + i++; span_nb++; + } + + if (i >= size) return 0; + + /* finding the matching closing sequence */ + bt = 0; + while (i < size && bt < span_nb) { + if (!tmp_i && data[i] == c) tmp_i = i; + if (data[i] == '`') bt++; + else bt = 0; + i++; + } + + /* not a well-formed codespan; use found matching emph char */ + if (bt < span_nb && i >= size) return tmp_i; + } + /* skipping a link */ + else if (data[i] == '[') { + size_t tmp_i = 0; + uint8_t cc; + + i++; + while (i < size && data[i] != ']') { + if (!tmp_i && data[i] == c) tmp_i = i; + i++; + } + + i++; + while (i < size && _isspace(data[i])) + i++; + + if (i >= size) + return tmp_i; + + switch (data[i]) { + case '[': + cc = ']'; break; + + case '(': + cc = ')'; break; + + default: + if (tmp_i) + return tmp_i; + else + continue; + } + + i++; + while (i < size && data[i] != cc) { + if (!tmp_i && data[i] == c) tmp_i = i; + i++; + } + + if (i >= size) + return tmp_i; + + i++; + } + } + + return 0; +} + +/* parse_emph1 • parsing single emphase */ +/* closed by a symbol not preceded by spacing and not followed by symbol */ +static size_t +parse_emph1(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) +{ + size_t i = 0, len; + hoedown_buffer *work = 0; + int r; + + /* skipping one symbol if coming from emph3 */ + if (size > 1 && data[0] == c && data[1] == c) i = 1; + + while (i < size) { + len = find_emph_char(data + i, size - i, c); + if (!len) return 0; + i += len; + if (i >= size) return 0; + + if (data[i] == c && !_isspace(data[i - 1])) { + + if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) { + if (i + 1 < size && isalnum(data[i + 1])) + continue; + } + + work = newbuf(doc, BUFFER_SPAN); + parse_inline(work, doc, data, i); + + if (doc->ext_flags & HOEDOWN_EXT_UNDERLINE && c == '_') + r = doc->md.underline(ob, work, &doc->data); + else + r = doc->md.emphasis(ob, work, &doc->data); + + popbuf(doc, BUFFER_SPAN); + return r ? i + 1 : 0; + } + } + + return 0; +} + +/* parse_emph2 • parsing single emphase */ +static size_t +parse_emph2(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) +{ + size_t i = 0, len; + hoedown_buffer *work = 0; + int r; + + while (i < size) { + len = find_emph_char(data + i, size - i, c); + if (!len) return 0; + i += len; + + if (i + 1 < size && data[i] == c && data[i + 1] == c && i && !_isspace(data[i - 1])) { + work = newbuf(doc, BUFFER_SPAN); + parse_inline(work, doc, data, i); + + if (c == '~') + r = doc->md.strikethrough(ob, work, &doc->data); + else if (c == '=') + r = doc->md.highlight(ob, work, &doc->data); + else + r = doc->md.double_emphasis(ob, work, &doc->data); + + popbuf(doc, BUFFER_SPAN); + return r ? i + 2 : 0; + } + i++; + } + return 0; +} + +/* parse_emph3 • parsing single emphase */ +/* finds the first closing tag, and delegates to the other emph */ +static size_t +parse_emph3(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) +{ + size_t i = 0, len; + int r; + + while (i < size) { + len = find_emph_char(data + i, size - i, c); + if (!len) return 0; + i += len; + + /* skip spacing preceded symbols */ + if (data[i] != c || _isspace(data[i - 1])) + continue; + + if (i + 2 < size && data[i + 1] == c && data[i + 2] == c && doc->md.triple_emphasis) { + /* triple symbol found */ + hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); + + parse_inline(work, doc, data, i); + r = doc->md.triple_emphasis(ob, work, &doc->data); + popbuf(doc, BUFFER_SPAN); + return r ? i + 3 : 0; + + } else if (i + 1 < size && data[i + 1] == c) { + /* double symbol found, handing over to emph1 */ + len = parse_emph1(ob, doc, data - 2, size + 2, c); + if (!len) return 0; + else return len - 2; + + } else { + /* single symbol found, handing over to emph2 */ + len = parse_emph2(ob, doc, data - 1, size + 1, c); + if (!len) return 0; + else return len - 1; + } + } + return 0; +} + +/* parse_math • parses a math span until the given ending delimiter */ +static size_t +parse_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size, const char *end, size_t delimsz, int displaymode) +{ + hoedown_buffer text = { NULL, 0, 0, 0, NULL, NULL, NULL }; + size_t i = delimsz; + + if (!doc->md.math) + return 0; + + /* find ending delimiter */ + while (1) { + while (i < size && data[i] != (uint8_t)end[0]) + i++; + + if (i >= size) + return 0; + + if (!is_escaped(data, i) && !(i + delimsz > size) + && memcmp(data + i, end, delimsz) == 0) + break; + + i++; + } + + /* prepare buffers */ + text.data = data + delimsz; + text.size = i - delimsz; + + /* if this is a $$ and MATH_EXPLICIT is not active, + * guess whether displaymode should be enabled from the context */ + i += delimsz; + if (delimsz == 2 && !(doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT)) + displaymode = is_empty_all(data - offset, offset) && is_empty_all(data + i, size - i); + + /* call callback */ + if (doc->md.math(ob, &text, displaymode, &doc->data)) + return i; + + return 0; +} + +/* char_emphasis • single and double emphasis parsing */ +static size_t +char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + uint8_t c = data[0]; + size_t ret; + + if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) { + if (offset > 0 && !_isspace(data[-1]) && data[-1] != '>' && data[-1] != '(') + return 0; + } + + if (size > 2 && data[1] != c) { + /* spacing cannot follow an opening emphasis; + * strikethrough and highlight only takes two characters '~~' */ + if (c == '~' || c == '=' || _isspace(data[1]) || (ret = parse_emph1(ob, doc, data + 1, size - 1, c)) == 0) + return 0; + + return ret + 1; + } + + if (size > 3 && data[1] == c && data[2] != c) { + if (_isspace(data[2]) || (ret = parse_emph2(ob, doc, data + 2, size - 2, c)) == 0) + return 0; + + return ret + 2; + } + + if (size > 4 && data[1] == c && data[2] == c && data[3] != c) { + if (c == '~' || c == '=' || _isspace(data[3]) || (ret = parse_emph3(ob, doc, data + 3, size - 3, c)) == 0) + return 0; + + return ret + 3; + } + + return 0; +} + + +/* char_linebreak • '\n' preceded by two spaces (assuming linebreak != 0) */ +static size_t +char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + if (offset < 2 || data[-1] != ' ' || data[-2] != ' ') + return 0; + + /* removing the last space from ob and rendering */ + while (ob->size && ob->data[ob->size - 1] == ' ') + ob->size--; + + return doc->md.linebreak(ob, &doc->data) ? 1 : 0; +} + + +/* char_codespan • '`' parsing a code span (assuming codespan != 0) */ +static size_t +char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; + size_t end, nb = 0, i, f_begin, f_end; + + /* counting the number of backticks in the delimiter */ + while (nb < size && data[nb] == '`') + nb++; + + /* finding the next delimiter */ + i = 0; + for (end = nb; end < size && i < nb; end++) { + if (data[end] == '`') i++; + else i = 0; + } + + if (i < nb && end >= size) + return 0; /* no matching delimiter */ + + /* trimming outside spaces */ + f_begin = nb; + while (f_begin < end && data[f_begin] == ' ') + f_begin++; + + f_end = end - nb; + while (f_end > nb && data[f_end-1] == ' ') + f_end--; + + /* real code span */ + if (f_begin < f_end) { + work.data = data + f_begin; + work.size = f_end - f_begin; + + if (!doc->md.codespan(ob, &work, &doc->data)) + end = 0; + } else { + if (!doc->md.codespan(ob, 0, &doc->data)) + end = 0; + } + + return end; +} + +/* char_quote • '"' parsing a quote */ +static size_t +char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + size_t end, nq = 0, i, f_begin, f_end; + + /* counting the number of quotes in the delimiter */ + while (nq < size && data[nq] == '"') + nq++; + + /* finding the next delimiter */ + end = nq; + while (1) { + i = end; + end += find_emph_char(data + end, size - end, '"'); + if (end == i) return 0; /* no matching delimiter */ + i = end; + while (end < size && data[end] == '"' && end - i < nq) end++; + if (end - i >= nq) break; + } + + /* trimming outside spaces */ + f_begin = nq; + while (f_begin < end && data[f_begin] == ' ') + f_begin++; + + f_end = end - nq; + while (f_end > nq && data[f_end-1] == ' ') + f_end--; + + /* real quote */ + if (f_begin < f_end) { + hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); + parse_inline(work, doc, data + f_begin, f_end - f_begin); + + if (!doc->md.quote(ob, work, &doc->data)) + end = 0; + popbuf(doc, BUFFER_SPAN); + } else { + if (!doc->md.quote(ob, 0, &doc->data)) + end = 0; + } + + return end; +} + + +/* char_escape • '\\' backslash escape */ +static size_t +char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + static const char *escape_chars = "\\`*_{}[]()#+-.!:|&<>^~=\"$"; + hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; + size_t w; + + if (size > 1) { + if (data[1] == '\\' && (doc->ext_flags & HOEDOWN_EXT_MATH) && + size > 2 && (data[2] == '(' || data[2] == '[')) { + const char *end = (data[2] == '[') ? "\\\\]" : "\\\\)"; + w = parse_math(ob, doc, data, offset, size, end, 3, data[2] == '['); + if (w) return w; + } + + if (strchr(escape_chars, data[1]) == NULL) + return 0; + + if (doc->md.normal_text) { + work.data = data + 1; + work.size = 1; + doc->md.normal_text(ob, &work, &doc->data); + } + else hoedown_buffer_putc(ob, data[1]); + } else if (size == 1) { + if (doc->md.normal_text) { + work.data = data; + work.size = 1; + doc->md.normal_text(ob, &work, &doc->data); + } + else hoedown_buffer_putc(ob, data[0]); + } + + return 2; +} + +/* char_entity • '&' escaped when it doesn't belong to an entity */ +/* valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */ +static size_t +char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + size_t end = 1; + hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; + + if (end < size && data[end] == '#') + end++; + + while (end < size && isalnum(data[end])) + end++; + + if (end < size && data[end] == ';') + end++; /* real entity */ + else + return 0; /* lone '&' */ + + if (doc->md.entity) { + work.data = data; + work.size = end; + doc->md.entity(ob, &work, &doc->data); + } + else hoedown_buffer_put(ob, data, end); + + return end; +} + +/* char_langle_tag • '<' when tags or autolinks are allowed */ +static size_t +char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; + hoedown_autolink_type altype = HOEDOWN_AUTOLINK_NONE; + size_t end = tag_length(data, size, &altype); + int ret = 0; + + work.data = data; + work.size = end; + + if (end > 2) { + if (doc->md.autolink && altype != HOEDOWN_AUTOLINK_NONE) { + hoedown_buffer *u_link = newbuf(doc, BUFFER_SPAN); + work.data = data + 1; + work.size = end - 2; + unscape_text(u_link, &work); + ret = doc->md.autolink(ob, u_link, altype, &doc->data); + popbuf(doc, BUFFER_SPAN); + } + else if (doc->md.raw_html) + ret = doc->md.raw_html(ob, &work, &doc->data); + } + + if (!ret) return 0; + else return end; +} + +static size_t +char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer *link, *link_url, *link_text; + size_t link_len, rewind; + + if (!doc->md.link || doc->in_link_body) + return 0; + + link = newbuf(doc, BUFFER_SPAN); + + if ((link_len = hoedown_autolink__www(&rewind, link, data, offset, size, HOEDOWN_AUTOLINK_SHORT_DOMAINS)) > 0) { + link_url = newbuf(doc, BUFFER_SPAN); + HOEDOWN_BUFPUTSL(link_url, "http://"); + hoedown_buffer_put(link_url, link->data, link->size); + + if (ob->size > rewind) + ob->size -= rewind; + else + ob->size = 0; + + if (doc->md.normal_text) { + link_text = newbuf(doc, BUFFER_SPAN); + doc->md.normal_text(link_text, link, &doc->data); + doc->md.link(ob, link_text, link_url, NULL, &doc->data); + popbuf(doc, BUFFER_SPAN); + } else { + doc->md.link(ob, link, link_url, NULL, &doc->data); + } + popbuf(doc, BUFFER_SPAN); + } + + popbuf(doc, BUFFER_SPAN); + return link_len; +} + +static size_t +char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer *link; + size_t link_len, rewind; + + if (!doc->md.autolink || doc->in_link_body) + return 0; + + link = newbuf(doc, BUFFER_SPAN); + + if ((link_len = hoedown_autolink__email(&rewind, link, data, offset, size, 0)) > 0) { + if (ob->size > rewind) + ob->size -= rewind; + else + ob->size = 0; + + doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_EMAIL, &doc->data); + } + + popbuf(doc, BUFFER_SPAN); + return link_len; +} + +static size_t +char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer *link; + size_t link_len, rewind; + + if (!doc->md.autolink || doc->in_link_body) + return 0; + + link = newbuf(doc, BUFFER_SPAN); + + if ((link_len = hoedown_autolink__url(&rewind, link, data, offset, size, 0)) > 0) { + if (ob->size > rewind) + ob->size -= rewind; + else + ob->size = 0; + + doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_NORMAL, &doc->data); + } + + popbuf(doc, BUFFER_SPAN); + return link_len; +} + +static size_t +char_image(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) { + size_t ret; + + if (size < 2 || data[1] != '[') return 0; + + ret = char_link(ob, doc, data + 1, offset + 1, size - 1); + if (!ret) return 0; + return ret + 1; +} + +/* char_link • '[': parsing a link, a footnote or an image */ +static size_t +char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + int is_img = (offset && data[-1] == '!' && !is_escaped(data - offset, offset - 1)); + int is_footnote = (doc->ext_flags & HOEDOWN_EXT_FOOTNOTES && data[1] == '^'); + size_t i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0; + hoedown_buffer *content = NULL; + hoedown_buffer *link = NULL; + hoedown_buffer *title = NULL; + hoedown_buffer *u_link = NULL; + size_t org_work_size = doc->work_bufs[BUFFER_SPAN].size; + int ret = 0, in_title = 0, qtype = 0; + + /* checking whether the correct renderer exists */ + if ((is_footnote && !doc->md.footnote_ref) || (is_img && !doc->md.image) + || (!is_img && !is_footnote && !doc->md.link)) + goto cleanup; + + /* looking for the matching closing bracket */ + i += find_emph_char(data + i, size - i, ']'); + txt_e = i; + + if (i < size && data[i] == ']') i++; + else goto cleanup; + + /* footnote link */ + if (is_footnote) { + hoedown_buffer id = { NULL, 0, 0, 0, NULL, NULL, NULL }; + struct footnote_ref *fr; + + if (txt_e < 3) + goto cleanup; + + id.data = data + 2; + id.size = txt_e - 2; + + fr = find_footnote_ref(&doc->footnotes_found, id.data, id.size); + + /* mark footnote used */ + if (fr && !fr->is_used) { + if(!add_footnote_ref(&doc->footnotes_used, fr)) + goto cleanup; + fr->is_used = 1; + fr->num = doc->footnotes_used.count; + + /* render */ + if (doc->md.footnote_ref) + ret = doc->md.footnote_ref(ob, fr->num, &doc->data); + } + + goto cleanup; + } + + /* skip any amount of spacing */ + /* (this is much more laxist than original markdown syntax) */ + while (i < size && _isspace(data[i])) + i++; + + /* inline style link */ + if (i < size && data[i] == '(') { + size_t nb_p; + + /* skipping initial spacing */ + i++; + + while (i < size && _isspace(data[i])) + i++; + + link_b = i; + + /* looking for link end: ' " ) */ + /* Count the number of open parenthesis */ + nb_p = 0; + + while (i < size) { + if (data[i] == '\\') i += 2; + else if (data[i] == '(' && i != 0) { + nb_p++; i++; + } + else if (data[i] == ')') { + if (nb_p == 0) break; + else nb_p--; i++; + } else if (i >= 1 && _isspace(data[i-1]) && (data[i] == '\'' || data[i] == '"')) break; + else i++; + } + + if (i >= size) goto cleanup; + link_e = i; + + /* looking for title end if present */ + if (data[i] == '\'' || data[i] == '"') { + qtype = data[i]; + in_title = 1; + i++; + title_b = i; + + while (i < size) { + if (data[i] == '\\') i += 2; + else if (data[i] == qtype) {in_title = 0; i++;} + else if ((data[i] == ')') && !in_title) break; + else i++; + } + + if (i >= size) goto cleanup; + + /* skipping spacing after title */ + title_e = i - 1; + while (title_e > title_b && _isspace(data[title_e])) + title_e--; + + /* checking for closing quote presence */ + if (data[title_e] != '\'' && data[title_e] != '"') { + title_b = title_e = 0; + link_e = i; + } + } + + /* remove spacing at the end of the link */ + while (link_e > link_b && _isspace(data[link_e - 1])) + link_e--; + + /* remove optional angle brackets around the link */ + if (data[link_b] == '<' && data[link_e - 1] == '>') { + link_b++; + link_e--; + } + + /* building escaped link and title */ + if (link_e > link_b) { + link = newbuf(doc, BUFFER_SPAN); + hoedown_buffer_put(link, data + link_b, link_e - link_b); + } + + if (title_e > title_b) { + title = newbuf(doc, BUFFER_SPAN); + hoedown_buffer_put(title, data + title_b, title_e - title_b); + } + + i++; + } + + /* reference style link */ + else if (i < size && data[i] == '[') { + hoedown_buffer *id = newbuf(doc, BUFFER_SPAN); + struct link_ref *lr; + + /* looking for the id */ + i++; + link_b = i; + while (i < size && data[i] != ']') i++; + if (i >= size) goto cleanup; + link_e = i; + + /* finding the link_ref */ + if (link_b == link_e) + replace_spacing(id, data + 1, txt_e - 1); + else + hoedown_buffer_put(id, data + link_b, link_e - link_b); + + lr = find_link_ref(doc->refs, id->data, id->size); + if (!lr) + goto cleanup; + + /* keeping link and title from link_ref */ + link = lr->link; + title = lr->title; + i++; + } + + /* shortcut reference style link */ + else { + hoedown_buffer *id = newbuf(doc, BUFFER_SPAN); + struct link_ref *lr; + + /* crafting the id */ + replace_spacing(id, data + 1, txt_e - 1); + + /* finding the link_ref */ + lr = find_link_ref(doc->refs, id->data, id->size); + if (!lr) + goto cleanup; + + /* keeping link and title from link_ref */ + link = lr->link; + title = lr->title; + + /* rewinding the spacing */ + i = txt_e + 1; + } + + /* building content: img alt is kept, only link content is parsed */ + if (txt_e > 1) { + content = newbuf(doc, BUFFER_SPAN); + if (is_img) { + hoedown_buffer_put(content, data + 1, txt_e - 1); + } else { + /* disable autolinking when parsing inline the + * content of a link */ + doc->in_link_body = 1; + parse_inline(content, doc, data + 1, txt_e - 1); + doc->in_link_body = 0; + } + } + + if (link) { + u_link = newbuf(doc, BUFFER_SPAN); + unscape_text(u_link, link); + } + + /* calling the relevant rendering function */ + if (is_img) { + ret = doc->md.image(ob, u_link, title, content, &doc->data); + } else { + ret = doc->md.link(ob, content, u_link, title, &doc->data); + } + + /* cleanup */ +cleanup: + doc->work_bufs[BUFFER_SPAN].size = (int)org_work_size; + return ret ? i : 0; +} + +static size_t +char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + size_t sup_start, sup_len; + hoedown_buffer *sup; + + if (!doc->md.superscript) + return 0; + + if (size < 2) + return 0; + + if (data[1] == '(') { + sup_start = 2; + sup_len = find_emph_char(data + 2, size - 2, ')') + 2; + + if (sup_len == size) + return 0; + } else { + sup_start = sup_len = 1; + + while (sup_len < size && !_isspace(data[sup_len])) + sup_len++; + } + + if (sup_len - sup_start == 0) + return (sup_start == 2) ? 3 : 0; + + sup = newbuf(doc, BUFFER_SPAN); + parse_inline(sup, doc, data + sup_start, sup_len - sup_start); + doc->md.superscript(ob, sup, &doc->data); + popbuf(doc, BUFFER_SPAN); + + return (sup_start == 2) ? sup_len + 1 : sup_len; +} + +static size_t +char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + /* double dollar */ + if (size > 1 && data[1] == '$') + return parse_math(ob, doc, data, offset, size, "$$", 2, 1); + + /* single dollar allowed only with MATH_EXPLICIT flag */ + if (doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT) + return parse_math(ob, doc, data, offset, size, "$", 1, 0); + + return 0; +} + +/********************************* + * BLOCK-LEVEL PARSING FUNCTIONS * + *********************************/ + +/* is_empty • returns the line length when it is empty, 0 otherwise */ +static size_t +is_empty(const uint8_t *data, size_t size) +{ + size_t i; + + for (i = 0; i < size && data[i] != '\n'; i++) + if (data[i] != ' ') + return 0; + + return i + 1; +} + +/* is_hrule • returns whether a line is a horizontal rule */ +static int +is_hrule(uint8_t *data, size_t size) +{ + size_t i = 0, n = 0; + uint8_t c; + + /* skipping initial spaces */ + if (size < 3) return 0; + if (data[0] == ' ') { i++; + if (data[1] == ' ') { i++; + if (data[2] == ' ') { i++; } } } + + /* looking at the hrule uint8_t */ + if (i + 2 >= size + || (data[i] != '*' && data[i] != '-' && data[i] != '_')) + return 0; + c = data[i]; + + /* the whole line must be the char or space */ + while (i < size && data[i] != '\n') { + if (data[i] == c) n++; + else if (data[i] != ' ') + return 0; + + i++; + } + + return n >= 3; +} + +/* check if a line is a code fence; return the + * end of the code fence. if passed, width of + * the fence rule and character will be returned */ +static size_t +is_codefence(uint8_t *data, size_t size, size_t *width, uint8_t *chr) +{ + size_t i = 0, n = 1; + uint8_t c; + + /* skipping initial spaces */ + if (size < 3) + return 0; + + if (data[0] == ' ') { i++; + if (data[1] == ' ') { i++; + if (data[2] == ' ') { i++; } } } + + /* looking at the hrule uint8_t */ + c = data[i]; + if (i + 2 >= size || !(c=='~' || c=='`')) + return 0; + + /* the fence must be that same character */ + while (++i < size && data[i] == c) + ++n; + + if (n < 3) + return 0; + + if (width) *width = n; + if (chr) *chr = c; + return i; +} + +/* expects single line, checks if it's a codefence and extracts language */ +static size_t +parse_codefence(uint8_t *data, size_t size, hoedown_buffer *lang, size_t *width, uint8_t *chr) +{ + size_t i, w, lang_start; + + i = w = is_codefence(data, size, width, chr); + if (i == 0) + return 0; + + while (i < size && _isspace(data[i])) + i++; + + lang_start = i; + + while (i < size && !_isspace(data[i])) + i++; + + lang->data = data + lang_start; + lang->size = i - lang_start; + + /* Avoid parsing a codespan as a fence */ + i = lang_start + 2; + while (i < size && !(data[i] == *chr && data[i-1] == *chr && data[i-2] == *chr)) i++; + if (i < size) return 0; + + return w; +} + +/* is_atxheader • returns whether the line is a hash-prefixed header */ +static int +is_atxheader(hoedown_document *doc, uint8_t *data, size_t size) +{ + if (data[0] != '#') + return 0; + + if (doc->ext_flags & HOEDOWN_EXT_SPACE_HEADERS) { + size_t level = 0; + + while (level < size && level < 6 && data[level] == '#') + level++; + + if (level < size && data[level] != ' ') + return 0; + } + + return 1; +} + +/* is_headerline • returns whether the line is a setext-style hdr underline */ +static int +is_headerline(uint8_t *data, size_t size) +{ + size_t i = 0; + + /* test of level 1 header */ + if (data[i] == '=') { + for (i = 1; i < size && data[i] == '='; i++); + while (i < size && data[i] == ' ') i++; + return (i >= size || data[i] == '\n') ? 1 : 0; } + + /* test of level 2 header */ + if (data[i] == '-') { + for (i = 1; i < size && data[i] == '-'; i++); + while (i < size && data[i] == ' ') i++; + return (i >= size || data[i] == '\n') ? 2 : 0; } + + return 0; +} + +static int +is_next_headerline(uint8_t *data, size_t size) +{ + size_t i = 0; + + while (i < size && data[i] != '\n') + i++; + + if (++i >= size) + return 0; + + return is_headerline(data + i, size - i); +} + +/* prefix_quote • returns blockquote prefix length */ +static size_t +prefix_quote(uint8_t *data, size_t size) +{ + size_t i = 0; + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + + if (i < size && data[i] == '>') { + if (i + 1 < size && data[i + 1] == ' ') + return i + 2; + + return i + 1; + } + + return 0; +} + +/* prefix_code • returns prefix length for block code*/ +static size_t +prefix_code(uint8_t *data, size_t size) +{ + if (size > 3 && data[0] == ' ' && data[1] == ' ' + && data[2] == ' ' && data[3] == ' ') return 4; + + return 0; +} + +/* prefix_oli • returns ordered list item prefix */ +static size_t +prefix_oli(uint8_t *data, size_t size) +{ + size_t i = 0; + + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + + if (i >= size || data[i] < '0' || data[i] > '9') + return 0; + + while (i < size && data[i] >= '0' && data[i] <= '9') + i++; + + if (i + 1 >= size || data[i] != '.' || data[i + 1] != ' ') + return 0; + + if (is_next_headerline(data + i, size - i)) + return 0; + + return i + 2; +} + +/* prefix_uli • returns ordered list item prefix */ +static size_t +prefix_uli(uint8_t *data, size_t size) +{ + size_t i = 0; + + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + + if (i + 1 >= size || + (data[i] != '*' && data[i] != '+' && data[i] != '-') || + data[i + 1] != ' ') + return 0; + + if (is_next_headerline(data + i, size - i)) + return 0; + + return i + 2; +} + + +/* parse_block • parsing of one block, returning next uint8_t to parse */ +static void parse_block(hoedown_buffer *ob, hoedown_document *doc, + uint8_t *data, size_t size); + + +/* parse_blockquote • handles parsing of a blockquote fragment */ +static size_t +parse_blockquote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t beg, end = 0, pre, work_size = 0; + uint8_t *work_data = 0; + hoedown_buffer *out = 0; + + out = newbuf(doc, BUFFER_BLOCK); + beg = 0; + while (beg < size) { + for (end = beg + 1; end < size && data[end - 1] != '\n'; end++); + + pre = prefix_quote(data + beg, end - beg); + + if (pre) + beg += pre; /* skipping prefix */ + + /* empty line followed by non-quote line */ + else if (is_empty(data + beg, end - beg) && + (end >= size || (prefix_quote(data + end, size - end) == 0 && + !is_empty(data + end, size - end)))) + break; + + if (beg < end) { /* copy into the in-place working buffer */ + /* hoedown_buffer_put(work, data + beg, end - beg); */ + if (!work_data) + work_data = data + beg; + else if (data + beg != work_data + work_size) + memmove(work_data + work_size, data + beg, end - beg); + work_size += end - beg; + } + beg = end; + } + + parse_block(out, doc, work_data, work_size); + if (doc->md.blockquote) + doc->md.blockquote(ob, out, &doc->data); + popbuf(doc, BUFFER_BLOCK); + return end; +} + +static size_t +parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render); + +/* parse_blockquote • handles parsing of a regular paragraph */ +static size_t +parse_paragraph(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; + size_t i = 0, end = 0; + int level = 0; + + work.data = data; + + while (i < size) { + for (end = i + 1; end < size && data[end - 1] != '\n'; end++) /* empty */; + + if (is_empty(data + i, size - i)) + break; + + if ((level = is_headerline(data + i, size - i)) != 0) + break; + + if (is_atxheader(doc, data + i, size - i) || + is_hrule(data + i, size - i) || + prefix_quote(data + i, size - i)) { + end = i; + break; + } + + i = end; + } + + work.size = i; + while (work.size && data[work.size - 1] == '\n') + work.size--; + + if (!level) { + hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK); + parse_inline(tmp, doc, work.data, work.size); + if (doc->md.paragraph) + doc->md.paragraph(ob, tmp, &doc->data); + popbuf(doc, BUFFER_BLOCK); + } else { + hoedown_buffer *header_work; + + if (work.size) { + size_t beg; + i = work.size; + work.size -= 1; + + while (work.size && data[work.size] != '\n') + work.size -= 1; + + beg = work.size + 1; + while (work.size && data[work.size - 1] == '\n') + work.size -= 1; + + if (work.size > 0) { + hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK); + parse_inline(tmp, doc, work.data, work.size); + + if (doc->md.paragraph) + doc->md.paragraph(ob, tmp, &doc->data); + + popbuf(doc, BUFFER_BLOCK); + work.data += beg; + work.size = i - beg; + } + else work.size = i; + } + + header_work = newbuf(doc, BUFFER_SPAN); + parse_inline(header_work, doc, work.data, work.size); + + if (doc->md.header) + doc->md.header(ob, header_work, (int)level, &doc->data); + + popbuf(doc, BUFFER_SPAN); + } + + return end; +} + +/* parse_fencedcode • handles parsing of a block-level code fragment */ +static size_t +parse_fencedcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + hoedown_buffer text = { 0, 0, 0, 0, NULL, NULL, NULL }; + hoedown_buffer lang = { 0, 0, 0, 0, NULL, NULL, NULL }; + size_t i = 0, text_start, line_start; + size_t w, w2; + size_t width, width2; + uint8_t chr, chr2; + + /* parse codefence line */ + while (i < size && data[i] != '\n') + i++; + + w = parse_codefence(data, i, &lang, &width, &chr); + if (!w) + return 0; + + /* search for end */ + i++; + text_start = i; + while ((line_start = i) < size) { + while (i < size && data[i] != '\n') + i++; + + w2 = is_codefence(data + line_start, i - line_start, &width2, &chr2); + if (w == w2 && width == width2 && chr == chr2 && + is_empty(data + (line_start+w), i - (line_start+w))) + break; + + i++; + } + + text.data = data + text_start; + text.size = line_start - text_start; + + if (doc->md.blockcode) + doc->md.blockcode(ob, text.size ? &text : NULL, lang.size ? &lang : NULL, &doc->data); + + return i; +} + +static size_t +parse_blockcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t beg, end, pre; + hoedown_buffer *work = 0; + + work = newbuf(doc, BUFFER_BLOCK); + + beg = 0; + while (beg < size) { + for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {}; + pre = prefix_code(data + beg, end - beg); + + if (pre) + beg += pre; /* skipping prefix */ + else if (!is_empty(data + beg, end - beg)) + /* non-empty non-prefixed line breaks the pre */ + break; + + if (beg < end) { + /* verbatim copy to the working buffer, + escaping entities */ + if (is_empty(data + beg, end - beg)) + hoedown_buffer_putc(work, '\n'); + else hoedown_buffer_put(work, data + beg, end - beg); + } + beg = end; + } + + while (work->size && work->data[work->size - 1] == '\n') + work->size -= 1; + + hoedown_buffer_putc(work, '\n'); + + if (doc->md.blockcode) + doc->md.blockcode(ob, work, NULL, &doc->data); + + popbuf(doc, BUFFER_BLOCK); + return beg; +} + +/* parse_listitem • parsing of a single list item */ +/* assuming initial prefix is already removed */ +static size_t +parse_listitem(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags *flags) +{ + hoedown_buffer *work = 0, *inter = 0; + size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i; + int in_empty = 0, has_inside_empty = 0, in_fence = 0; + + /* keeping track of the first indentation prefix */ + while (orgpre < 3 && orgpre < size && data[orgpre] == ' ') + orgpre++; + + beg = prefix_uli(data, size); + if (!beg) + beg = prefix_oli(data, size); + + if (!beg) + return 0; + + /* skipping to the beginning of the following line */ + end = beg; + while (end < size && data[end - 1] != '\n') + end++; + + /* getting working buffers */ + work = newbuf(doc, BUFFER_SPAN); + inter = newbuf(doc, BUFFER_SPAN); + + /* putting the first line into the working buffer */ + hoedown_buffer_put(work, data + beg, end - beg); + beg = end; + + /* process the following lines */ + while (beg < size) { + size_t has_next_uli = 0, has_next_oli = 0; + + end++; + + while (end < size && data[end - 1] != '\n') + end++; + + /* process an empty line */ + if (is_empty(data + beg, end - beg)) { + in_empty = 1; + beg = end; + continue; + } + + /* calculating the indentation */ + i = 0; + while (i < 4 && beg + i < end && data[beg + i] == ' ') + i++; + + pre = i; + + if (doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) { + if (is_codefence(data + beg + i, end - beg - i, NULL, NULL)) + in_fence = !in_fence; + } + + /* Only check for new list items if we are **not** inside + * a fenced code block */ + if (!in_fence) { + has_next_uli = prefix_uli(data + beg + i, end - beg - i); + has_next_oli = prefix_oli(data + beg + i, end - beg - i); + } + + /* checking for a new item */ + if ((has_next_uli && !is_hrule(data + beg + i, end - beg - i)) || has_next_oli) { + if (in_empty) + has_inside_empty = 1; + + /* the following item must have the same (or less) indentation */ + if (pre <= orgpre) { + /* if the following item has different list type, we end this list */ + if (in_empty && ( + ((*flags & HOEDOWN_LIST_ORDERED) && has_next_uli) || + (!(*flags & HOEDOWN_LIST_ORDERED) && has_next_oli))) + *flags |= HOEDOWN_LI_END; + + break; + } + + if (!sublist) + sublist = work->size; + } + /* joining only indented stuff after empty lines; + * note that now we only require 1 space of indentation + * to continue a list */ + else if (in_empty && pre == 0) { + *flags |= HOEDOWN_LI_END; + break; + } + + if (in_empty) { + hoedown_buffer_putc(work, '\n'); + has_inside_empty = 1; + in_empty = 0; + } + + /* adding the line without prefix into the working buffer */ + hoedown_buffer_put(work, data + beg + i, end - beg - i); + beg = end; + } + + /* render of li contents */ + if (has_inside_empty) + *flags |= HOEDOWN_LI_BLOCK; + + if (*flags & HOEDOWN_LI_BLOCK) { + /* intermediate render of block li */ + if (sublist && sublist < work->size) { + parse_block(inter, doc, work->data, sublist); + parse_block(inter, doc, work->data + sublist, work->size - sublist); + } + else + parse_block(inter, doc, work->data, work->size); + } else { + /* intermediate render of inline li */ + if (sublist && sublist < work->size) { + parse_inline(inter, doc, work->data, sublist); + parse_block(inter, doc, work->data + sublist, work->size - sublist); + } + else + parse_inline(inter, doc, work->data, work->size); + } + + /* render of li itself */ + if (doc->md.listitem) + doc->md.listitem(ob, inter, *flags, &doc->data); + + popbuf(doc, BUFFER_SPAN); + popbuf(doc, BUFFER_SPAN); + return beg; +} + + +/* parse_list • parsing ordered or unordered list block */ +static size_t +parse_list(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags flags) +{ + hoedown_buffer *work = 0; + size_t i = 0, j; + + work = newbuf(doc, BUFFER_BLOCK); + + while (i < size) { + j = parse_listitem(work, doc, data + i, size - i, &flags); + i += j; + + if (!j || (flags & HOEDOWN_LI_END)) + break; + } + + if (doc->md.list) + doc->md.list(ob, work, flags, &doc->data); + popbuf(doc, BUFFER_BLOCK); + return i; +} + +/* parse_atxheader • parsing of atx-style headers */ +static size_t +parse_atxheader(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t level = 0; + size_t i, end, skip; + + while (level < size && level < 6 && data[level] == '#') + level++; + + for (i = level; i < size && data[i] == ' '; i++); + + for (end = i; end < size && data[end] != '\n'; end++); + skip = end; + + while (end && data[end - 1] == '#') + end--; + + while (end && data[end - 1] == ' ') + end--; + + if (end > i) { + hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); + + parse_inline(work, doc, data + i, end - i); + + if (doc->md.header) + doc->md.header(ob, work, (int)level, &doc->data); + + popbuf(doc, BUFFER_SPAN); + } + + return skip; +} + +/* parse_footnote_def • parse a single footnote definition */ +static void +parse_footnote_def(hoedown_buffer *ob, hoedown_document *doc, unsigned int num, uint8_t *data, size_t size) +{ + hoedown_buffer *work = 0; + work = newbuf(doc, BUFFER_SPAN); + + parse_block(work, doc, data, size); + + if (doc->md.footnote_def) + doc->md.footnote_def(ob, work, num, &doc->data); + popbuf(doc, BUFFER_SPAN); +} + +/* parse_footnote_list • render the contents of the footnotes */ +static void +parse_footnote_list(hoedown_buffer *ob, hoedown_document *doc, struct footnote_list *footnotes) +{ + hoedown_buffer *work = 0; + struct footnote_item *item; + struct footnote_ref *ref; + + if (footnotes->count == 0) + return; + + work = newbuf(doc, BUFFER_BLOCK); + + item = footnotes->head; + while (item) { + ref = item->ref; + parse_footnote_def(work, doc, ref->num, ref->contents->data, ref->contents->size); + item = item->next; + } + + if (doc->md.footnotes) + doc->md.footnotes(ob, work, &doc->data); + popbuf(doc, BUFFER_BLOCK); +} + +/* htmlblock_is_end • check for end of HTML block : ( *)\n */ +/* returns tag length on match, 0 otherwise */ +/* assumes data starts with "<" */ +static size_t +htmlblock_is_end( + const char *tag, + size_t tag_len, + hoedown_document *doc, + uint8_t *data, + size_t size) +{ + size_t i = tag_len + 3, w; + + /* try to match the end tag */ + /* note: we're not considering tags like "" which are still valid */ + if (i > size || + data[1] != '/' || + strncasecmp((char *)data + 2, tag, tag_len) != 0 || + data[tag_len + 2] != '>') + return 0; + + /* rest of the line must be empty */ + if ((w = is_empty(data + i, size - i)) == 0 && i < size) + return 0; + + return i + w; +} + +/* htmlblock_find_end • try to find HTML block ending tag */ +/* returns the length on match, 0 otherwise */ +static size_t +htmlblock_find_end( + const char *tag, + size_t tag_len, + hoedown_document *doc, + uint8_t *data, + size_t size) +{ + size_t i = 0, w; + + while (1) { + while (i < size && data[i] != '<') i++; + if (i >= size) return 0; + + w = htmlblock_is_end(tag, tag_len, doc, data + i, size - i); + if (w) return i + w; + i++; + } +} + +/* htmlblock_find_end_strict • try to find end of HTML block in strict mode */ +/* (it must be an unindented line, and have a blank line afterwads) */ +/* returns the length on match, 0 otherwise */ +static size_t +htmlblock_find_end_strict( + const char *tag, + size_t tag_len, + hoedown_document *doc, + uint8_t *data, + size_t size) +{ + size_t i = 0, mark; + + while (1) { + mark = i; + while (i < size && data[i] != '\n') i++; + if (i < size) i++; + if (i == mark) return 0; + + if (data[mark] == ' ' && mark > 0) continue; + mark += htmlblock_find_end(tag, tag_len, doc, data + mark, i - mark); + if (mark == i && (is_empty(data + i, size - i) || i >= size)) break; + } + + return i; +} + +/* parse_htmlblock • parsing of inline HTML block */ +static size_t +parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render) +{ + hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; + size_t i, j = 0, tag_len, tag_end; + const char *curtag = NULL; + + work.data = data; + + /* identification of the opening tag */ + if (size < 2 || data[0] != '<') + return 0; + + i = 1; + while (i < size && data[i] != '>' && data[i] != ' ') + i++; + + if (i < size) + curtag = hoedown_find_block_tag((char *)data + 1, (int)i - 1); + + /* handling of special cases */ + if (!curtag) { + + /* HTML comment, laxist form */ + if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') { + i = 5; + + while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>')) + i++; + + i++; + + if (i < size) + j = is_empty(data + i, size - i); + + if (j) { + work.size = i + j; + if (do_render && doc->md.blockhtml) + doc->md.blockhtml(ob, &work, &doc->data); + return work.size; + } + } + + /* HR, which is the only self-closing block tag considered */ + if (size > 4 && (data[1] == 'h' || data[1] == 'H') && (data[2] == 'r' || data[2] == 'R')) { + i = 3; + while (i < size && data[i] != '>') + i++; + + if (i + 1 < size) { + i++; + j = is_empty(data + i, size - i); + if (j) { + work.size = i + j; + if (do_render && doc->md.blockhtml) + doc->md.blockhtml(ob, &work, &doc->data); + return work.size; + } + } + } + + /* no special case recognised */ + return 0; + } + + /* looking for a matching closing tag in strict mode */ + tag_len = strlen(curtag); + tag_end = htmlblock_find_end_strict(curtag, tag_len, doc, data, size); + + /* if not found, trying a second pass looking for indented match */ + /* but not if tag is "ins" or "del" (following original Markdown.pl) */ + if (!tag_end && strcmp(curtag, "ins") != 0 && strcmp(curtag, "del") != 0) + tag_end = htmlblock_find_end(curtag, tag_len, doc, data, size); + + if (!tag_end) + return 0; + + /* the end of the block has been found */ + work.size = tag_end; + if (do_render && doc->md.blockhtml) + doc->md.blockhtml(ob, &work, &doc->data); + + return tag_end; +} + +static void +parse_table_row( + hoedown_buffer *ob, + hoedown_document *doc, + uint8_t *data, + size_t size, + size_t columns, + hoedown_table_flags *col_data, + hoedown_table_flags header_flag) +{ + size_t i = 0, col, len; + hoedown_buffer *row_work = 0; + + if (!doc->md.table_cell || !doc->md.table_row) + return; + + row_work = newbuf(doc, BUFFER_SPAN); + + if (i < size && data[i] == '|') + i++; + + for (col = 0; col < columns && i < size; ++col) { + size_t cell_start, cell_end; + hoedown_buffer *cell_work; + + cell_work = newbuf(doc, BUFFER_SPAN); + + while (i < size && _isspace(data[i])) + i++; + + cell_start = i; + + len = find_emph_char(data + i, size - i, '|'); + + /* Two possibilities for len == 0: + 1) No more pipe char found in the current line. + 2) The next pipe is right after the current one, i.e. empty cell. + For case 1, we skip to the end of line; for case 2 we just continue. + */ + if (len == 0 && i < size && data[i] != '|') + len = size - i; + i += len; + + cell_end = i - 1; + + while (cell_end > cell_start && _isspace(data[cell_end])) + cell_end--; + + parse_inline(cell_work, doc, data + cell_start, 1 + cell_end - cell_start); + doc->md.table_cell(row_work, cell_work, col_data[col] | header_flag, &doc->data); + + popbuf(doc, BUFFER_SPAN); + i++; + } + + for (; col < columns; ++col) { + hoedown_buffer empty_cell = { 0, 0, 0, 0, NULL, NULL, NULL }; + doc->md.table_cell(row_work, &empty_cell, col_data[col] | header_flag, &doc->data); + } + + doc->md.table_row(ob, row_work, &doc->data); + + popbuf(doc, BUFFER_SPAN); +} + +static size_t +parse_table_header( + hoedown_buffer *ob, + hoedown_document *doc, + uint8_t *data, + size_t size, + size_t *columns, + hoedown_table_flags **column_data) +{ + int pipes; + size_t i = 0, col, header_end, under_end; + + pipes = 0; + while (i < size && data[i] != '\n') + if (data[i++] == '|') + pipes++; + + if (i == size || pipes == 0) + return 0; + + header_end = i; + + while (header_end > 0 && _isspace(data[header_end - 1])) + header_end--; + + if (data[0] == '|') + pipes--; + + if (header_end && data[header_end - 1] == '|') + pipes--; + + if (pipes < 0) + return 0; + + *columns = pipes + 1; + *column_data = hoedown_calloc(*columns, sizeof(hoedown_table_flags)); + + /* Parse the header underline */ + i++; + if (i < size && data[i] == '|') + i++; + + under_end = i; + while (under_end < size && data[under_end] != '\n') + under_end++; + + for (col = 0; col < *columns && i < under_end; ++col) { + size_t dashes = 0; + + while (i < under_end && data[i] == ' ') + i++; + + if (data[i] == ':') { + i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_LEFT; + dashes++; + } + + while (i < under_end && data[i] == '-') { + i++; dashes++; + } + + if (i < under_end && data[i] == ':') { + i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_RIGHT; + dashes++; + } + + while (i < under_end && data[i] == ' ') + i++; + + if (i < under_end && data[i] != '|' && data[i] != '+') + break; + + if (dashes < 3) + break; + + i++; + } + + if (col < *columns) + return 0; + + parse_table_row( + ob, doc, data, + header_end, + *columns, + *column_data, + HOEDOWN_TABLE_HEADER + ); + + return under_end + 1; +} + +static size_t +parse_table( + hoedown_buffer *ob, + hoedown_document *doc, + uint8_t *data, + size_t size) +{ + size_t i; + + hoedown_buffer *work = 0; + hoedown_buffer *header_work = 0; + hoedown_buffer *body_work = 0; + + size_t columns; + hoedown_table_flags *col_data = NULL; + + work = newbuf(doc, BUFFER_BLOCK); + header_work = newbuf(doc, BUFFER_SPAN); + body_work = newbuf(doc, BUFFER_BLOCK); + + i = parse_table_header(header_work, doc, data, size, &columns, &col_data); + if (i > 0) { + + while (i < size) { + size_t row_start; + int pipes = 0; + + row_start = i; + + while (i < size && data[i] != '\n') + if (data[i++] == '|') + pipes++; + + if (pipes == 0 || i == size) { + i = row_start; + break; + } + + parse_table_row( + body_work, + doc, + data + row_start, + i - row_start, + columns, + col_data, 0 + ); + + i++; + } + + if (doc->md.table_header) + doc->md.table_header(work, header_work, &doc->data); + + if (doc->md.table_body) + doc->md.table_body(work, body_work, &doc->data); + + if (doc->md.table) + doc->md.table(ob, work, &doc->data); + } + + free(col_data); + popbuf(doc, BUFFER_SPAN); + popbuf(doc, BUFFER_BLOCK); + popbuf(doc, BUFFER_BLOCK); + return i; +} + +/* parse_block • parsing of one block, returning next uint8_t to parse */ +static void +parse_block(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t beg, end, i; + uint8_t *txt_data; + beg = 0; + + if (doc->work_bufs[BUFFER_SPAN].size + + doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting) + return; + + while (beg < size) { + txt_data = data + beg; + end = size - beg; + + if (is_atxheader(doc, txt_data, end)) + beg += parse_atxheader(ob, doc, txt_data, end); + + else if (data[beg] == '<' && doc->md.blockhtml && + (i = parse_htmlblock(ob, doc, txt_data, end, 1)) != 0) + beg += i; + + else if ((i = is_empty(txt_data, end)) != 0) + beg += i; + + else if (is_hrule(txt_data, end)) { + if (doc->md.hrule) + doc->md.hrule(ob, &doc->data); + + while (beg < size && data[beg] != '\n') + beg++; + + beg++; + } + + else if ((doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) != 0 && + (i = parse_fencedcode(ob, doc, txt_data, end)) != 0) + beg += i; + + else if ((doc->ext_flags & HOEDOWN_EXT_TABLES) != 0 && + (i = parse_table(ob, doc, txt_data, end)) != 0) + beg += i; + + else if (prefix_quote(txt_data, end)) + beg += parse_blockquote(ob, doc, txt_data, end); + + else if (!(doc->ext_flags & HOEDOWN_EXT_DISABLE_INDENTED_CODE) && prefix_code(txt_data, end)) + beg += parse_blockcode(ob, doc, txt_data, end); + + else if (prefix_uli(txt_data, end)) + beg += parse_list(ob, doc, txt_data, end, 0); + + else if (prefix_oli(txt_data, end)) + beg += parse_list(ob, doc, txt_data, end, HOEDOWN_LIST_ORDERED); + + else + beg += parse_paragraph(ob, doc, txt_data, end); + } +} + + + +/********************* + * REFERENCE PARSING * + *********************/ + +/* is_footnote • returns whether a line is a footnote definition or not */ +static int +is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct footnote_list *list) +{ + size_t i = 0; + hoedown_buffer *contents = 0; + size_t ind = 0; + int in_empty = 0; + size_t start = 0; + + size_t id_offset, id_end; + + /* up to 3 optional leading spaces */ + if (beg + 3 >= end) return 0; + if (data[beg] == ' ') { i = 1; + if (data[beg + 1] == ' ') { i = 2; + if (data[beg + 2] == ' ') { i = 3; + if (data[beg + 3] == ' ') return 0; } } } + i += beg; + + /* id part: caret followed by anything between brackets */ + if (data[i] != '[') return 0; + i++; + if (i >= end || data[i] != '^') return 0; + i++; + id_offset = i; + while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']') + i++; + if (i >= end || data[i] != ']') return 0; + id_end = i; + + /* spacer: colon (space | tab)* newline? (space | tab)* */ + i++; + if (i >= end || data[i] != ':') return 0; + i++; + + /* getting content buffer */ + contents = hoedown_buffer_new(64); + + start = i; + + /* process lines similar to a list item */ + while (i < end) { + while (i < end && data[i] != '\n' && data[i] != '\r') i++; + + /* process an empty line */ + if (is_empty(data + start, i - start)) { + in_empty = 1; + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; + } + start = i; + continue; + } + + /* calculating the indentation */ + ind = 0; + while (ind < 4 && start + ind < end && data[start + ind] == ' ') + ind++; + + /* joining only indented stuff after empty lines; + * note that now we only require 1 space of indentation + * to continue, just like lists */ + if (ind == 0) { + if (start == id_end + 2 && data[start] == '\t') {} + else break; + } + else if (in_empty) { + hoedown_buffer_putc(contents, '\n'); + } + + in_empty = 0; + + /* adding the line into the content buffer */ + hoedown_buffer_put(contents, data + start + ind, i - start - ind); + /* add carriage return */ + if (i < end) { + hoedown_buffer_putc(contents, '\n'); + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; + } + } + start = i; + } + + if (last) + *last = start; + + if (list) { + struct footnote_ref *ref; + ref = create_footnote_ref(list, data + id_offset, id_end - id_offset); + if (!ref) + return 0; + if (!add_footnote_ref(list, ref)) { + free_footnote_ref(ref); + return 0; + } + ref->contents = contents; + } + + return 1; +} + +/* is_ref • returns whether a line is a reference or not */ +static int +is_ref(const uint8_t *data, size_t beg, size_t end, size_t *last, struct link_ref **refs) +{ +/* int n; */ + size_t i = 0; + size_t id_offset, id_end; + size_t link_offset, link_end; + size_t title_offset, title_end; + size_t line_end; + + /* up to 3 optional leading spaces */ + if (beg + 3 >= end) return 0; + if (data[beg] == ' ') { i = 1; + if (data[beg + 1] == ' ') { i = 2; + if (data[beg + 2] == ' ') { i = 3; + if (data[beg + 3] == ' ') return 0; } } } + i += beg; + + /* id part: anything but a newline between brackets */ + if (data[i] != '[') return 0; + i++; + id_offset = i; + while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']') + i++; + if (i >= end || data[i] != ']') return 0; + id_end = i; + + /* spacer: colon (space | tab)* newline? (space | tab)* */ + i++; + if (i >= end || data[i] != ':') return 0; + i++; + while (i < end && data[i] == ' ') i++; + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; } + while (i < end && data[i] == ' ') i++; + if (i >= end) return 0; + + /* link: spacing-free sequence, optionally between angle brackets */ + if (data[i] == '<') + i++; + + link_offset = i; + + while (i < end && data[i] != ' ' && data[i] != '\n' && data[i] != '\r') + i++; + + if (data[i - 1] == '>') link_end = i - 1; + else link_end = i; + + /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */ + while (i < end && data[i] == ' ') i++; + if (i < end && data[i] != '\n' && data[i] != '\r' + && data[i] != '\'' && data[i] != '"' && data[i] != '(') + return 0; + line_end = 0; + /* computing end-of-line */ + if (i >= end || data[i] == '\r' || data[i] == '\n') line_end = i; + if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r') + line_end = i + 1; + + /* optional (space|tab)* spacer after a newline */ + if (line_end) { + i = line_end + 1; + while (i < end && data[i] == ' ') i++; } + + /* optional title: any non-newline sequence enclosed in '"() + alone on its line */ + title_offset = title_end = 0; + if (i + 1 < end + && (data[i] == '\'' || data[i] == '"' || data[i] == '(')) { + i++; + title_offset = i; + /* looking for EOL */ + while (i < end && data[i] != '\n' && data[i] != '\r') i++; + if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r') + title_end = i + 1; + else title_end = i; + /* stepping back */ + i -= 1; + while (i > title_offset && data[i] == ' ') + i -= 1; + if (i > title_offset + && (data[i] == '\'' || data[i] == '"' || data[i] == ')')) { + line_end = title_end; + title_end = i; } } + + if (!line_end || link_end == link_offset) + return 0; /* garbage after the link empty link */ + + /* a valid ref has been found, filling-in return structures */ + if (last) + *last = line_end; + + if (refs) { + struct link_ref *ref; + + ref = add_link_ref(refs, data + id_offset, id_end - id_offset); + if (!ref) + return 0; + + ref->link = hoedown_buffer_new(link_end - link_offset); + hoedown_buffer_put(ref->link, data + link_offset, link_end - link_offset); + + if (title_end > title_offset) { + ref->title = hoedown_buffer_new(title_end - title_offset); + hoedown_buffer_put(ref->title, data + title_offset, title_end - title_offset); + } + } + + return 1; +} + +static void expand_tabs(hoedown_buffer *ob, const uint8_t *line, size_t size) +{ + /* This code makes two assumptions: + * - Input is valid UTF-8. (Any byte with top two bits 10 is skipped, + * whether or not it is a valid UTF-8 continuation byte.) + * - Input contains no combining characters. (Combining characters + * should be skipped but are not.) + */ + size_t i = 0, tab = 0; + + while (i < size) { + size_t org = i; + + while (i < size && line[i] != '\t') { + /* ignore UTF-8 continuation bytes */ + if ((line[i] & 0xc0) != 0x80) + tab++; + i++; + } + + if (i > org) + hoedown_buffer_put(ob, line + org, i - org); + + if (i >= size) + break; + + do { + hoedown_buffer_putc(ob, ' '); tab++; + } while (tab % 4); + + i++; + } +} + +/********************** + * EXPORTED FUNCTIONS * + **********************/ + +hoedown_document * +hoedown_document_new( + const hoedown_renderer *renderer, + hoedown_extensions extensions, + size_t max_nesting) +{ + hoedown_document *doc = NULL; + + assert(max_nesting > 0 && renderer); + + doc = hoedown_malloc(sizeof(hoedown_document)); + memcpy(&doc->md, renderer, sizeof(hoedown_renderer)); + + doc->data.opaque = renderer->opaque; + + hoedown_stack_init(&doc->work_bufs[BUFFER_BLOCK], 4); + hoedown_stack_init(&doc->work_bufs[BUFFER_SPAN], 8); + + memset(doc->active_char, 0x0, 256); + + if (extensions & HOEDOWN_EXT_UNDERLINE && doc->md.underline) { + doc->active_char['_'] = MD_CHAR_EMPHASIS; + } + + if (doc->md.emphasis || doc->md.double_emphasis || doc->md.triple_emphasis) { + doc->active_char['*'] = MD_CHAR_EMPHASIS; + doc->active_char['_'] = MD_CHAR_EMPHASIS; + if (extensions & HOEDOWN_EXT_STRIKETHROUGH) + doc->active_char['~'] = MD_CHAR_EMPHASIS; + if (extensions & HOEDOWN_EXT_HIGHLIGHT) + doc->active_char['='] = MD_CHAR_EMPHASIS; + } + + if (doc->md.codespan) + doc->active_char['`'] = MD_CHAR_CODESPAN; + + if (doc->md.linebreak) + doc->active_char['\n'] = MD_CHAR_LINEBREAK; + + if (doc->md.image || doc->md.link || doc->md.footnotes || doc->md.footnote_ref) { + doc->active_char['['] = MD_CHAR_LINK; + doc->active_char['!'] = MD_CHAR_IMAGE; + } + + doc->active_char['<'] = MD_CHAR_LANGLE; + doc->active_char['\\'] = MD_CHAR_ESCAPE; + doc->active_char['&'] = MD_CHAR_ENTITY; + + if (extensions & HOEDOWN_EXT_AUTOLINK) { + doc->active_char[':'] = MD_CHAR_AUTOLINK_URL; + doc->active_char['@'] = MD_CHAR_AUTOLINK_EMAIL; + doc->active_char['w'] = MD_CHAR_AUTOLINK_WWW; + } + + if (extensions & HOEDOWN_EXT_SUPERSCRIPT) + doc->active_char['^'] = MD_CHAR_SUPERSCRIPT; + + if (extensions & HOEDOWN_EXT_QUOTE) + doc->active_char['"'] = MD_CHAR_QUOTE; + + if (extensions & HOEDOWN_EXT_MATH) + doc->active_char['$'] = MD_CHAR_MATH; + + /* Extension data */ + doc->ext_flags = extensions; + doc->max_nesting = max_nesting; + doc->in_link_body = 0; + + return doc; +} + +void +hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size) +{ + static const uint8_t UTF8_BOM[] = {0xEF, 0xBB, 0xBF}; + + hoedown_buffer *text; + size_t beg, end; + + int footnotes_enabled; + + text = hoedown_buffer_new(64); + + /* Preallocate enough space for our buffer to avoid expanding while copying */ + hoedown_buffer_grow(text, size); + + /* reset the references table */ + memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *)); + + footnotes_enabled = doc->ext_flags & HOEDOWN_EXT_FOOTNOTES; + + /* reset the footnotes lists */ + if (footnotes_enabled) { + memset(&doc->footnotes_found, 0x0, sizeof(doc->footnotes_found)); + memset(&doc->footnotes_used, 0x0, sizeof(doc->footnotes_used)); + } + + /* first pass: looking for references, copying everything else */ + beg = 0; + + /* Skip a possible UTF-8 BOM, even though the Unicode standard + * discourages having these in UTF-8 documents */ + if (size >= 3 && memcmp(data, UTF8_BOM, 3) == 0) + beg += 3; + + while (beg < size) /* iterating over lines */ + if (footnotes_enabled && is_footnote(data, beg, size, &end, &doc->footnotes_found)) + beg = end; + else if (is_ref(data, beg, size, &end, doc->refs)) + beg = end; + else { /* skipping to the next line */ + end = beg; + while (end < size && data[end] != '\n' && data[end] != '\r') + end++; + + /* adding the line body if present */ + if (end > beg) + expand_tabs(text, data + beg, end - beg); + + while (end < size && (data[end] == '\n' || data[end] == '\r')) { + /* add one \n per newline */ + if (data[end] == '\n' || (end + 1 < size && data[end + 1] != '\n')) + hoedown_buffer_putc(text, '\n'); + end++; + } + + beg = end; + } + + /* pre-grow the output buffer to minimize allocations */ + hoedown_buffer_grow(ob, text->size + (text->size >> 1)); + + /* second pass: actual rendering */ + if (doc->md.doc_header) + doc->md.doc_header(ob, 0, &doc->data); + + if (text->size) { + /* adding a final newline if not already present */ + if (text->data[text->size - 1] != '\n' && text->data[text->size - 1] != '\r') + hoedown_buffer_putc(text, '\n'); + + parse_block(ob, doc, text->data, text->size); + } + + /* footnotes */ + if (footnotes_enabled) + parse_footnote_list(ob, doc, &doc->footnotes_used); + + if (doc->md.doc_footer) + doc->md.doc_footer(ob, 0, &doc->data); + + /* clean-up */ + hoedown_buffer_free(text); + free_link_refs(doc->refs); + if (footnotes_enabled) { + free_footnote_list(&doc->footnotes_found, 1); + free_footnote_list(&doc->footnotes_used, 0); + } + + assert(doc->work_bufs[BUFFER_SPAN].size == 0); + assert(doc->work_bufs[BUFFER_BLOCK].size == 0); +} + +void +hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size) +{ + size_t i = 0, mark; + hoedown_buffer *text = hoedown_buffer_new(64); + + /* reset the references table */ + memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *)); + + /* first pass: expand tabs and process newlines */ + hoedown_buffer_grow(text, size); + while (1) { + mark = i; + while (i < size && data[i] != '\n' && data[i] != '\r') + i++; + + expand_tabs(text, data + mark, i - mark); + + if (i >= size) + break; + + while (i < size && (data[i] == '\n' || data[i] == '\r')) { + /* add one \n per newline */ + if (data[i] == '\n' || (i + 1 < size && data[i + 1] != '\n')) + hoedown_buffer_putc(text, '\n'); + i++; + } + } + + /* second pass: actual rendering */ + hoedown_buffer_grow(ob, text->size + (text->size >> 1)); + + if (doc->md.doc_header) + doc->md.doc_header(ob, 1, &doc->data); + + parse_inline(ob, doc, text->data, text->size); + + if (doc->md.doc_footer) + doc->md.doc_footer(ob, 1, &doc->data); + + /* clean-up */ + hoedown_buffer_free(text); + + assert(doc->work_bufs[BUFFER_SPAN].size == 0); + assert(doc->work_bufs[BUFFER_BLOCK].size == 0); +} + +void +hoedown_document_free(hoedown_document *doc) +{ + size_t i; + + for (i = 0; i < (size_t)doc->work_bufs[BUFFER_SPAN].asize; ++i) + hoedown_buffer_free(doc->work_bufs[BUFFER_SPAN].item[i]); + + for (i = 0; i < (size_t)doc->work_bufs[BUFFER_BLOCK].asize; ++i) + hoedown_buffer_free(doc->work_bufs[BUFFER_BLOCK].item[i]); + + hoedown_stack_uninit(&doc->work_bufs[BUFFER_SPAN]); + hoedown_stack_uninit(&doc->work_bufs[BUFFER_BLOCK]); + + free(doc); +} diff --git a/include/hoedown/document.h b/include/hoedown/document.h new file mode 100644 index 0000000..a8178fe --- /dev/null +++ b/include/hoedown/document.h @@ -0,0 +1,172 @@ +/* document.h - generic markdown parser */ + +#ifndef HOEDOWN_DOCUMENT_H +#define HOEDOWN_DOCUMENT_H + +#include "buffer.h" +#include "autolink.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * CONSTANTS * + *************/ + +typedef enum hoedown_extensions { + /* block-level extensions */ + HOEDOWN_EXT_TABLES = (1 << 0), + HOEDOWN_EXT_FENCED_CODE = (1 << 1), + HOEDOWN_EXT_FOOTNOTES = (1 << 2), + + /* span-level extensions */ + HOEDOWN_EXT_AUTOLINK = (1 << 3), + HOEDOWN_EXT_STRIKETHROUGH = (1 << 4), + HOEDOWN_EXT_UNDERLINE = (1 << 5), + HOEDOWN_EXT_HIGHLIGHT = (1 << 6), + HOEDOWN_EXT_QUOTE = (1 << 7), + HOEDOWN_EXT_SUPERSCRIPT = (1 << 8), + HOEDOWN_EXT_MATH = (1 << 9), + + /* other flags */ + HOEDOWN_EXT_NO_INTRA_EMPHASIS = (1 << 11), + HOEDOWN_EXT_SPACE_HEADERS = (1 << 12), + HOEDOWN_EXT_MATH_EXPLICIT = (1 << 13), + + /* negative flags */ + HOEDOWN_EXT_DISABLE_INDENTED_CODE = (1 << 14) +} hoedown_extensions; + +#define HOEDOWN_EXT_BLOCK (\ + HOEDOWN_EXT_TABLES |\ + HOEDOWN_EXT_FENCED_CODE |\ + HOEDOWN_EXT_FOOTNOTES ) + +#define HOEDOWN_EXT_SPAN (\ + HOEDOWN_EXT_AUTOLINK |\ + HOEDOWN_EXT_STRIKETHROUGH |\ + HOEDOWN_EXT_UNDERLINE |\ + HOEDOWN_EXT_HIGHLIGHT |\ + HOEDOWN_EXT_QUOTE |\ + HOEDOWN_EXT_SUPERSCRIPT |\ + HOEDOWN_EXT_MATH ) + +#define HOEDOWN_EXT_FLAGS (\ + HOEDOWN_EXT_NO_INTRA_EMPHASIS |\ + HOEDOWN_EXT_SPACE_HEADERS |\ + HOEDOWN_EXT_MATH_EXPLICIT ) + +#define HOEDOWN_EXT_NEGATIVE (\ + HOEDOWN_EXT_DISABLE_INDENTED_CODE ) + +typedef enum hoedown_list_flags { + HOEDOWN_LIST_ORDERED = (1 << 0), + HOEDOWN_LI_BLOCK = (1 << 1) /*
  • containing block data */ +} hoedown_list_flags; + +typedef enum hoedown_table_flags { + HOEDOWN_TABLE_ALIGN_LEFT = 1, + HOEDOWN_TABLE_ALIGN_RIGHT = 2, + HOEDOWN_TABLE_ALIGN_CENTER = 3, + HOEDOWN_TABLE_ALIGNMASK = 3, + HOEDOWN_TABLE_HEADER = 4 +} hoedown_table_flags; + +typedef enum hoedown_autolink_type { + HOEDOWN_AUTOLINK_NONE, /* used internally when it is not an autolink*/ + HOEDOWN_AUTOLINK_NORMAL, /* normal http/http/ftp/mailto/etc link */ + HOEDOWN_AUTOLINK_EMAIL /* e-mail link without explit mailto: */ +} hoedown_autolink_type; + + +/********* + * TYPES * + *********/ + +struct hoedown_document; +typedef struct hoedown_document hoedown_document; + +struct hoedown_renderer_data { + void *opaque; +}; +typedef struct hoedown_renderer_data hoedown_renderer_data; + +/* hoedown_renderer - functions for rendering parsed data */ +struct hoedown_renderer { + /* state object */ + void *opaque; + + /* block level callbacks - NULL skips the block */ + void (*blockcode)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data); + void (*blockquote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*header)(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data); + void (*hrule)(hoedown_buffer *ob, const hoedown_renderer_data *data); + void (*list)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data); + void (*listitem)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data); + void (*paragraph)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table_header)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table_body)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table_row)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table_cell)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data); + void (*footnotes)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*footnote_def)(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data); + void (*blockhtml)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + + /* span level callbacks - NULL or return 0 prints the span verbatim */ + int (*autolink)(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data); + int (*codespan)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + int (*double_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*underline)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*highlight)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*quote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*image)(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data); + int (*linebreak)(hoedown_buffer *ob, const hoedown_renderer_data *data); + int (*link)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data); + int (*triple_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*strikethrough)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*superscript)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*footnote_ref)(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data); + int (*math)(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data); + int (*raw_html)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + + /* low level callbacks - NULL copies input directly into the output */ + void (*entity)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + void (*normal_text)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + + /* miscellaneous callbacks */ + void (*doc_header)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data); + void (*doc_footer)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data); +}; +typedef struct hoedown_renderer hoedown_renderer; + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_document_new: allocate a new document processor instance */ +hoedown_document *hoedown_document_new( + const hoedown_renderer *renderer, + hoedown_extensions extensions, + size_t max_nesting +) __attribute__ ((malloc)); + +/* hoedown_document_render: render regular Markdown using the document processor */ +void hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size); + +/* hoedown_document_render_inline: render inline Markdown using the document processor */ +void hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size); + +/* hoedown_document_free: deallocate a document processor instance */ +void hoedown_document_free(hoedown_document *doc); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_DOCUMENT_H **/ diff --git a/include/hoedown/escape.c b/include/hoedown/escape.c new file mode 100644 index 0000000..122c6ec --- /dev/null +++ b/include/hoedown/escape.c @@ -0,0 +1,188 @@ +#include "escape.h" + +#include +#include +#include + + +#define likely(x) __builtin_expect((x),1) +#define unlikely(x) __builtin_expect((x),0) + + +/* + * The following characters will not be escaped: + * + * -_.+!*'(),%#@?=;:/,+&$ alphanum + * + * Note that this character set is the addition of: + * + * - The characters which are safe to be in an URL + * - The characters which are *not* safe to be in + * an URL because they are RESERVED characters. + * + * We assume (lazily) that any RESERVED char that + * appears inside an URL is actually meant to + * have its native function (i.e. as an URL + * component/separator) and hence needs no escaping. + * + * There are two exceptions: the chacters & (amp) + * and ' (single quote) do not appear in the table. + * They are meant to appear in the URL as components, + * yet they require special HTML-entity escaping + * to generate valid HTML markup. + * + * All other characters will be escaped to %XX. + * + */ +static const uint8_t HREF_SAFE[UINT8_MAX+1] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +void +hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size) +{ + static const char hex_chars[] = "0123456789ABCDEF"; + size_t i = 0, mark; + char hex_str[3]; + + hex_str[0] = '%'; + + while (i < size) { + mark = i; + while (i < size && HREF_SAFE[data[i]]) i++; + + /* Optimization for cases where there's nothing to escape */ + if (mark == 0 && i >= size) { + hoedown_buffer_put(ob, data, size); + return; + } + + if (likely(i > mark)) { + hoedown_buffer_put(ob, data + mark, i - mark); + } + + /* escaping */ + if (i >= size) + break; + + switch (data[i]) { + /* amp appears all the time in URLs, but needs + * HTML-entity escaping to be inside an href */ + case '&': + HOEDOWN_BUFPUTSL(ob, "&"); + break; + + /* the single quote is a valid URL character + * according to the standard; it needs HTML + * entity escaping too */ + case '\'': + HOEDOWN_BUFPUTSL(ob, "'"); + break; + + /* the space can be escaped to %20 or a plus + * sign. we're going with the generic escape + * for now. the plus thing is more commonly seen + * when building GET strings */ +#if 0 + case ' ': + hoedown_buffer_putc(ob, '+'); + break; +#endif + + /* every other character goes with a %XX escaping */ + default: + hex_str[1] = hex_chars[(data[i] >> 4) & 0xF]; + hex_str[2] = hex_chars[data[i] & 0xF]; + hoedown_buffer_put(ob, (uint8_t *)hex_str, 3); + } + + i++; + } +} + + +/** + * According to the OWASP rules: + * + * & --> & + * < --> < + * > --> > + * " --> " + * ' --> ' ' is not recommended + * / --> / forward slash is included as it helps end an HTML entity + * + */ +static const uint8_t HTML_ESCAPE_TABLE[UINT8_MAX+1] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +static const char *HTML_ESCAPES[] = { + "", + """, + "&", + "'", + "/", + "<", + ">" +}; + +void +hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure) +{ + size_t i = 0, mark; + + while (1) { + mark = i; + while (i < size && HTML_ESCAPE_TABLE[data[i]] == 0) i++; + + /* Optimization for cases where there's nothing to escape */ + if (mark == 0 && i >= size) { + hoedown_buffer_put(ob, data, size); + return; + } + + if (likely(i > mark)) + hoedown_buffer_put(ob, data + mark, i - mark); + + if (i >= size) break; + + /* The forward slash is only escaped in secure mode */ + if (!secure && data[i] == '/') { + hoedown_buffer_putc(ob, '/'); + } else { + hoedown_buffer_puts(ob, HTML_ESCAPES[HTML_ESCAPE_TABLE[data[i]]]); + } + + i++; + } +} diff --git a/include/hoedown/escape.h b/include/hoedown/escape.h new file mode 100644 index 0000000..d7659c2 --- /dev/null +++ b/include/hoedown/escape.h @@ -0,0 +1,28 @@ +/* escape.h - escape utilities */ + +#ifndef HOEDOWN_ESCAPE_H +#define HOEDOWN_ESCAPE_H + +#include "buffer.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_escape_href: escape (part of) a URL inside HTML */ +void hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size); + +/* hoedown_escape_html: escape HTML */ +void hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_ESCAPE_H **/ diff --git a/include/hoedown/html.c b/include/hoedown/html.c new file mode 100644 index 0000000..b5101c1 --- /dev/null +++ b/include/hoedown/html.c @@ -0,0 +1,754 @@ +#include "html.h" + +#include +#include +#include +#include + +#include "escape.h" + +#define USE_XHTML(opt) (opt->flags & HOEDOWN_HTML_USE_XHTML) + +hoedown_html_tag +hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname) +{ + size_t i; + int closed = 0; + + if (size < 3 || data[0] != '<') + return HOEDOWN_HTML_TAG_NONE; + + i = 1; + + if (data[i] == '/') { + closed = 1; + i++; + } + + for (; i < size; ++i, ++tagname) { + if (*tagname == 0) + break; + + if (data[i] != *tagname) + return HOEDOWN_HTML_TAG_NONE; + } + + if (i == size) + return HOEDOWN_HTML_TAG_NONE; + + if (isspace(data[i]) || data[i] == '>') + return closed ? HOEDOWN_HTML_TAG_CLOSE : HOEDOWN_HTML_TAG_OPEN; + + return HOEDOWN_HTML_TAG_NONE; +} + +static void escape_html(hoedown_buffer *ob, const uint8_t *source, size_t length) +{ + hoedown_escape_html(ob, source, length, 0); +} + +static void escape_href(hoedown_buffer *ob, const uint8_t *source, size_t length) +{ + hoedown_escape_href(ob, source, length); +} + +/******************** + * GENERIC RENDERER * + ********************/ +static int +rndr_autolink(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + if (!link || !link->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, "data, link->size); + + if (state->link_attributes) { + hoedown_buffer_putc(ob, '\"'); + state->link_attributes(ob, link, data); + hoedown_buffer_putc(ob, '>'); + } else { + HOEDOWN_BUFPUTSL(ob, "\">"); + } + + /* + * Pretty printing: if we get an email address as + * an actual URI, e.g. `mailto:foo@bar.com`, we don't + * want to print the `mailto:` prefix + */ + if (hoedown_buffer_prefix(link, "mailto:") == 0) { + escape_html(ob, link->data + 7, link->size - 7); + } else { + escape_html(ob, link->data, link->size); + } + + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static void +rndr_blockcode(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + + if (lang) { + HOEDOWN_BUFPUTSL(ob, "
    data, lang->size);
    +		HOEDOWN_BUFPUTSL(ob, "\">");
    +	} else {
    +		HOEDOWN_BUFPUTSL(ob, "
    ");
    +	}
    +
    +	if (text)
    +		escape_html(ob, text->data, text->size);
    +
    +	HOEDOWN_BUFPUTSL(ob, "
    \n"); +} + +static void +rndr_blockquote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "
    \n"); + if (content) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "
    \n"); +} + +static int +rndr_codespan(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) +{ + HOEDOWN_BUFPUTSL(ob, ""); + if (text) escape_html(ob, text->data, text->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static int +rndr_strikethrough(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static int +rndr_double_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static int +rndr_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) return 0; + HOEDOWN_BUFPUTSL(ob, ""); + if (content) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static int +rndr_underline(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static int +rndr_highlight(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static int +rndr_quote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static int +rndr_linebreak(hoedown_buffer *ob, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); + return 1; +} + +static void +rndr_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + if (ob->size) + hoedown_buffer_putc(ob, '\n'); + + if (level <= state->toc_data.nesting_level) + hoedown_buffer_printf(ob, "", level, state->toc_data.header_count++); + else + hoedown_buffer_printf(ob, "", level); + + if (content) hoedown_buffer_put(ob, content->data, content->size); + hoedown_buffer_printf(ob, "\n", level); +} + +static int +rndr_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + HOEDOWN_BUFPUTSL(ob, "size) + escape_href(ob, link->data, link->size); + + if (title && title->size) { + HOEDOWN_BUFPUTSL(ob, "\" title=\""); + escape_html(ob, title->data, title->size); + } + + if (state->link_attributes) { + hoedown_buffer_putc(ob, '\"'); + state->link_attributes(ob, link, data); + hoedown_buffer_putc(ob, '>'); + } else { + HOEDOWN_BUFPUTSL(ob, "\">"); + } + + if (content && content->size) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static void +rndr_list(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "
      \n" : "
        \n"), 5); + if (content) hoedown_buffer_put(ob, content->data, content->size); + hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "
    \n" : "\n"), 6); +} + +static void +rndr_listitem(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data) +{ + HOEDOWN_BUFPUTSL(ob, "
  • "); + if (content) { + size_t size = content->size; + while (size && content->data[size - 1] == '\n') + size--; + + hoedown_buffer_put(ob, content->data, size); + } + HOEDOWN_BUFPUTSL(ob, "
  • \n"); +} + +static void +rndr_paragraph(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + size_t i = 0; + + if (ob->size) hoedown_buffer_putc(ob, '\n'); + + if (!content || !content->size) + return; + + while (i < content->size && isspace(content->data[i])) i++; + + if (i == content->size) + return; + + HOEDOWN_BUFPUTSL(ob, "

    "); + if (state->flags & HOEDOWN_HTML_HARD_WRAP) { + size_t org; + while (i < content->size) { + org = i; + while (i < content->size && content->data[i] != '\n') + i++; + + if (i > org) + hoedown_buffer_put(ob, content->data + org, i - org); + + /* + * do not insert a line break if this newline + * is the last character on the paragraph + */ + if (i >= content->size - 1) + break; + + rndr_linebreak(ob, data); + i++; + } + } else { + hoedown_buffer_put(ob, content->data + i, content->size - i); + } + HOEDOWN_BUFPUTSL(ob, "

    \n"); +} + +static void +rndr_raw_block(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) +{ + size_t org, sz; + + if (!text) + return; + + /* FIXME: Do we *really* need to trim the HTML? How does that make a difference? */ + sz = text->size; + while (sz > 0 && text->data[sz - 1] == '\n') + sz--; + + org = 0; + while (org < sz && text->data[org] == '\n') + org++; + + if (org >= sz) + return; + + if (ob->size) + hoedown_buffer_putc(ob, '\n'); + + hoedown_buffer_put(ob, text->data + org, sz - org); + hoedown_buffer_putc(ob, '\n'); +} + +static int +rndr_triple_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) return 0; + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static void +rndr_hrule(hoedown_buffer *ob, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + if (ob->size) hoedown_buffer_putc(ob, '\n'); + hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); +} + +static int +rndr_image(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + if (!link || !link->size) return 0; + + HOEDOWN_BUFPUTSL(ob, "data, link->size); + HOEDOWN_BUFPUTSL(ob, "\" alt=\""); + + if (alt && alt->size) + escape_html(ob, alt->data, alt->size); + + if (title && title->size) { + HOEDOWN_BUFPUTSL(ob, "\" title=\""); + escape_html(ob, title->data, title->size); } + + hoedown_buffer_puts(ob, USE_XHTML(state) ? "\"/>" : "\">"); + return 1; +} + +static int +rndr_raw_html(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + /* ESCAPE overrides SKIP_HTML. It doesn't look to see if + * there are any valid tags, just escapes all of them. */ + if((state->flags & HOEDOWN_HTML_ESCAPE) != 0) { + escape_html(ob, text->data, text->size); + return 1; + } + + if ((state->flags & HOEDOWN_HTML_SKIP_HTML) != 0) + return 1; + + hoedown_buffer_put(ob, text->data, text->size); + return 1; +} + +static void +rndr_table(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "\n"); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "
    \n"); +} + +static void +rndr_table_header(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "\n"); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "\n"); +} + +static void +rndr_table_body(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "\n"); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "\n"); +} + +static void +rndr_tablerow(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + HOEDOWN_BUFPUTSL(ob, "\n"); + if (content) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "\n"); +} + +static void +rndr_tablecell(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data) +{ + if (flags & HOEDOWN_TABLE_HEADER) { + HOEDOWN_BUFPUTSL(ob, ""); + break; + + case HOEDOWN_TABLE_ALIGN_LEFT: + HOEDOWN_BUFPUTSL(ob, " style=\"text-align: left\">"); + break; + + case HOEDOWN_TABLE_ALIGN_RIGHT: + HOEDOWN_BUFPUTSL(ob, " style=\"text-align: right\">"); + break; + + default: + HOEDOWN_BUFPUTSL(ob, ">"); + } + + if (content) + hoedown_buffer_put(ob, content->data, content->size); + + if (flags & HOEDOWN_TABLE_HEADER) { + HOEDOWN_BUFPUTSL(ob, "\n"); + } else { + HOEDOWN_BUFPUTSL(ob, "\n"); + } +} + +static int +rndr_superscript(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) return 0; + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static void +rndr_normal_text(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (content) + escape_html(ob, content->data, content->size); +} + +static void +rndr_footnotes(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "
    \n"); + hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); + HOEDOWN_BUFPUTSL(ob, "
      \n"); + + if (content) hoedown_buffer_put(ob, content->data, content->size); + + HOEDOWN_BUFPUTSL(ob, "\n
    \n
    \n"); +} + +static void +rndr_footnote_def(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data) +{ + size_t i = 0; + int pfound = 0; + + /* insert anchor at the end of first paragraph block */ + if (content) { + while ((i+3) < content->size) { + if (content->data[i++] != '<') continue; + if (content->data[i++] != '/') continue; + if (content->data[i++] != 'p' && content->data[i] != 'P') continue; + if (content->data[i] != '>') continue; + i -= 3; + pfound = 1; + break; + } + } + + hoedown_buffer_printf(ob, "\n
  • \n", num); + if (pfound) { + hoedown_buffer_put(ob, content->data, i); + hoedown_buffer_printf(ob, " ", num); + hoedown_buffer_put(ob, content->data + i, content->size - i); + } else if (content) { + hoedown_buffer_put(ob, content->data, content->size); + } + HOEDOWN_BUFPUTSL(ob, "
  • \n"); +} + +static int +rndr_footnote_ref(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data) +{ + hoedown_buffer_printf(ob, "%d", num, num, num); + return 1; +} + +static int +rndr_math(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data) +{ + hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\[" : "\\("), 2); + escape_html(ob, text->data, text->size); + hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\]" : "\\)"), 2); + return 1; +} + +static void +toc_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + if (level <= state->toc_data.nesting_level) { + /* set the level offset if this is the first header + * we're parsing for the document */ + if (state->toc_data.current_level == 0) + state->toc_data.level_offset = level - 1; + + level -= state->toc_data.level_offset; + + if (level > state->toc_data.current_level) { + while (level > state->toc_data.current_level) { + HOEDOWN_BUFPUTSL(ob, "
      \n
    • \n"); + state->toc_data.current_level++; + } + } else if (level < state->toc_data.current_level) { + HOEDOWN_BUFPUTSL(ob, "
    • \n"); + while (level < state->toc_data.current_level) { + HOEDOWN_BUFPUTSL(ob, "
    \n
  • \n"); + state->toc_data.current_level--; + } + HOEDOWN_BUFPUTSL(ob,"
  • \n"); + } else { + HOEDOWN_BUFPUTSL(ob,"
  • \n
  • \n"); + } + + hoedown_buffer_printf(ob, "", state->toc_data.header_count++); + if (content) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "\n"); + } +} + +static int +toc_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data) +{ + if (content && content->size) hoedown_buffer_put(ob, content->data, content->size); + return 1; +} + +static void +toc_finalize(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state; + + if (inline_render) + return; + + state = data->opaque; + + while (state->toc_data.current_level > 0) { + HOEDOWN_BUFPUTSL(ob, "
  • \n\n"); + state->toc_data.current_level--; + } + + state->toc_data.header_count = 0; +} + +hoedown_renderer * +hoedown_html_toc_renderer_new(int nesting_level) +{ + static const hoedown_renderer cb_default = { + NULL, + + NULL, + NULL, + toc_header, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + + NULL, + rndr_codespan, + rndr_double_emphasis, + rndr_emphasis, + rndr_underline, + rndr_highlight, + rndr_quote, + NULL, + NULL, + toc_link, + rndr_triple_emphasis, + rndr_strikethrough, + rndr_superscript, + NULL, + NULL, + NULL, + + NULL, + rndr_normal_text, + + NULL, + toc_finalize + }; + + hoedown_html_renderer_state *state; + hoedown_renderer *renderer; + + /* Prepare the state pointer */ + state = hoedown_malloc(sizeof(hoedown_html_renderer_state)); + memset(state, 0x0, sizeof(hoedown_html_renderer_state)); + + state->toc_data.nesting_level = nesting_level; + + /* Prepare the renderer */ + renderer = hoedown_malloc(sizeof(hoedown_renderer)); + memcpy(renderer, &cb_default, sizeof(hoedown_renderer)); + + renderer->opaque = state; + return renderer; +} + +hoedown_renderer * +hoedown_html_renderer_new(hoedown_html_flags render_flags, int nesting_level) +{ + static const hoedown_renderer cb_default = { + NULL, + + rndr_blockcode, + rndr_blockquote, + rndr_header, + rndr_hrule, + rndr_list, + rndr_listitem, + rndr_paragraph, + rndr_table, + rndr_table_header, + rndr_table_body, + rndr_tablerow, + rndr_tablecell, + rndr_footnotes, + rndr_footnote_def, + rndr_raw_block, + + rndr_autolink, + rndr_codespan, + rndr_double_emphasis, + rndr_emphasis, + rndr_underline, + rndr_highlight, + rndr_quote, + rndr_image, + rndr_linebreak, + rndr_link, + rndr_triple_emphasis, + rndr_strikethrough, + rndr_superscript, + rndr_footnote_ref, + rndr_math, + rndr_raw_html, + + NULL, + rndr_normal_text, + + NULL, + NULL + }; + + hoedown_html_renderer_state *state; + hoedown_renderer *renderer; + + /* Prepare the state pointer */ + state = hoedown_malloc(sizeof(hoedown_html_renderer_state)); + memset(state, 0x0, sizeof(hoedown_html_renderer_state)); + + state->flags = render_flags; + state->toc_data.nesting_level = nesting_level; + + /* Prepare the renderer */ + renderer = hoedown_malloc(sizeof(hoedown_renderer)); + memcpy(renderer, &cb_default, sizeof(hoedown_renderer)); + + if (render_flags & HOEDOWN_HTML_SKIP_HTML || render_flags & HOEDOWN_HTML_ESCAPE) + renderer->blockhtml = NULL; + + renderer->opaque = state; + return renderer; +} + +void +hoedown_html_renderer_free(hoedown_renderer *renderer) +{ + free(renderer->opaque); + free(renderer); +} diff --git a/include/hoedown/html.h b/include/hoedown/html.h new file mode 100644 index 0000000..e46e7fd --- /dev/null +++ b/include/hoedown/html.h @@ -0,0 +1,84 @@ +/* html.h - HTML renderer and utilities */ + +#ifndef HOEDOWN_HTML_H +#define HOEDOWN_HTML_H + +#include "document.h" +#include "buffer.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * CONSTANTS * + *************/ + +typedef enum hoedown_html_flags { + HOEDOWN_HTML_SKIP_HTML = (1 << 0), + HOEDOWN_HTML_ESCAPE = (1 << 1), + HOEDOWN_HTML_HARD_WRAP = (1 << 2), + HOEDOWN_HTML_USE_XHTML = (1 << 3) +} hoedown_html_flags; + +typedef enum hoedown_html_tag { + HOEDOWN_HTML_TAG_NONE = 0, + HOEDOWN_HTML_TAG_OPEN, + HOEDOWN_HTML_TAG_CLOSE +} hoedown_html_tag; + + +/********* + * TYPES * + *********/ + +struct hoedown_html_renderer_state { + void *opaque; + + struct { + int header_count; + int current_level; + int level_offset; + int nesting_level; + } toc_data; + + hoedown_html_flags flags; + + /* extra callbacks */ + void (*link_attributes)(hoedown_buffer *ob, const hoedown_buffer *url, const hoedown_renderer_data *data); +}; +typedef struct hoedown_html_renderer_state hoedown_html_renderer_state; + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_html_smartypants: process an HTML snippet using SmartyPants for smart punctuation */ +void hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *data, size_t size); + +/* hoedown_html_is_tag: checks if data starts with a specific tag, returns the tag type or NONE */ +hoedown_html_tag hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname); + + +/* hoedown_html_renderer_new: allocates a regular HTML renderer */ +hoedown_renderer *hoedown_html_renderer_new( + hoedown_html_flags render_flags, + int nesting_level +) __attribute__ ((malloc)); + +/* hoedown_html_toc_renderer_new: like hoedown_html_renderer_new, but the returned renderer produces the Table of Contents */ +hoedown_renderer *hoedown_html_toc_renderer_new( + int nesting_level +) __attribute__ ((malloc)); + +/* hoedown_html_renderer_free: deallocate an HTML renderer */ +void hoedown_html_renderer_free(hoedown_renderer *renderer); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_HTML_H **/ diff --git a/include/hoedown/html_blocks.c b/include/hoedown/html_blocks.c new file mode 100644 index 0000000..f5e9dce --- /dev/null +++ b/include/hoedown/html_blocks.c @@ -0,0 +1,240 @@ +/* ANSI-C code produced by gperf version 3.0.3 */ +/* Command-line: gperf -L ANSI-C -N hoedown_find_block_tag -c -C -E -S 1 --ignore-case -m100 html_block_names.gperf */ +/* Computed positions: -k'1-2' */ + +#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ + && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ + && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ + && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ + && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ + && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ + && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ + && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ + && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ + && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ + && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ + && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ + && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ + && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ + && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ + && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ + && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ + && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ + && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ + && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ + && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ + && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ + && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) +/* The character set is not based on ISO-646. */ +#error "gperf generated tables don't work with this execution character set. Please report a bug to ." +#endif + +/* maximum key range = 24, duplicates = 0 */ + +#ifndef GPERF_DOWNCASE +#define GPERF_DOWNCASE 1 +static unsigned char gperf_downcase[256] = + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, + 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, + 122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, + 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, + 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, + 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, + 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, + 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, + 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, + 255 + }; +#endif + +#ifndef GPERF_CASE_STRNCMP +#define GPERF_CASE_STRNCMP 1 +static int +gperf_case_strncmp (register const char *s1, register const char *s2, register unsigned int n) +{ + for (; n > 0;) + { + unsigned char c1 = gperf_downcase[(unsigned char)*s1++]; + unsigned char c2 = gperf_downcase[(unsigned char)*s2++]; + if (c1 != 0 && c1 == c2) + { + n--; + continue; + } + return (int)c1 - (int)c2; + } + return 0; +} +#endif + +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +static unsigned int +hash (register const char *str, register unsigned int len) +{ + static const unsigned char asso_values[] = + { + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 22, 21, 19, 18, 16, 0, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 1, 25, 0, 25, + 1, 0, 0, 13, 0, 25, 25, 11, 2, 1, + 0, 25, 25, 5, 0, 2, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 1, 25, + 0, 25, 1, 0, 0, 13, 0, 25, 25, 11, + 2, 1, 0, 25, 25, 5, 0, 2, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25 + }; + register int hval = (int)len; + + switch (hval) + { + default: + hval += asso_values[(unsigned char)str[1]+1]; + /*FALLTHROUGH*/ + case 1: + hval += asso_values[(unsigned char)str[0]]; + break; + } + return hval; +} + +#ifdef __GNUC__ +__inline +#ifdef __GNUC_STDC_INLINE__ +__attribute__ ((__gnu_inline__)) +#endif +#endif +const char * +hoedown_find_block_tag (register const char *str, register unsigned int len) +{ + enum + { + TOTAL_KEYWORDS = 24, + MIN_WORD_LENGTH = 1, + MAX_WORD_LENGTH = 10, + MIN_HASH_VALUE = 1, + MAX_HASH_VALUE = 24 + }; + + if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH) + { + register int key = hash (str, len); + + if (key <= MAX_HASH_VALUE && key >= MIN_HASH_VALUE) + { + register const char *resword; + + switch (key - 1) + { + case 0: + resword = "p"; + goto compare; + case 1: + resword = "h6"; + goto compare; + case 2: + resword = "div"; + goto compare; + case 3: + resword = "del"; + goto compare; + case 4: + resword = "form"; + goto compare; + case 5: + resword = "table"; + goto compare; + case 6: + resword = "figure"; + goto compare; + case 7: + resword = "pre"; + goto compare; + case 8: + resword = "fieldset"; + goto compare; + case 9: + resword = "noscript"; + goto compare; + case 10: + resword = "script"; + goto compare; + case 11: + resword = "style"; + goto compare; + case 12: + resword = "dl"; + goto compare; + case 13: + resword = "ol"; + goto compare; + case 14: + resword = "ul"; + goto compare; + case 15: + resword = "math"; + goto compare; + case 16: + resword = "ins"; + goto compare; + case 17: + resword = "h5"; + goto compare; + case 18: + resword = "iframe"; + goto compare; + case 19: + resword = "h4"; + goto compare; + case 20: + resword = "h3"; + goto compare; + case 21: + resword = "blockquote"; + goto compare; + case 22: + resword = "h2"; + goto compare; + case 23: + resword = "h1"; + goto compare; + } + return 0; + compare: + if ((((unsigned char)*str ^ (unsigned char)*resword) & ~32) == 0 && !gperf_case_strncmp (str, resword, len) && resword[len] == '\0') + return resword; + } + } + return 0; +} diff --git a/include/hoedown/html_smartypants.c b/include/hoedown/html_smartypants.c new file mode 100644 index 0000000..e3dfa28 --- /dev/null +++ b/include/hoedown/html_smartypants.c @@ -0,0 +1,435 @@ +#include "html.h" + +#include +#include +#include +#include + +#ifdef _MSC_VER +#define snprintf _snprintf +#endif + +struct smartypants_data { + int in_squote; + int in_dquote; +}; + +static size_t smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); + +static size_t (*smartypants_cb_ptrs[]) + (hoedown_buffer *, struct smartypants_data *, uint8_t, const uint8_t *, size_t) = +{ + NULL, /* 0 */ + smartypants_cb__dash, /* 1 */ + smartypants_cb__parens, /* 2 */ + smartypants_cb__squote, /* 3 */ + smartypants_cb__dquote, /* 4 */ + smartypants_cb__amp, /* 5 */ + smartypants_cb__period, /* 6 */ + smartypants_cb__number, /* 7 */ + smartypants_cb__ltag, /* 8 */ + smartypants_cb__backtick, /* 9 */ + smartypants_cb__escape, /* 10 */ +}; + +static const uint8_t smartypants_cb_chars[UINT8_MAX+1] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 4, 0, 0, 0, 5, 3, 2, 0, 0, 0, 0, 1, 6, 0, + 0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, + 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +static int +word_boundary(uint8_t c) +{ + return c == 0 || isspace(c) || ispunct(c); +} + +/* + If 'text' begins with any kind of single quote (e.g. "'" or "'" etc.), + returns the length of the sequence of characters that makes up the single- + quote. Otherwise, returns zero. +*/ +static size_t +squote_len(const uint8_t *text, size_t size) +{ + static char* single_quote_list[] = { "'", "'", "'", "'", NULL }; + char** p; + + for (p = single_quote_list; *p; ++p) { + size_t len = strlen(*p); + if (size >= len && memcmp(text, *p, len) == 0) { + return len; + } + } + + return 0; +} + +/* Converts " or ' at very beginning or end of a word to left or right quote */ +static int +smartypants_quotes(hoedown_buffer *ob, uint8_t previous_char, uint8_t next_char, uint8_t quote, int *is_open) +{ + char ent[8]; + + if (*is_open && !word_boundary(next_char)) + return 0; + + if (!(*is_open) && !word_boundary(previous_char)) + return 0; + + snprintf(ent, sizeof(ent), "&%c%cquo;", (*is_open) ? 'r' : 'l', quote); + *is_open = !(*is_open); + hoedown_buffer_puts(ob, ent); + return 1; +} + +/* + Converts ' to left or right single quote; but the initial ' might be in + different forms, e.g. ' or ' or '. + 'squote_text' points to the original single quote, and 'squote_size' is its length. + 'text' points at the last character of the single-quote, e.g. ' or ; +*/ +static size_t +smartypants_squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size, + const uint8_t *squote_text, size_t squote_size) +{ + if (size >= 2) { + uint8_t t1 = tolower(text[1]); + size_t next_squote_len = squote_len(text+1, size-1); + + /* convert '' to “ or ” */ + if (next_squote_len > 0) { + uint8_t next_char = (size > 1+next_squote_len) ? text[1+next_squote_len] : 0; + if (smartypants_quotes(ob, previous_char, next_char, 'd', &smrt->in_dquote)) + return next_squote_len; + } + + /* Tom's, isn't, I'm, I'd */ + if ((t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && + (size == 3 || word_boundary(text[2]))) { + HOEDOWN_BUFPUTSL(ob, "’"); + return 0; + } + + /* you're, you'll, you've */ + if (size >= 3) { + uint8_t t2 = tolower(text[2]); + + if (((t1 == 'r' && t2 == 'e') || + (t1 == 'l' && t2 == 'l') || + (t1 == 'v' && t2 == 'e')) && + (size == 4 || word_boundary(text[3]))) { + HOEDOWN_BUFPUTSL(ob, "’"); + return 0; + } + } + } + + if (smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 's', &smrt->in_squote)) + return 0; + + hoedown_buffer_put(ob, squote_text, squote_size); + return 0; +} + +/* Converts ' to left or right single quote. */ +static size_t +smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + return smartypants_squote(ob, smrt, previous_char, text, size, text, 1); +} + +/* Converts (c), (r), (tm) */ +static size_t +smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size >= 3) { + uint8_t t1 = tolower(text[1]); + uint8_t t2 = tolower(text[2]); + + if (t1 == 'c' && t2 == ')') { + HOEDOWN_BUFPUTSL(ob, "©"); + return 2; + } + + if (t1 == 'r' && t2 == ')') { + HOEDOWN_BUFPUTSL(ob, "®"); + return 2; + } + + if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') { + HOEDOWN_BUFPUTSL(ob, "™"); + return 3; + } + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts "--" to em-dash, etc. */ +static size_t +smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size >= 3 && text[1] == '-' && text[2] == '-') { + HOEDOWN_BUFPUTSL(ob, "—"); + return 2; + } + + if (size >= 2 && text[1] == '-') { + HOEDOWN_BUFPUTSL(ob, "–"); + return 1; + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts " etc. */ +static size_t +smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + size_t len; + if (size >= 6 && memcmp(text, """, 6) == 0) { + if (smartypants_quotes(ob, previous_char, size >= 7 ? text[6] : 0, 'd', &smrt->in_dquote)) + return 5; + } + + len = squote_len(text, size); + if (len > 0) { + return (len-1) + smartypants_squote(ob, smrt, previous_char, text+(len-1), size-(len-1), text, len); + } + + if (size >= 4 && memcmp(text, "�", 4) == 0) + return 3; + + hoedown_buffer_putc(ob, '&'); + return 0; +} + +/* Converts "..." to ellipsis */ +static size_t +smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size >= 3 && text[1] == '.' && text[2] == '.') { + HOEDOWN_BUFPUTSL(ob, "…"); + return 2; + } + + if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.') { + HOEDOWN_BUFPUTSL(ob, "…"); + return 4; + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts `` to opening double quote */ +static size_t +smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size >= 2 && text[1] == '`') { + if (smartypants_quotes(ob, previous_char, size >= 3 ? text[2] : 0, 'd', &smrt->in_dquote)) + return 1; + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts 1/2, 1/4, 3/4 */ +static size_t +smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (word_boundary(previous_char) && size >= 3) { + if (text[0] == '1' && text[1] == '/' && text[2] == '2') { + if (size == 3 || word_boundary(text[3])) { + HOEDOWN_BUFPUTSL(ob, "½"); + return 2; + } + } + + if (text[0] == '1' && text[1] == '/' && text[2] == '4') { + if (size == 3 || word_boundary(text[3]) || + (size >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h')) { + HOEDOWN_BUFPUTSL(ob, "¼"); + return 2; + } + } + + if (text[0] == '3' && text[1] == '/' && text[2] == '4') { + if (size == 3 || word_boundary(text[3]) || + (size >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's')) { + HOEDOWN_BUFPUTSL(ob, "¾"); + return 2; + } + } + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts " to left or right double quote */ +static size_t +smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (!smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 'd', &smrt->in_dquote)) + HOEDOWN_BUFPUTSL(ob, """); + + return 0; +} + +static size_t +smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + static const char *skip_tags[] = { + "pre", "code", "var", "samp", "kbd", "math", "script", "style" + }; + static const size_t skip_tags_count = 8; + + size_t tag, i = 0; + + /* This is a comment. Copy everything verbatim until --> or EOF is seen. */ + if (i + 4 < size && memcmp(text + i, "", 3) != 0) + i++; + i += 3; + hoedown_buffer_put(ob, text, i + 1); + return i; + } + + while (i < size && text[i] != '>') + i++; + + for (tag = 0; tag < skip_tags_count; ++tag) { + if (hoedown_html_is_tag(text, size, skip_tags[tag]) == HOEDOWN_HTML_TAG_OPEN) + break; + } + + if (tag < skip_tags_count) { + for (;;) { + while (i < size && text[i] != '<') + i++; + + if (i == size) + break; + + if (hoedown_html_is_tag(text + i, size - i, skip_tags[tag]) == HOEDOWN_HTML_TAG_CLOSE) + break; + + i++; + } + + while (i < size && text[i] != '>') + i++; + } + + hoedown_buffer_put(ob, text, i + 1); + return i; +} + +static size_t +smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size < 2) + return 0; + + switch (text[1]) { + case '\\': + case '"': + case '\'': + case '.': + case '-': + case '`': + hoedown_buffer_putc(ob, text[1]); + return 1; + + default: + hoedown_buffer_putc(ob, '\\'); + return 0; + } +} + +#if 0 +static struct { + uint8_t c0; + const uint8_t *pattern; + const uint8_t *entity; + int skip; +} smartypants_subs[] = { + { '\'', "'s>", "’", 0 }, + { '\'', "'t>", "’", 0 }, + { '\'', "'re>", "’", 0 }, + { '\'', "'ll>", "’", 0 }, + { '\'', "'ve>", "’", 0 }, + { '\'', "'m>", "’", 0 }, + { '\'', "'d>", "’", 0 }, + { '-', "--", "—", 1 }, + { '-', "<->", "–", 0 }, + { '.', "...", "…", 2 }, + { '.', ". . .", "…", 4 }, + { '(', "(c)", "©", 2 }, + { '(', "(r)", "®", 2 }, + { '(', "(tm)", "™", 3 }, + { '3', "<3/4>", "¾", 2 }, + { '3', "<3/4ths>", "¾", 2 }, + { '1', "<1/2>", "½", 2 }, + { '1', "<1/4>", "¼", 2 }, + { '1', "<1/4th>", "¼", 2 }, + { '&', "�", 0, 3 }, +}; +#endif + +void +hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *text, size_t size) +{ + size_t i; + struct smartypants_data smrt = {0, 0}; + + if (!text) + return; + + hoedown_buffer_grow(ob, size); + + for (i = 0; i < size; ++i) { + size_t org; + uint8_t action = 0; + + org = i; + while (i < size && (action = smartypants_cb_chars[text[i]]) == 0) + i++; + + if (i > org) + hoedown_buffer_put(ob, text + org, i - org); + + if (i < size) { + i += smartypants_cb_ptrs[(int)action] + (ob, &smrt, i ? text[i - 1] : 0, text + i, size - i); + } + } +} diff --git a/include/hoedown/stack.c b/include/hoedown/stack.c new file mode 100644 index 0000000..5c6102c --- /dev/null +++ b/include/hoedown/stack.c @@ -0,0 +1,79 @@ +#include "stack.h" + +#include "buffer.h" + +#include +#include +#include + +void +hoedown_stack_init(hoedown_stack *st, size_t initial_size) +{ + assert(st); + + st->item = NULL; + st->size = st->asize = 0; + + if (!initial_size) + initial_size = 8; + + hoedown_stack_grow(st, initial_size); +} + +void +hoedown_stack_uninit(hoedown_stack *st) +{ + assert(st); + + free(st->item); +} + +void +hoedown_stack_grow(hoedown_stack *st, size_t neosz) +{ + assert(st); + + if (st->asize >= neosz) + return; + + st->item = hoedown_realloc(st->item, neosz * sizeof(void *)); + memset(st->item + st->asize, 0x0, (neosz - st->asize) * sizeof(void *)); + + st->asize = neosz; + + if (st->size > neosz) + st->size = neosz; +} + +void +hoedown_stack_push(hoedown_stack *st, void *item) +{ + assert(st); + + if (st->size >= st->asize) + hoedown_stack_grow(st, st->size * 2); + + st->item[st->size++] = item; +} + +void * +hoedown_stack_pop(hoedown_stack *st) +{ + assert(st); + + if (!st->size) + return NULL; + + return st->item[--st->size]; +} + +void * +hoedown_stack_top(const hoedown_stack *st) +{ + assert(st); + + if (!st->size) + return NULL; + + return st->item[st->size - 1]; +} diff --git a/include/hoedown/stack.h b/include/hoedown/stack.h new file mode 100644 index 0000000..bf9b439 --- /dev/null +++ b/include/hoedown/stack.h @@ -0,0 +1,52 @@ +/* stack.h - simple stacking */ + +#ifndef HOEDOWN_STACK_H +#define HOEDOWN_STACK_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +/********* + * TYPES * + *********/ + +struct hoedown_stack { + void **item; + size_t size; + size_t asize; +}; +typedef struct hoedown_stack hoedown_stack; + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_stack_init: initialize a stack */ +void hoedown_stack_init(hoedown_stack *st, size_t initial_size); + +/* hoedown_stack_uninit: free internal data of the stack */ +void hoedown_stack_uninit(hoedown_stack *st); + +/* hoedown_stack_grow: increase the allocated size to the given value */ +void hoedown_stack_grow(hoedown_stack *st, size_t neosz); + +/* hoedown_stack_push: push an item to the top of the stack */ +void hoedown_stack_push(hoedown_stack *st, void *item); + +/* hoedown_stack_pop: retrieve and remove the item at the top of the stack */ +void *hoedown_stack_pop(hoedown_stack *st); + +/* hoedown_stack_top: retrieve the item at the top of the stack */ +void *hoedown_stack_top(const hoedown_stack *st); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_STACK_H **/ diff --git a/include/hoedown/version.c b/include/hoedown/version.c new file mode 100644 index 0000000..744209b --- /dev/null +++ b/include/hoedown/version.c @@ -0,0 +1,9 @@ +#include "version.h" + +void +hoedown_version(int *major, int *minor, int *revision) +{ + *major = HOEDOWN_VERSION_MAJOR; + *minor = HOEDOWN_VERSION_MINOR; + *revision = HOEDOWN_VERSION_REVISION; +} diff --git a/include/hoedown/version.h b/include/hoedown/version.h new file mode 100644 index 0000000..62fb366 --- /dev/null +++ b/include/hoedown/version.h @@ -0,0 +1,33 @@ +/* version.h - holds Hoedown's version */ + +#ifndef HOEDOWN_VERSION_H +#define HOEDOWN_VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * CONSTANTS * + *************/ + +#define HOEDOWN_VERSION "3.0.7" +#define HOEDOWN_VERSION_MAJOR 3 +#define HOEDOWN_VERSION_MINOR 0 +#define HOEDOWN_VERSION_REVISION 7 + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_version: retrieve Hoedown's version numbers */ +void hoedown_version(int *major, int *minor, int *revision); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_VERSION_H **/ diff --git a/include/libqmatrixclient b/include/libqmatrixclient index be7d25e..b467b08 160000 --- a/include/libqmatrixclient +++ b/include/libqmatrixclient @@ -1 +1 @@ -Subproject commit be7d25ed22abd07a254bfb8ff6c30de4fcc79e6a +Subproject commit b467b0816f5f6816778f90b55a9d0b5437310fd5 diff --git a/js/md.js b/js/md.js deleted file mode 100644 index ceae2ef..0000000 --- a/js/md.js +++ /dev/null @@ -1,90 +0,0 @@ -.pragma library - -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}; - -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/js/util.js b/js/util.js deleted file mode 100644 index 6b0ade3..0000000 --- a/js/util.js +++ /dev/null @@ -1,12 +0,0 @@ -.pragma library - -function pushToStack(stack, page) { - if(page && stack.currentItem !== page) { - if(stack.depth === 1) { - stack.replace(page) - } else { - stack.pop(null) - stack.replace(page) - } - } -} diff --git a/qml/main.qml b/qml/main.qml index 216b04d..29aa113 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -1,20 +1,21 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.Material 2.12 import Qt.labs.settings 1.0 import Qt.labs.platform 1.0 as Platform +import Spectral.Panel 2.0 import Spectral.Component 2.0 import Spectral.Page 2.0 +import Spectral.Effect 2.0 import Spectral 0.1 import Spectral.Setting 0.1 -import "qrc:/js/util.js" as Util - ApplicationWindow { - readonly property var currentConnection: accountListView.currentConnection ? accountListView.currentConnection : null + Material.theme: MPalette.theme + Material.background: MPalette.background width: 960 height: 640 @@ -26,9 +27,9 @@ ApplicationWindow { visible: true title: qsTr("Spectral") - Material.theme: MSettings.darkTheme ? Material.Dark : Material.Light - - Material.accent: spectralController.color(currentConnection ? currentConnection.localUserId : "") + background: Rectangle { + color: MSettings.darkTheme ? "#303030" : "#FFFFFF" + } Platform.SystemTrayIcon { visible: MSettings.showTray @@ -54,353 +55,147 @@ ApplicationWindow { quitOnLastWindowClosed: !MSettings.showTray onNotificationClicked: { - roomPage.enteredRoom = currentConnection.room(roomId) - roomPage.goToEvent(eventId) + roomListForm.enteredRoom = spectralController.connection.room(roomId) + roomForm.goToEvent(eventId) showWindow() } onErrorOccured: { - errorDialog.error = error - errorDialog.detail = detail - errorDialog.open() + roomListForm.errorControl.error = error + roomListForm.errorControl.detail = detail + roomListForm.errorControl.visible = true } + onSyncDone: roomListForm.errorControl.visible = false } - AccountListModel { - id: accountListModel - controller: spectralController + Shortcut { + sequence: StandardKey.Quit + onActivated: Qt.quit() } Dialog { - property string error - property string detail + property bool busy: false + width: 360 x: (window.width - width) / 2 y: (window.height - height) / 2 - id: errorDialog + id: loginDialog - title: error + " Error" - contentItem: Label { text: errorDialog.detail } - } + parent: ApplicationWindow.overlay - Component { - id: loginPage + title: "Login" - Login { controller: spectralController } - } + contentItem: Column { + AutoTextField { + width: parent.width - Room { - id: roomPage + id: serverField - parent: null + placeholderText: "Server Address" + text: "https://matrix.org" + } - connection: currentConnection - } + AutoTextField { + width: parent.width - Setting { - id: settingPage + id: usernameField - parent: null + placeholderText: "Username" + } - listModel: accountListModel - } + AutoTextField { + width: parent.width - RowLayout { - anchors.fill: parent - spacing: 0 + id: passwordField - Rectangle { - Layout.preferredWidth: 64 - Layout.fillHeight: true - - id: sideNav - - color: Material.primary - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - AutoListView { - property var currentConnection: null - - Layout.fillWidth: true - Layout.fillHeight: true - - id: accountListView - - model: accountListModel - - spacing: 0 - - clip: true - - delegate: Column { - property bool expanded: accountListView.currentConnection === connection - - width: parent.width - - spacing: 0 - - SideNavButton { - width: parent.width - height: width - - selected: stackView.currentItem === page && currentConnection === connection - - ImageItem { - anchors.fill: parent - anchors.margins: 12 - - hint: user.displayName - source: user.paintable - } - - highlightColor: spectralController.color(user.id) - - page: roomPage - - onClicked: { - accountListView.currentConnection = connection - roomPage.filter = 0 - } - } - - Column { - width: parent.width - height: expanded ? implicitHeight : 0 - - spacing: 0 - clip: true - - SideNavButton { - width: parent.width - height: width - - MaterialIcon { - anchors.fill: parent - - icon: "\ue7f7" - color: "white" - } - - onClicked: roomPage.filter = 1 - } - - SideNavButton { - width: parent.width - height: width - - MaterialIcon { - anchors.fill: parent - - icon: "\ue7fd" - color: "white" - } - - onClicked: roomPage.filter = 2 - } - - SideNavButton { - width: parent.width - height: width - - MaterialIcon { - anchors.fill: parent - - icon: "\ue7fb" - color: "white" - } - - onClicked: roomPage.filter = 3 - } - - Behavior on height { - PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 } - } - } - } - } - - SideNavButton { - Layout.fillWidth: true - Layout.preferredHeight: width - - MaterialIcon { - anchors.fill: parent - - icon: "\ue145" - color: "white" - } - - enabled: !addRoomMenu.opened - onClicked: addRoomMenu.popup() - - Menu { - id: addRoomMenu - - MenuItem { - text:"New Room" - onTriggered: addRoomDialog.open() - - Dialog { - id: addRoomDialog - parent: ApplicationWindow.overlay - - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 360 - - title: "New Room" - modal: true - standardButtons: Dialog.Ok | Dialog.Cancel - - contentItem: Column { - AutoTextField { - width: parent.width - - id: addRoomDialogNameTextField - - placeholderText: "Name" - } - AutoTextField { - width: parent.width - - id: addRoomDialogTopicTextField - - placeholderText: "Topic" - } - } - - onAccepted: spectralController.createRoom(currentConnection, addRoomDialogNameTextField.text, addRoomDialogTopicTextField.text) - } - } - - MenuItem { - text: "Join Room" - - onTriggered: joinRoomDialog.open() - - Dialog { - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 360 - - id: joinRoomDialog - - parent: ApplicationWindow.overlay - - title: "Input Room Alias or ID" - modal: true - standardButtons: Dialog.Ok | Dialog.Cancel - - contentItem: AutoTextField { - id: joinRoomDialogTextField - placeholderText: "#matrix:matrix.org" - } - - onAccepted: spectralController.joinRoom(currentConnection, joinRoomDialogTextField.text) - } - } - - MenuItem { - text: "Direct Chat" - - onTriggered: directChatDialog.open() - - Dialog { - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 360 - - id: directChatDialog - - parent: ApplicationWindow.overlay - - title: "Input User ID" - modal: true - standardButtons: Dialog.Ok | Dialog.Cancel - - contentItem: AutoTextField { - id: directChatDialogTextField - placeholderText: "@bot:matrix.org" - } - - onAccepted: spectralController.createDirectChat(currentConnection, directChatDialogTextField.text) - } - } - } - } - - SideNavButton { - Layout.fillWidth: true - Layout.preferredHeight: width - - MaterialIcon { - anchors.fill: parent - - icon: "\ue8b8" - color: "white" - } - page: settingPage - } - - SideNavButton { - Layout.fillWidth: true - Layout.preferredHeight: width - - MaterialIcon { - anchors.fill: parent - - icon: "\ue8ac" - color: "white" - } - - onClicked: MSettings.confirmOnExit ? confirmExitDialog.open() : Qt.quit() - - Dialog { - x: (window.width - width) / 2 - y: (window.height - height) / 2 - width: 360 - - id: confirmExitDialog - - parent: ApplicationWindow.overlay - - title: "Exit" - modal: true - standardButtons: Dialog.Ok | Dialog.Cancel - - contentItem: Column { - Label { text: "Exit?" } - CheckBox { - text: "Do not ask next time" - checked: !MSettings.confirmOnExit - - onCheckedChanged: MSettings.confirmOnExit = !checked - } - } - - onAccepted: Qt.quit() - } - } + placeholderText: "Password" + echoMode: TextInput.Password } } - StackView { + footer: DialogButtonBox { + Button { + text: "OK" + flat: true + enabled: !loginDialog.busy + + onClicked: loginDialog.doLogin() + } + + Button { + text: "Cancel" + flat: true + enabled: !loginDialog.busy + + onClicked: loginDialog.close() + } + + ToolTip { + id: loginButtonTooltip + + } + } + + onVisibleChanged: { + if (visible) spectralController.onErrorOccured.connect(showError) + else spectralController.onErrorOccured.disconnect(showError) + } + + function showError(error, detail) { + loginDialog.busy = false + loginButtonTooltip.text = error + ": " + detail + loginButtonTooltip.open() + } + + function doLogin() { + if (!(serverField.text.startsWith("http") && serverField.text.includes("://"))) { + loginButtonTooltip.text = "Server address should start with http(s)://" + loginButtonTooltip.open() + return + } + + loginDialog.busy = true + spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text) + + spectralController.connectionAdded.connect(function(conn) { + busy = false + loginDialog.close() + }) + } + } + + SplitView { + anchors.fill: parent + + RoomListPanel { + width: window.width * 0.35 + Layout.minimumWidth: 180 + + id: roomListForm + + clip: true + + controller: spectralController + + onLeaveRoom: roomForm.saveReadMarker(room) + } + + RoomPanel { Layout.fillWidth: true - Layout.fillHeight: true + Layout.minimumWidth: 480 - id: stackView + id: roomForm - initialItem: roomPage + clip: true + + currentRoom: roomListForm.enteredRoom } } Binding { target: imageProvider property: "connection" - value: currentConnection + value: spectralController.connection } function showWindow() { @@ -413,9 +208,9 @@ ApplicationWindow { window.hide() } - Component.onCompleted: { - spectralController.initiated.connect(function() { - if (spectralController.accountCount == 0) stackView.push(loginPage) - }) - } + Component.onCompleted: { + spectralController.initiated.connect(function() { + if (spectralController.accountCount == 0) loginDialog.open() + }) + } } diff --git a/qtquickcontrols2.conf b/qtquickcontrols2.conf index 0cc5319..6db2aaf 100644 --- a/qtquickcontrols2.conf +++ b/qtquickcontrols2.conf @@ -7,7 +7,7 @@ Style=Material [Material] Theme=Light +Variant=Dense Primary=#344955 -Accent=#498882 -;Foreground=Black -;Background=#161616 +Accent=#673AB7 +Font/Family="Roboto,Noto Sans,Noto Color Emoji" diff --git a/res.qrc b/res.qrc index 6f4daaa..d5d29dd 100644 --- a/res.qrc +++ b/res.qrc @@ -2,30 +2,19 @@ qtquickcontrols2.conf qml/main.qml - js/md.js - js/util.js imports/Spectral/Component/Emoji/EmojiPicker.qml imports/Spectral/Component/Emoji/qmldir imports/Spectral/Component/Timeline/DownloadableContent.qml - imports/Spectral/Component/Timeline/GenericBubble.qml imports/Spectral/Component/Timeline/MessageDelegate.qml imports/Spectral/Component/Timeline/qmldir imports/Spectral/Component/Timeline/StateDelegate.qml imports/Spectral/Component/AutoMouseArea.qml imports/Spectral/Component/MaterialIcon.qml imports/Spectral/Component/qmldir - imports/Spectral/Component/SideNavButton.qml imports/Spectral/Effect/ElevationEffect.qml imports/Spectral/Effect/qmldir - imports/Spectral/Menu/MessageContextMenu.qml - imports/Spectral/Menu/qmldir - imports/Spectral/Menu/RoomContextMenu.qml - imports/Spectral/Page/Login.qml imports/Spectral/Page/qmldir - imports/Spectral/Page/Room.qml assets/font/material.ttf - assets/img/avatar.png - assets/img/background.jpg assets/img/icon.icns assets/img/icon.ico assets/img/icon.png @@ -33,25 +22,26 @@ imports/Spectral/Font/MaterialFont.qml imports/Spectral/Font/qmldir imports/Spectral/Setting/qmldir - imports/Spectral/Page/Setting.qml - imports/Spectral/Page/SettingForm.ui.qml - imports/Spectral/Page/SettingCategoryDelegate.qml - imports/Spectral/Page/SettingAccountDelegate.qml - imports/Spectral/Page/LoginForm.ui.qml imports/Spectral/Panel/qmldir imports/Spectral/Panel/RoomDrawer.qml imports/Spectral/Panel/RoomListPanel.qml - imports/Spectral/Panel/RoomListPanelForm.ui.qml imports/Spectral/Panel/RoomPanel.qml - imports/Spectral/Panel/RoomPanelForm.ui.qml imports/Spectral/Panel/RoomHeader.qml - imports/Spectral/Panel/RoomListDelegate.qml imports/Spectral/Component/ScrollHelper.qml imports/Spectral/Component/AutoListView.qml - imports/Spectral/Component/Timeline/TimelineImage.qml - imports/Spectral/Component/Timeline/TimelineLabel.qml imports/Spectral/Component/AutoTextField.qml imports/Spectral/Panel/RoomPanelInput.qml imports/Spectral/Component/SplitView.qml + imports/Spectral/Font/CommonFont.qml + imports/Spectral/Component/Timeline/SectionDelegate.qml + assets/img/roompanel.svg + assets/img/matrix.svg + imports/Spectral/Effect/RippleEffect.qml + imports/Spectral/Effect/CircleMask.qml + assets/img/roompanel-dark.svg + imports/Spectral/Component/Timeline/ImageDelegate.qml + imports/Spectral/Component/Avatar.qml + imports/Spectral/Setting/Palette.qml + imports/Spectral/Component/Timeline/FileDelegate.qml diff --git a/spectral.pro b/spectral.pro index 881e417..bb01f3c 100644 --- a/spectral.pro +++ b/spectral.pro @@ -20,6 +20,9 @@ isEmpty(USE_SYSTEM_SORTFILTERPROXYMODEL) { isEmpty(USE_SYSTEM_QMATRIXCLIENT) { USE_SYSTEM_QMATRIXCLIENT = false } +isEmpty(BUNDLE_FONT) { + BUNDLE_FONT = false +} $$USE_SYSTEM_QMATRIXCLIENT { PKGCONFIG += QMatrixClient @@ -34,6 +37,27 @@ $$USE_SYSTEM_SORTFILTERPROXYMODEL { include(include/SortFilterProxyModel/SortFilterProxyModel.pri) } +INCLUDEPATH += include/hoedown +HEADERS += \ + include/hoedown/autolink.h \ + include/hoedown/buffer.h \ + include/hoedown/document.h \ + include/hoedown/escape.h \ + include/hoedown/html.h \ + include/hoedown/stack.h \ + include/hoedown/version.h + +SOURCES += \ + include/hoedown/autolink.c \ + include/hoedown/buffer.c \ + include/hoedown/document.c \ + include/hoedown/escape.c \ + include/hoedown/html.c \ + include/hoedown/html_blocks.c \ + include/hoedown/html_smartypants.c \ + include/hoedown/stack.c \ + include/hoedown/version.c + # The following define makes your compiler emit warnings if you use # any feature of Qt which as been marked deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the @@ -45,8 +69,14 @@ DEFINES += QT_DEPRECATED_WARNINGS # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 -RESOURCES += \ - res.qrc +RESOURCES += res.qrc +$$BUNDLE_FONT { + message("Bundling fonts.") + DEFINES += BUNDLE_FONT + RESOURCES += font.qrc +} else { + message("Using fonts from operating system.") +} # Additional import path used to resolve QML modules in Qt Creator's code model QML_IMPORT_PATH += imports/ @@ -102,12 +132,10 @@ HEADERS += \ src/emojimodel.h \ src/spectralroom.h \ src/userlistmodel.h \ - src/imageitem.h \ src/accountlistmodel.h \ src/spectraluser.h \ src/notifications/manager.h \ - src/utils.h \ - src/paintable.h + src/utils.h SOURCES += src/main.cpp \ src/controller.cpp \ @@ -117,11 +145,9 @@ SOURCES += src/main.cpp \ src/emojimodel.cpp \ src/spectralroom.cpp \ src/userlistmodel.cpp \ - src/imageitem.cpp \ src/accountlistmodel.cpp \ src/spectraluser.cpp \ - src/utils.cpp \ - src/paintable.cpp + src/utils.cpp unix:!mac { SOURCES += src/notifications/managerlinux.cpp diff --git a/src/controller.cpp b/src/controller.cpp index 06dffed..c600a4a 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -10,6 +10,8 @@ #include "csapi/joining.h" #include "csapi/logout.h" +#include "utils.h" + #include #include #include @@ -42,8 +44,8 @@ Controller::Controller(QObject* parent) Controller::~Controller() { for (Connection* c : m_connections) { - c->saveState(); c->stopSync(); + c->saveState(); } } @@ -58,29 +60,29 @@ inline QString accessTokenFileName(const AccountSettings& account) { void Controller::loginWithCredentials(QString serverAddr, QString user, QString pass) { if (!user.isEmpty() && !pass.isEmpty()) { - Connection* m_connection = new Connection(this); - m_connection->setHomeserver(QUrl(serverAddr)); - m_connection->connectToServer(user, pass, ""); - connect(m_connection, &Connection::connected, [=] { - AccountSettings account(m_connection->userId()); + Connection* conn = new Connection(this); + conn->setHomeserver(QUrl(serverAddr)); + conn->connectToServer(user, pass, ""); + connect(conn, &Connection::connected, [=] { + AccountSettings account(conn->userId()); account.setKeepLoggedIn(true); account.clearAccessToken(); // Drop the legacy - just in case - account.setHomeserver(m_connection->homeserver()); - account.setDeviceId(m_connection->deviceId()); + account.setHomeserver(conn->homeserver()); + account.setDeviceId(conn->deviceId()); account.setDeviceName("Spectral"); - if (!saveAccessToken(account, m_connection->accessToken())) + if (!saveAccessToken(account, conn->accessToken())) qWarning() << "Couldn't save access token"; account.sync(); - addConnection(m_connection); + addConnection(conn); + setConnection(conn); }); - connect(m_connection, &Connection::networkError, - [=](QString error, QByteArray detail) { - emit errorOccured("Network", error); - }); - connect(m_connection, &Connection::loginError, - [=](QString error, QByteArray detail) { - emit errorOccured("Login Failed", error); + connect(conn, &Connection::networkError, + [=](QString error, QString, int, int) { + emit errorOccured("Network Error", error); }); + connect(conn, &Connection::loginError, [=](QString error, QString) { + emit errorOccured("Login Failed", error); + }); } } @@ -98,6 +100,7 @@ void Controller::logout(Connection* conn) { conn->stopSync(); emit conn->stateChanged(); emit conn->loggedOut(); + if (!m_connections.isEmpty()) setConnection(m_connections[0]); }); connect(job, &LogoutJob::failure, this, [=] { emit errorOccured("Server-side Logout Failed", job->errorString()); @@ -109,11 +112,12 @@ void Controller::addConnection(Connection* c) { m_connections.push_back(c); - connect(c, &Connection::syncDone, this, [=] { - c->sync(30000); + c->setLazyLoading(true); - static int counter = 0; - if (++counter % 17 == 2) c->saveState(); + connect(c, &Connection::syncDone, this, [=] { + emit syncDone(); + c->sync(30000); + c->saveState(); }); connect(c, &Connection::loggedOut, this, [=] { dropConnection(c); }); @@ -146,17 +150,17 @@ void Controller::invokeLogin() { c->loadState(); addConnection(c); }); - connect(c, &Connection::loginError, - [=](QString error, QByteArray detail) { - emit errorOccured("Login Failed", error); - }); + connect(c, &Connection::loginError, [=](QString error, QString) { + emit errorOccured("Login Failed", error); + }); connect(c, &Connection::networkError, - [=](QString error, QByteArray detail) { - emit errorOccured("Network", error); + [=](QString error, QString, int, int) { + emit errorOccured("Network Error", error); }); c->connectWithToken(account.userId(), accessToken, account.deviceId()); } } + if (!m_connections.isEmpty()) setConnection(m_connections[0]); emit initiated(); } @@ -184,7 +188,7 @@ bool Controller::saveAccessToken(const AccountSettings& account, auto fileDir = QFileInfo(accountTokenFile).dir(); if (!((fileDir.exists() || fileDir.mkpath(".")) && accountTokenFile.open(QFile::WriteOnly))) { - emit errorOccured("Token", "Cannot save access token."); + emit errorOccured("I/O Denied", "Cannot save access token."); } else { accountTokenFile.write(accessToken); return true; @@ -227,19 +231,22 @@ void Controller::playAudio(QUrl localFile) { connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); }); } -QColor Controller::color(QString userId) { - return QColor(SettingsGroup("UI/Color").value(userId, "#498882").toString()); -} - -void Controller::setColor(QString userId, QColor newColor) { - SettingsGroup("UI/Color").setValue(userId, newColor.name()); -} - void Controller::postNotification(const QString& roomId, const QString& eventId, const QString& roomName, const QString& senderName, - const QString& text, const QImage& icon, - const QUrl& iconPath) { + const QString& text, const QImage& icon) { notificationsManager.postNotification(roomId, eventId, roomName, senderName, - text, icon, iconPath); + text, icon); +} + +int Controller::dpi() { + return SettingsGroup("Interface").value("dpi", 100).toInt(); +} + +void Controller::setDpi(int dpi) { + SettingsGroup("Interface").setValue("dpi", dpi); +} + +QString Controller::removeReply(const QString& text) { + return utils::removeReply(text); } diff --git a/src/controller.h b/src/controller.h index 0234fb4..32fc031 100644 --- a/src/controller.h +++ b/src/controller.h @@ -20,6 +20,8 @@ class Controller : public QObject { connectionDropped) Q_PROPERTY(bool quitOnLastWindowClosed READ quitOnLastWindowClosed WRITE setQuitOnLastWindowClosed NOTIFY quitOnLastWindowClosedChanged) + Q_PROPERTY(Connection* connection READ connection WRITE setConnection NOTIFY + connectionChanged) public: explicit Controller(QObject* parent = nullptr); @@ -30,6 +32,9 @@ class Controller : public QObject { QVector connections() { return m_connections; } + Q_INVOKABLE int dpi(); + Q_INVOKABLE void setDpi(int dpi); + // All the non-Q_INVOKABLE functions. void addConnection(Connection* c); void dropConnection(Connection* c); @@ -47,13 +52,23 @@ class Controller : public QObject { } } - Q_INVOKABLE QColor color(QString userId); - Q_INVOKABLE void setColor(QString userId, QColor newColor); + Connection* connection() { + if (m_connection.isNull()) return nullptr; + return m_connection; + } + + void setConnection(Connection* conn) { + if (!conn) return; + if (conn == m_connection) return; + m_connection = conn; + emit connectionChanged(); + } private: QClipboard* m_clipboard = QApplication::clipboard(); NotificationsManager notificationsManager; QVector m_connections; + QPointer m_connection; QByteArray loadAccessToken(const AccountSettings& account); bool saveAccessToken(const AccountSettings& account, @@ -61,17 +76,21 @@ class Controller : public QObject { void loadSettings(); void saveSettings() const; + Q_INVOKABLE QString removeReply(const QString& text); + private slots: void invokeLogin(); signals: void busyChanged(); void errorOccured(QString error, QString detail); + void syncDone(); void connectionAdded(Connection* conn); void connectionDropped(Connection* conn); void initiated(); void notificationClicked(const QString roomId, const QString eventId); void quitOnLastWindowClosedChanged(); + void connectionChanged(); public slots: void logout(Connection* conn); @@ -82,8 +101,7 @@ class Controller : public QObject { void playAudio(QUrl localFile); void postNotification(const QString& roomId, const QString& eventId, const QString& roomName, const QString& senderName, - const QString& text, const QImage& icon, - const QUrl& iconPath); + const QString& text, const QImage& icon); }; #endif // CONTROLLER_H diff --git a/src/imageitem.cpp b/src/imageitem.cpp deleted file mode 100644 index 91a397b..0000000 --- a/src/imageitem.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include "imageitem.h" - -#include -#include -#include - -ImageItem::ImageItem(QQuickItem *parent) : QQuickPaintedItem(parent) {} - -inline static QString stringtoColor(QString string) { - int hash = 0; - for (int i = 0; i < string.length(); i++) - hash = string.at(i).unicode() + ((hash << 5) - hash); - QString colour = "#"; - for (int j = 0; j < 3; j++) - colour += ("00" + QString::number((hash >> (j * 8)) & 0xFF, 16)).right(2); - return colour; -} - -inline static QImage getImageFromPaintable(QPointer p, QRectF b) { - if (p.isNull()) return {}; - QImage image(p->image(int(b.width()), int(b.height()))); - if (image.isNull()) return {}; - return image; -} - -void ImageItem::paint(QPainter *painter) { - QRectF bounding_rect = boundingRect(); - - painter->setRenderHint(QPainter::Antialiasing, true); - - QImage image(getImageFromPaintable(m_paintable, bounding_rect)); - - if (image.isNull()) { - painter->setPen(Qt::NoPen); - if (m_color.isEmpty()) - painter->setBrush(QColor(stringtoColor(m_hint))); - else - painter->setBrush(QColor(m_color)); - if (m_round) - painter->drawEllipse(0, 0, int(bounding_rect.width()), - int(bounding_rect.height())); - else - painter->drawRect(0, 0, int(bounding_rect.width()), - int(bounding_rect.height())); - painter->setPen(QPen(Qt::white, 2)); - QFont font; - font.setStyleHint(QFont::SansSerif); - - font.setPixelSize(int(bounding_rect.width() / 2)); - font.setBold(true); - painter->setFont(font); - painter->drawText( - QRect(0, 0, int(bounding_rect.width()), int(bounding_rect.height())), - Qt::AlignCenter, m_hint.at(0).toUpper()); - } else { - QImage scaled = image.scaled( - int(bounding_rect.width()) + 1, int(bounding_rect.height()) + 1, - Qt::KeepAspectRatioByExpanding, Qt::FastTransformation); - - QPointF center = bounding_rect.center() - scaled.rect().center(); - - if (m_round) { - QPainterPath clip; - clip.addEllipse( - 0, 0, bounding_rect.width(), - bounding_rect.height()); // this is the shape we want to clip to - painter->setClipPath(clip); - } - - painter->drawImage(center, scaled); - } -} - -void ImageItem::setPaintable(Paintable *paintable) { - if (!paintable) return; - if (!m_paintable.isNull()) m_paintable->disconnect(this); - m_paintable = paintable; - connect(m_paintable, &Paintable::paintableChanged, this, - [=] { this->update(); }); - emit paintableChanged(); - update(); -} - -void ImageItem::setHint(QString newHint) { - if (m_hint != newHint) { - m_hint = newHint; - emit hintChanged(); - update(); - } -} - -void ImageItem::setDefaultColor(QString color) { - if (color != m_color) { - m_color = color; - emit defaultColorChanged(); - update(); - } -} - -void ImageItem::setRound(bool value) { - if (m_round != value) { - m_round = value; - emit roundChanged(); - update(); - } -} diff --git a/src/imageitem.h b/src/imageitem.h deleted file mode 100644 index a0c6816..0000000 --- a/src/imageitem.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef IMAGEITEM_H -#define IMAGEITEM_H - -#include -#include -#include -#include -#include -#include - -#include "paintable.h" - -class ImageItem : public QQuickPaintedItem { - Q_OBJECT - Q_PROPERTY(Paintable* source READ paintable WRITE setPaintable NOTIFY - paintableChanged) - Q_PROPERTY(QString hint READ hint WRITE setHint NOTIFY hintChanged) - Q_PROPERTY(QString defaultColor READ defaultColor WRITE setDefaultColor NOTIFY - defaultColorChanged) - Q_PROPERTY(bool round READ round WRITE setRound NOTIFY roundChanged) - - public: - ImageItem(QQuickItem* parent = nullptr); - - void paint(QPainter* painter); - - Paintable* paintable() { return m_paintable; } - void setPaintable(Paintable* paintable); - - QString hint() { return m_hint; } - void setHint(QString hint); - - QString defaultColor() { return m_color; } - void setDefaultColor(QString color); - - bool round() { return m_round; } - void setRound(bool value); - - signals: - void paintableChanged(); - void hintChanged(); - void defaultColorChanged(); - void roundChanged(); - - private: - QPointer m_paintable; - QString m_hint = "H"; - QString m_color; - bool m_round = true; -}; - -#endif // IMAGEITEM_H diff --git a/src/imageprovider.cpp b/src/imageprovider.cpp index 85aad34..c092b3e 100644 --- a/src/imageprovider.cpp +++ b/src/imageprovider.cpp @@ -1,79 +1,89 @@ #include "imageprovider.h" -#include -#include -#include #include -#include +#include -#include "jobs/mediathumbnailjob.h" +using QMatrixClient::BaseJob; -#include "connection.h" - -using QMatrixClient::MediaThumbnailJob; - -ImageProvider::ImageProvider(QObject* parent) - : QObject(parent), - QQuickImageProvider( - QQmlImageProviderBase::Image, - QQmlImageProviderBase::ForceAsynchronousImageLoading) { -#if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) - qRegisterMetaType(); -#endif +ThumbnailResponse::ThumbnailResponse(QMatrixClient::Connection* c, + QString id, const QSize& size) + : c(c), + mediaId(std::move(id)), + requestedSize(size), + errorStr("Image request hasn't started") { + if (requestedSize.isEmpty()) { + errorStr.clear(); + emit finished(); + return; + } + if (mediaId.count('/') != 1) { + errorStr = + tr("Media id '%1' doesn't follow server/mediaId pattern") + .arg(mediaId); + emit finished(); + return; + } + // Execute a request on the main thread asynchronously + moveToThread(c->thread()); + QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, + Qt::QueuedConnection); } -QImage ImageProvider::requestImage(const QString& id, QSize* pSize, - const QSize& requestedSize) { - if (!id.startsWith("mxc://")) { - qWarning() << "ImageProvider: won't fetch an invalid id:" << id - << "doesn't follow server/mediaId pattern"; - return {}; - } +void ThumbnailResponse::startRequest() { + // Runs in the main thread, not QML thread + Q_ASSERT(QThread::currentThread() == c->thread()); + job = c->getThumbnail(mediaId, requestedSize); + // Connect to any possible outcome including abandonment + // to make sure the QML thread is not left stuck forever. + connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult); +} - QUrl mxcUri{id}; - - QUrl tempfilePath = QUrl::fromLocalFile( - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + - mxcUri.fileName() + "-" + QString::number(requestedSize.width()) + "x" + - QString::number(requestedSize.height()) + ".png"); - - QImage cachedImage; - if (cachedImage.load(tempfilePath.toLocalFile())) { - if (pSize != nullptr) *pSize = cachedImage.size(); - return cachedImage; - } - - MediaThumbnailJob* job = nullptr; - QReadLocker locker(&m_lock); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) - QMetaObject::invokeMethod( - m_connection, - [=] { return m_connection->getThumbnail(mxcUri, requestedSize); }, - Qt::BlockingQueuedConnection, &job); -#else - QMetaObject::invokeMethod(m_connection, "getThumbnail", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(MediaThumbnailJob*, job), - Q_ARG(QUrl, mxcUri), Q_ARG(QSize, requestedSize)); -#endif - if (!job) { - qDebug() << "ImageProvider: failed to send a request"; - return {}; - } - QImage result; +void ThumbnailResponse::prepareResult() { + Q_ASSERT(QThread::currentThread() == job->thread()); + Q_ASSERT(job->error() != BaseJob::Pending); { - QWaitCondition condition; // The most compact way to block on a signal - job->connect(job, &MediaThumbnailJob::finished, job, [&] { - result = job->thumbnail(); - condition.wakeAll(); - }); - condition.wait(&m_lock); + QWriteLocker _(&lock); + if (job->error() == BaseJob::Success) { + image = job->thumbnail(); + errorStr.clear(); + } else if (job->error() == BaseJob::Abandoned) { + errorStr = tr("Image request has been cancelled"); + qDebug() << "ThumbnailResponse: cancelled for" << mediaId; + } else { + errorStr = job->errorString(); + qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" + << errorStr; + } + job = nullptr; } - - if (pSize != nullptr) *pSize = result.size(); - - result.save(tempfilePath.toLocalFile()); - - return result; + emit finished(); +} + +void ThumbnailResponse::doCancel() { + // Runs in the main thread, not QML thread + if (job) { + Q_ASSERT(QThread::currentThread() == job->thread()); + job->abandon(); + } +} + +QQuickTextureFactory* ThumbnailResponse::textureFactory() const { + QReadLocker _(&lock); + return QQuickTextureFactory::textureFactoryForImage(image); +} + +QString ThumbnailResponse::errorString() const { + QReadLocker _(&lock); + return errorStr; +} + +void ThumbnailResponse::cancel() { + QMetaObject::invokeMethod(this, &ThumbnailResponse::doCancel, + Qt::QueuedConnection); +} + +QQuickImageResponse* ImageProvider::requestImageResponse( + const QString& id, const QSize& requestedSize) { + qDebug() << "ImageProvider: requesting " << id << "of size" << requestedSize; + return new ThumbnailResponse(m_connection.load(), id, requestedSize); } diff --git a/src/imageprovider.h b/src/imageprovider.h index 1e668bc..4d15b04 100644 --- a/src/imageprovider.h +++ b/src/imageprovider.h @@ -1,38 +1,67 @@ #ifndef IMAGEPROVIDER_H #define IMAGEPROVIDER_H +#pragma once + +#include + +#include +#include -#include #include -#include +#include -#include "connection.h" +namespace QMatrixClient { +class Connection; +} -class ImageProvider : public QObject, public QQuickImageProvider { +class ThumbnailResponse : public QQuickImageResponse { + Q_OBJECT + public: + ThumbnailResponse(QMatrixClient::Connection* c, QString mediaId, + const QSize& requestedSize); + ~ThumbnailResponse() override = default; + +private slots: + void startRequest(); + void prepareResult(); + void doCancel(); + + private: + QMatrixClient::Connection* c; + const QString mediaId; + const QSize requestedSize; + QMatrixClient::MediaThumbnailJob* job = nullptr; + + QImage image; + QString errorStr; + mutable QReadWriteLock lock; // Guards ONLY these two members above + + QQuickTextureFactory* textureFactory() const override; + QString errorString() const override; + void cancel() override; +}; + +class ImageProvider : public QObject, public QQuickAsyncImageProvider { Q_OBJECT Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE setConnection NOTIFY connectionChanged) public: - explicit ImageProvider(QObject* parent = nullptr); + explicit ImageProvider() = default; - QImage requestImage(const QString& id, QSize* pSize, - const QSize& requestedSize) override; - - void initializeEngine(QQmlEngine* engine, const char* uri); + QQuickImageResponse* requestImageResponse( + const QString& id, const QSize& requestedSize) override; QMatrixClient::Connection* connection() { return m_connection; } - void setConnection(QMatrixClient::Connection* newConnection) { - if (m_connection != newConnection) { - m_connection = newConnection; - emit connectionChanged(); - } + void setConnection(QMatrixClient::Connection* connection) { + m_connection.store(connection); + emit connectionChanged(); } signals: void connectionChanged(); private: - QReadWriteLock m_lock; - QMatrixClient::Connection* m_connection = nullptr; + QAtomicPointer m_connection; }; #endif // IMAGEPROVIDER_H diff --git a/src/main.cpp b/src/main.cpp index 804373f..ca17cf3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,12 +1,13 @@ +#include #include #include +#include #include #include #include "accountlistmodel.h" #include "controller.h" #include "emojimodel.h" -#include "imageitem.h" #include "imageprovider.h" #include "messageeventmodel.h" #include "room.h" @@ -23,7 +24,19 @@ using namespace QMatrixClient; int main(int argc, char *argv[]) { - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD) + if (qgetenv("QT_SCALE_FACTOR").size() == 0) { + QSettings settings("ENCOM", "Spectral"); + float factor = settings.value("Interface/dpi", 100).toFloat() / 100; + + qDebug() << "DPI:" << factor; + + if (factor != -1) + qputenv("QT_SCALE_FACTOR", QString::number(factor).toUtf8()); + } +#endif + + QNetworkProxyFactory::setUseSystemConfiguration(true); QApplication app(argc, argv); @@ -34,7 +47,6 @@ int main(int argc, char *argv[]) { qmlRegisterType("SortFilterProxyModel", 0, 2, "SortFilterProxyModel"); - qmlRegisterType("Spectral", 0, 1, "ImageItem"); qmlRegisterType("Spectral", 0, 1, "Controller"); qmlRegisterType("Spectral", 0, 1, "AccountListModel"); qmlRegisterType("Spectral", 0, 1, "RoomListModel"); @@ -51,6 +63,11 @@ int main(int argc, char *argv[]) { qRegisterMetaType("SpectralRoom*"); qRegisterMetaType("SpectralUser*"); +#if defined(BUNDLE_FONT) + QFontDatabase::addApplicationFont(":/assets/font/roboto.ttf"); + QFontDatabase::addApplicationFont(":/assets/font/twemoji.ttf"); +#endif + QQmlApplicationEngine engine; engine.addImportPath("qrc:/imports"); diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index 60c277b..d5e9799 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -9,16 +9,11 @@ #include #include -#include #include #include // for qmlRegisterType() #include "utils.h" -static QString parseAvatarUrl(QUrl url) { - return url.host() + "/" + url.path(); -} - QHash MessageEventModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles[EventTypeRole] = "eventType"; @@ -39,6 +34,9 @@ QHash MessageEventModel::roleNames() const { roles[LongOperationRole] = "progressInfo"; roles[AnnotationRole] = "annotation"; roles[EventResolvedTypeRole] = "eventResolvedType"; + roles[ReplyEventIdRole] = "replyEventId"; + roles[ReplyAuthorRole] = "replyAuthor"; + roles[ReplyDisplayRole] = "replyDisplay"; roles[UserMarkerRole] = "userMarker"; return roles; } @@ -89,6 +87,9 @@ void MessageEventModel::setRoom(SpectralRoom *room) { {AboveEventTypeRole, AboveAuthorRole, AboveSectionRole, AboveTimeRole}); } + for (auto i = m_currentRoom->maxTimelineIndex() - biggest; + i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) + refreshLastUserEvents(i); }); connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] { beginInsertRows({}, 0, 0); }); @@ -108,9 +109,10 @@ void MessageEventModel::setRoom(SpectralRoom *room) { endMoveRows(); movingEvent = false; } - refreshRow(timelineBaseIndex()); // Refresh the looks + refreshRow(timelineBaseIndex()); // Refresh the looks + refreshLastUserEvents(0); if (m_currentRoom->timelineSize() > 1) // Refresh above - refreshEventRoles(timelineBaseIndex() + 1); + refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole}); if (timelineBaseIndex() > 0) // Refresh below, see #312 refreshEventRoles(timelineBaseIndex() - 1, {AboveEventTypeRole, AboveAuthorRole, @@ -128,9 +130,11 @@ void MessageEventModel::setRoom(SpectralRoom *room) { {ReadMarkerRole}); refreshEventRoles(lastReadEventId, {ReadMarkerRole}); }); - connect( - m_currentRoom, &Room::replacedEvent, this, - [this](const RoomEvent *newEvent) { refreshEvent(newEvent->id()); }); + connect(m_currentRoom, &Room::replacedEvent, this, + [this](const RoomEvent *newEvent) { + refreshLastUserEvents(refreshEvent(newEvent->id()) - + timelineBaseIndex()); + }); connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferCompleted, this, @@ -140,7 +144,7 @@ void MessageEventModel::setRoom(SpectralRoom *room) { connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::readMarkerForUserMoved, this, - [=](User *user, QString fromEventId, QString toEventId) { + [=](User *, QString fromEventId, QString toEventId) { refreshEventRoles(fromEventId, {UserMarkerRole}); refreshEventRoles(toEventId, {UserMarkerRole}); }); @@ -214,6 +218,23 @@ QString MessageEventModel::renderDate(QDateTime timestamp) const { return date.toString(Qt::DefaultLocaleShortDate); } +void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) { + if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow) + return; + + const auto &timelineBottom = m_currentRoom->messageEvents().rbegin(); + const auto &lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); + const auto limit = timelineBottom + std::min(baseTimelineRow + 10, + m_currentRoom->timelineSize()); + for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); + it != limit; ++it) { + if ((*it)->senderId() == lastSender) { + auto idx = index(it - timelineBottom); + emit dataChanged(idx, idx); + } + } +} + int MessageEventModel::rowCount(const QModelIndex &parent) const { if (!m_currentRoom || parent.isValid()) return 0; return m_currentRoom->timelineSize(); @@ -235,13 +256,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { const auto &evt = isPending ? **pendingIt : **timelineIt; if (role == Qt::DisplayRole) { - return utils::eventToString(evt, m_currentRoom, Qt::RichText); + return utils::removeReply(utils::eventToString(evt, m_currentRoom, Qt::RichText)); } if (role == MessageRole) { - static const QRegExp rmReplyRegExp("^> <@.*:.*> .*\n\n(.*)"); - return utils::eventToString(evt, m_currentRoom) - .replace(rmReplyRegExp, "\\1"); + return utils::removeReply(utils::eventToString(evt, m_currentRoom)); } if (role == Qt::ToolTipRole) { @@ -306,13 +325,14 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { if (role == HighlightRole) return m_currentRoom->isEventHighlighted(&evt); - if (role == ReadMarkerRole) return evt.id() == lastReadEventId && row > timelineBaseIndex(); + if (role == ReadMarkerRole) + return evt.id() == lastReadEventId && row > timelineBaseIndex(); if (role == SpecialMarksRole) { if (isPending) return pendingIt->deliveryStatus(); if (is(evt)) return EventStatus::Hidden; - if (evt.isRedacted()) return EventStatus::Redacted; + if (evt.isRedacted()) return EventStatus::Hidden; if (evt.isStateEvent() && static_cast(evt).repeatsState()) @@ -348,6 +368,28 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { return variantList; } + if (role == ReplyEventIdRole || role == ReplyDisplayRole || + role == ReplyAuthorRole) { + const QString &replyEventId = evt.contentJson()["m.relates_to"] + .toObject()["m.in_reply_to"] + .toObject()["event_id"] + .toString(); + if (replyEventId.isEmpty()) return {}; + const auto replyIt = m_currentRoom->findInTimeline(replyEventId); + if (replyIt == m_currentRoom->timelineEdge()) return {}; + const auto& replyEvt = **replyIt; + switch (role) { + case ReplyEventIdRole: + return replyEventId; + case ReplyDisplayRole: + return utils::removeReply(utils::eventToString(replyEvt, m_currentRoom, Qt::RichText)); + case ReplyAuthorRole: + return QVariant::fromValue( + m_currentRoom->user(replyEvt.senderId())); + } + return {}; + } + if (role == AboveEventTypeRole || role == AboveSectionRole || role == AboveAuthorRole || role == AboveTimeRole) for (auto r = row + 1; r < rowCount(); ++r) { diff --git a/src/messageeventmodel.h b/src/messageeventmodel.h index f142a7c..a828f99 100644 --- a/src/messageeventmodel.h +++ b/src/messageeventmodel.h @@ -1,8 +1,8 @@ #ifndef MESSAGEEVENTMODEL_H #define MESSAGEEVENTMODEL_H -#include "spectralroom.h" #include "room.h" +#include "spectralroom.h" #include @@ -30,6 +30,9 @@ class MessageEventModel : public QAbstractListModel { LongOperationRole, AnnotationRole, UserMarkerRole, + ReplyEventIdRole, + ReplyAuthorRole, + ReplyDisplayRole, // For debugging EventResolvedTypeRole, }; @@ -62,6 +65,7 @@ class MessageEventModel : public QAbstractListModel { const QMatrixClient::Room::rev_iter_t& baseIt) const; QString renderDate(QDateTime timestamp) const; + void refreshLastUserEvents(int baseRow); void refreshEventRoles(int row, const QVector& roles = {}); int refreshEventRoles(const QString& eventId, const QVector& roles = {}); diff --git a/src/notifications/manager.h b/src/notifications/manager.h index b55eac2..0115713 100644 --- a/src/notifications/manager.h +++ b/src/notifications/manager.h @@ -23,7 +23,7 @@ class NotificationsManager : public QObject { void postNotification(const QString &roomId, const QString &eventId, const QString &roomName, const QString &senderName, - const QString &text, const QImage &icon, const QUrl &iconPath); + const QString &text, const QImage &icon); signals: void notificationClicked(const QString roomId, const QString eventId); diff --git a/src/notifications/managerlinux.cpp b/src/notifications/managerlinux.cpp index 3a09b08..cd5c7d0 100644 --- a/src/notifications/managerlinux.cpp +++ b/src/notifications/managerlinux.cpp @@ -25,8 +25,7 @@ NotificationsManager::NotificationsManager(QObject *parent) void NotificationsManager::postNotification( const QString &roomid, const QString &eventid, const QString &roomname, - const QString &sender, const QString &text, const QImage &icon, - const QUrl &iconPath) { + const QString &sender, const QString &text, const QImage &icon) { uint id = showNotification(roomname, sender + ": " + text, icon); notificationIds[id] = roomEventId{roomid, eventid}; } diff --git a/src/notifications/managermac.mm b/src/notifications/managermac.mm index 2ddc939..782be1f 100644 --- a/src/notifications/managermac.mm +++ b/src/notifications/managermac.mm @@ -19,13 +19,11 @@ NotificationsManager::postNotification( const QString &roomName, const QString &senderName, const QString &text, - const QImage &icon, - const QUrl &iconPath) + const QImage &icon) { Q_UNUSED(roomId); Q_UNUSED(eventId); Q_UNUSED(icon); - Q_UNUSED(iconPath); NSUserNotification * notif = [[NSUserNotification alloc] init]; diff --git a/src/notifications/managerwin.cpp b/src/notifications/managerwin.cpp index 70d943c..b6bf61a 100644 --- a/src/notifications/managerwin.cpp +++ b/src/notifications/managerwin.cpp @@ -46,8 +46,7 @@ NotificationsManager::NotificationsManager(QObject *parent) : QObject(parent) {} void NotificationsManager::postNotification( const QString &room_id, const QString &event_id, const QString &room_name, - const QString &sender, const QString &text, const QImage &icon, - const QUrl &iconPath) { + const QString &sender, const QString &text, const QImage &icon) { Q_UNUSED(room_id) Q_UNUSED(event_id) Q_UNUSED(icon) @@ -65,9 +64,6 @@ void NotificationsManager::postNotification( templ.setTextField(QString("%1").arg(text).toStdWString(), WinToastTemplate::SecondLine); - templ.setImagePath( - reinterpret_cast(QDir::toNativeSeparators(iconPath.toLocalFile()).utf16())); - count++; CustomHandler *customHandler = new CustomHandler(count, this); notificationIds[count] = roomEventId{room_id, event_id}; diff --git a/src/paintable.cpp b/src/paintable.cpp deleted file mode 100644 index a44b826..0000000 --- a/src/paintable.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#include "paintable.h" - -Paintable::Paintable(QObject *parent) : QObject(parent) {} diff --git a/src/paintable.h b/src/paintable.h deleted file mode 100644 index 5eb56ef..0000000 --- a/src/paintable.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef PAINTABLE_H -#define PAINTABLE_H - -#include -#include - -class Paintable : public QObject { - Q_OBJECT - public: - Paintable(QObject* parent = nullptr); - virtual ~Paintable() = default; - - virtual QImage image(int) = 0; - virtual QImage image(int, int) = 0; - - signals: - void paintableChanged(); -}; - -#endif // PAINTABLE_H diff --git a/src/roomlistmodel.cpp b/src/roomlistmodel.cpp index f6c8839..c1e5114 100644 --- a/src/roomlistmodel.cpp +++ b/src/roomlistmodel.cpp @@ -70,25 +70,24 @@ void RoomListModel::connectRoomSignals(SpectralRoom* room) { [=] { unreadMessagesChanged(room); }); connect(room, &Room::notificationCountChanged, this, [=] { unreadMessagesChanged(room); }); + connect(room, &Room::avatarChanged, this, + [this, room] { refresh(room, {AvatarRole}); }); connect(room, &Room::tagsChanged, this, [=] { refresh(room); }); connect(room, &Room::joinStateChanged, this, [=] { refresh(room); }); connect(room, &Room::addedMessages, this, [=] { refresh(room, {LastEventRole}); }); - connect(room, &Room::aboutToAddNewMessages, this, - [=](QMatrixClient::RoomEventsRange eventsRange) { - RoomEvent* event = (eventsRange.end() - 1)->get(); - User* sender = room->user(event->senderId()); - if (sender == room->localUser()) return; - QUrl _url = room->avatarUrl(); - emit newMessage( - room->id(), event->id(), room->displayName(), - sender->displayname(), utils::eventToString(*event), - room->avatar(128), - QUrl::fromLocalFile(QStandardPaths::writableLocation( - QStandardPaths::CacheLocation) + - "/avatar/" + _url.authority() + '_' + - _url.fileName() + ".png")); - }); + connect(room, &Room::notificationCountChanged, this, [=] { + if (room->notificationCount() == 0) return; + if (room->timelineSize() == 0) return; + const RoomEvent* lastEvent = room->messageEvents().rbegin()->get(); + if (lastEvent->isStateEvent()) return; + User* sender = room->user(lastEvent->senderId()); + if (sender == room->localUser()) return; + emit newMessage( + room->id(), lastEvent->id(), room->displayName(), + sender->displayname(), utils::eventToString(*lastEvent), + room->avatar(128)); + }); } void RoomListModel::updateRoom(Room* room, Room* prev) { @@ -152,7 +151,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const { } SpectralRoom* room = m_rooms.at(index.row()); if (role == NameRole) return room->displayName(); - if (role == PaintableRole) return QVariant::fromValue(room->paintable()); + if (role == AvatarRole) return room->avatarMediaId(); if (role == TopicRole) return room->topic(); if (role == CategoryRole) { if (room->joinState() == JoinState::Invite) return RoomType::Invited; @@ -162,6 +161,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const { return RoomType::Normal; } if (role == UnreadCountRole) return room->unreadCount(); + if (role == NotificationCountRole) return room->notificationCount(); if (role == HighlightCountRole) return room->highlightCount(); if (role == LastEventRole) return room->lastEvent(); if (role == LastActiveTimeRole) return room->lastActiveTime(); @@ -192,10 +192,11 @@ void RoomListModel::unreadMessagesChanged(SpectralRoom* room) { QHash RoomListModel::roleNames() const { QHash roles; roles[NameRole] = "name"; - roles[PaintableRole] = "paintable"; + roles[AvatarRole] = "avatar"; roles[TopicRole] = "topic"; roles[CategoryRole] = "category"; roles[UnreadCountRole] = "unreadCount"; + roles[NotificationCountRole] = "notificationCount"; roles[HighlightCountRole] = "highlightCount"; roles[LastEventRole] = "lastEvent"; roles[LastActiveTimeRole] = "lastActiveTime"; diff --git a/src/roomlistmodel.h b/src/roomlistmodel.h index c42e956..d8c56d6 100644 --- a/src/roomlistmodel.h +++ b/src/roomlistmodel.h @@ -31,10 +31,11 @@ class RoomListModel : public QAbstractListModel { public: enum EventRoles { NameRole = Qt::UserRole + 1, - PaintableRole, + AvatarRole, TopicRole, CategoryRole, UnreadCountRole, + NotificationCountRole, HighlightCountRole, LastEventRole, LastActiveTimeRole, @@ -76,7 +77,7 @@ class RoomListModel : public QAbstractListModel { void roomAdded(SpectralRoom* room); void newMessage(const QString& roomId, const QString& eventId, const QString& roomName, const QString& senderName, - const QString& text, const QImage& icon, const QUrl& iconPath); + const QString& text, const QImage& icon); }; #endif // ROOMLISTMODEL_H diff --git a/src/spectralroom.cpp b/src/spectralroom.cpp index 01f38dc..2dd1060 100644 --- a/src/spectralroom.cpp +++ b/src/spectralroom.cpp @@ -9,64 +9,67 @@ #include "events/typingevent.h" #include +#include +#include #include #include +#include "html.h" + #include "utils.h" SpectralRoom::SpectralRoom(Connection* connection, QString roomId, JoinState joinState) - : Room(connection, std::move(roomId), joinState), m_paintable(this) { + : Room(connection, std::move(roomId), joinState) { connect(this, &SpectralRoom::notificationCountChanged, this, &SpectralRoom::countChanged); connect(this, &SpectralRoom::highlightCountChanged, this, &SpectralRoom::countChanged); connect(this, &Room::addedMessages, this, [=] { setBusy(false); }); + connect(this, &Room::fileTransferCompleted, this, [=] { + setFileUploadingProgress(0); + setHasFileUploading(false); + }); +} + +inline QString getMIME(const QUrl& fileUrl) { + return QMimeDatabase().mimeTypeForFile(fileUrl.toLocalFile()).name(); +} + +inline QSize getImageSize(const QUrl& imageUrl) { + QImageReader reader(imageUrl.toLocalFile()); + return reader.size(); } void SpectralRoom::chooseAndUploadFile() { auto localFile = QFileDialog::getOpenFileUrl(Q_NULLPTR, tr("Save File as")); if (!localFile.isEmpty()) { - UploadContentJob* job = - connection()->uploadFile(localFile.toLocalFile(), getMIME(localFile)); - if (isJobRunning(job)) { - setHasFileUploading(true); - connect(job, &BaseJob::uploadProgress, this, - [=](qint64 bytesSent, qint64 bytesTotal) { - if (bytesTotal != 0) { - setFileUploadingProgress(bytesSent * 100 / bytesTotal); - } - }); - connect(job, &BaseJob::success, this, - [=] { postFile(localFile, job->contentUri()); }); - connect(job, &BaseJob::finished, this, [=] { - setHasFileUploading(false); + QString txnID = postFile(localFile.fileName(), localFile, false); + setHasFileUploading(true); + connect(this, &Room::fileTransferCompleted, + [=](QString id, QUrl localFile, QUrl mxcUrl) { + if (id == txnID) { + setFileUploadingProgress(0); + setHasFileUploading(false); + } + }); + connect(this, &Room::fileTransferFailed, [=](QString id, QString error) { + if (id == txnID) { setFileUploadingProgress(0); - }); - } else { - qDebug() << "Failed transfer."; - } + setHasFileUploading(false); + } + }); + connect( + this, &Room::fileTransferProgress, + [=](QString id, qint64 progress, qint64 total) { + if (id == txnID) { + qDebug() << "Progress:" << progress << total; + setFileUploadingProgress(int(float(progress) / float(total) * 100)); + } + }); } } -void SpectralRoom::postFile(const QUrl& localFile, const QUrl& mxcUrl) { - const QString mime = getMIME(localFile); - const QString fileName = localFile.fileName(); - QString msgType = "m.file"; - if (mime.startsWith("image")) msgType = "m.image"; - if (mime.startsWith("video")) msgType = "m.video"; - if (mime.startsWith("audio")) msgType = "m.audio"; - QJsonObject json{QJsonObject{{"msgtype", msgType}, - {"body", fileName}, - {"filename", fileName}, - {"url", mxcUrl.url()}}}; - postJson("m.room.message", json); -} - -QString SpectralRoom::getMIME(const QUrl& fileUrl) const { - return QMimeDatabase().mimeTypeForFile(fileUrl.toLocalFile()).name(); -} - void SpectralRoom::saveFileAs(QString eventId) { auto fileName = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save File as"), fileNameToDownload(eventId)); @@ -85,16 +88,14 @@ bool SpectralRoom::hasUsersTyping() { return count != 0; } -QString SpectralRoom::getUsersTyping() { - QString usersTypingStr; +QVariantList SpectralRoom::getUsersTyping() { QList users = usersTyping(); users.removeOne(localUser()); + QVariantList out; for (User* user : users) { - usersTypingStr += user->displayname() + " "; + out.append(QVariant::fromValue(user)); } - usersTypingStr += users.count() < 2 ? "is" : "are"; - usersTypingStr += " typing."; - return usersTypingStr; + return out; } void SpectralRoom::sendTypingNotification(bool isTyping) { @@ -162,8 +163,6 @@ QDateTime SpectralRoom::lastActiveTime() { return messageEvents().rbegin()->get()->timestamp(); } -float SpectralRoom::orderForTag(QString name) { return tag(name).order; } - int SpectralRoom::savedTopVisibleIndex() const { return firstDisplayedMarker() == timelineEdge() ? 0 @@ -204,3 +203,22 @@ QVariantList SpectralRoom::getUsers(const QString& prefix) { return matchedList; } + +QString SpectralRoom::postMarkdownText(const QString& markdown) { + unsigned char *sequence = (unsigned char *) qstrdup(markdown.toUtf8().constData()); + qint64 length = strlen((char *) sequence); + + hoedown_renderer* renderer = hoedown_html_renderer_new(HOEDOWN_HTML_USE_XHTML, 32); + hoedown_extensions extensions = (hoedown_extensions) ((HOEDOWN_EXT_BLOCK | HOEDOWN_EXT_SPAN | HOEDOWN_EXT_MATH_EXPLICIT) & ~HOEDOWN_EXT_QUOTE); + hoedown_document* document = hoedown_document_new(renderer, extensions, 32); + hoedown_buffer* html = hoedown_buffer_new(length); + hoedown_document_render(document, html, sequence, length); + QString result = QString::fromUtf8((char *) html->data, html->size); + + free(sequence); + hoedown_buffer_free(html); + hoedown_document_free(document); + hoedown_html_renderer_free(renderer); + + return postHtmlText(markdown, result); +} diff --git a/src/spectralroom.h b/src/spectralroom.h index ebb736e..68722dc 100644 --- a/src/spectralroom.h +++ b/src/spectralroom.h @@ -1,7 +1,6 @@ #ifndef SpectralRoom_H #define SpectralRoom_H -#include "paintable.h" #include "room.h" #include "spectraluser.h" @@ -11,31 +10,10 @@ using namespace QMatrixClient; -class RoomPaintable : public Paintable { - Q_OBJECT - public: - RoomPaintable(Room* parent) : Paintable(parent), m_room(parent) { - connect(m_room, &Room::avatarChanged, [=] { emit paintableChanged(); }); - } - - QImage image(int dimension) override { - if (!m_room) return QImage(); - return m_room->avatar(dimension); - } - QImage image(int width, int height) override { - if (!m_room) return QImage(); - return m_room->avatar(width, height); - } - - private: - Room* m_room; -}; - class SpectralRoom : public Room { Q_OBJECT - Q_PROPERTY(Paintable* paintable READ paintable CONSTANT) Q_PROPERTY(bool hasUsersTyping READ hasUsersTyping NOTIFY typingChanged) - Q_PROPERTY(QString usersTyping READ getUsersTyping NOTIFY typingChanged) + Q_PROPERTY(QVariantList usersTyping READ getUsersTyping NOTIFY typingChanged) Q_PROPERTY(QString cachedInput READ cachedInput WRITE setCachedInput NOTIFY cachedInputChanged) Q_PROPERTY(bool hasFileUploading READ hasFileUploading NOTIFY @@ -48,8 +26,6 @@ class SpectralRoom : public Room { explicit SpectralRoom(Connection* connection, QString roomId, JoinState joinState = {}); - Paintable* paintable() { return &m_paintable; } - const QString& cachedInput() const { return m_cachedInput; } void setCachedInput(const QString& input) { if (input != m_cachedInput) { @@ -67,7 +43,7 @@ class SpectralRoom : public Room { } bool hasUsersTyping(); - QString getUsersTyping(); + QVariantList getUsersTyping(); QString lastEvent(); bool isEventHighlighted(const QMatrixClient::RoomEvent* e) const; @@ -90,7 +66,6 @@ class SpectralRoom : public Room { } } - Q_INVOKABLE float orderForTag(QString name); Q_INVOKABLE int savedTopVisibleIndex() const; Q_INVOKABLE int savedBottomVisibleIndex() const; Q_INVOKABLE void saveViewport(int topIndex, int bottomIndex); @@ -99,6 +74,8 @@ class SpectralRoom : public Room { Q_INVOKABLE QVariantList getUsers(const QString& prefix); + Q_INVOKABLE QString postMarkdownText(const QString& markdown); + private: QString m_cachedInput; QSet highlights; @@ -108,9 +85,6 @@ class SpectralRoom : public Room { bool m_busy = false; - QString getMIME(const QUrl& fileUrl) const; - void postFile(const QUrl& localFile, const QUrl& mxcUrl); - void checkForHighlights(const QMatrixClient::TimelineItem& ti); void onAddNewTimelineEvents(timeline_iter_t from) override; @@ -133,9 +107,6 @@ class SpectralRoom : public Room { void sendTypingNotification(bool isTyping); void sendReply(QString userId, QString eventId, QString replyContent, QString sendContent); - - private: - RoomPaintable m_paintable; }; #endif // SpectralRoom_H diff --git a/src/spectraluser.h b/src/spectraluser.h index 11a81d4..e62e51e 100644 --- a/src/spectraluser.h +++ b/src/spectraluser.h @@ -1,44 +1,18 @@ #ifndef SpectralUser_H #define SpectralUser_H -#include "paintable.h" #include "room.h" #include "user.h" #include -#include using namespace QMatrixClient; -class UserPaintable : public Paintable { - Q_OBJECT - public: - UserPaintable(User* parent) : Paintable(parent), m_user(parent) {} - - QImage image(int dimension) override { - if (!m_user) return QImage(); - return m_user->avatar(dimension); - } - QImage image(int width, int height) override { - if (!m_user) return QImage(); - return m_user->avatar(width, height); - } - - private: - User* m_user; -}; - class SpectralUser : public User { Q_OBJECT - Q_PROPERTY(Paintable* paintable READ paintable CONSTANT) public: SpectralUser(QString userId, Connection* connection) - : User(userId, connection), m_paintable(this) {} - - Paintable* paintable() { return &m_paintable; } - - private: - UserPaintable m_paintable; + : User(userId, connection) {} }; #endif // SpectralUser_H diff --git a/src/userlistmodel.cpp b/src/userlistmodel.cpp index eb242d9..15cff77 100644 --- a/src/userlistmodel.cpp +++ b/src/userlistmodel.cpp @@ -34,12 +34,8 @@ void UserListModel::setRoom(QMatrixClient::Room* room) { connect(m_currentRoom, &Room::memberRenamed, this, &UserListModel::userAdded); { - QElapsedTimer et; - et.start(); m_users = m_currentRoom->users(); std::sort(m_users.begin(), m_users.end(), room->memberSorter()); - qDebug() << "Sorting" << m_users.size() << "user(s) in" - << m_currentRoom->displayName() << "took" << et; } for (User* user : m_users) { connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged); @@ -72,8 +68,8 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const { if (role == UserIDRole) { return user->id(); } - if (role == PaintableRole) { - return QVariant::fromValue((static_cast(user))->paintable()); + if (role == AvatarRole) { + return user->avatarMediaId(); } return QVariant(); @@ -115,7 +111,7 @@ void UserListModel::refresh(QMatrixClient::User* user, QVector roles) { void UserListModel::avatarChanged(QMatrixClient::User* user, const QMatrixClient::Room* context) { - if (context == m_currentRoom) refresh(user, {PaintableRole}); + if (context == m_currentRoom) refresh(user, {AvatarRole}); } int UserListModel::findUserPos(User* user) const { @@ -130,6 +126,6 @@ QHash UserListModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[UserIDRole] = "userId"; - roles[PaintableRole] = "paintable"; + roles[AvatarRole] = "avatar"; return roles; } diff --git a/src/userlistmodel.h b/src/userlistmodel.h index 88146ad..fd01a5e 100644 --- a/src/userlistmodel.h +++ b/src/userlistmodel.h @@ -17,7 +17,7 @@ class UserListModel : public QAbstractListModel { Q_PROPERTY( QMatrixClient::Room* room READ room WRITE setRoom NOTIFY roomChanged) public: - enum EventRoles { NameRole = Qt::UserRole + 1, UserIDRole, PaintableRole }; + enum EventRoles { NameRole = Qt::UserRole + 1, UserIDRole, AvatarRole }; using User = QMatrixClient::User; diff --git a/src/utils.cpp b/src/utils.cpp index b14e9be..3154dd3 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -2,5 +2,14 @@ QString utils::removeReply(const QString& text) { QString result(text); - return result.remove(utils::removeReplyRegex); + result.remove(utils::removeRichReplyRegex); + result.remove(utils::removeReplyRegex); + return result; +} + +QString utils::cleanHTML(const QString& text, QMatrixClient::Room* room) { + QString result(text); + result.replace(codePillRegExp, "\\1"); + result.replace(userPillRegExp, "\\1"); + return result; } diff --git a/src/utils.h b/src/utils.h index 2304610..cce426d 100644 --- a/src/utils.h +++ b/src/utils.h @@ -2,9 +2,10 @@ #define Utils_H #include "room.h" +#include "user.h" #include -#include +#include #include #include @@ -13,9 +14,18 @@ #include namespace utils { -const QRegExp removeReplyRegex{"> <.*>.*\\n\\n"}; +static const QRegularExpression removeReplyRegex{ + "> <.*?>.*?\\n\\n", QRegularExpression::DotMatchesEverythingOption}; +static const QRegularExpression removeRichReplyRegex{ + ".*?", QRegularExpression::DotMatchesEverythingOption}; +static const QRegularExpression codePillRegExp{ + "
        (.*?)
        ", QRegularExpression::DotMatchesEverythingOption}; +static const QRegularExpression userPillRegExp{ + "(.*?)", + QRegularExpression::DotMatchesEverythingOption}; QString removeReply(const QString& text); +QString cleanHTML(const QString& text, QMatrixClient::Room* room); template QString eventToString(const BaseEventT& evt, @@ -31,13 +41,8 @@ QString eventToString(const BaseEventT& evt, if (prettyPrint && e.hasTextContent() && e.mimeType().name() != "text/plain") { - static const QRegExp userPillRegExp( - "(.*)"); - QString formattedStr( - static_cast(e.content())->body); - formattedStr.replace(userPillRegExp, - "\\1"); - return formattedStr; + return cleanHTML(static_cast(e.content())->body, + room); } if (e.hasFileContent()) { auto fileCaption = e.content()->fileInfo()->originalName;