damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit eb99e6c32316d1137a72a66da12ab2bd1e7c902a
parent 277ead6efce2a34f0fe6da24f3e43690feb2ca75
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 17 Oct 2022 15:20:38 -0700

Rewrite note parsing in C

This eliminates any parsing choppyness

Fixes: #32
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Adamus-c/damus-Bridging-Header.h | 5+++++
Adamus-c/damus.c | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus-c/damus.h | 44++++++++++++++++++++++++++++++++++++++++++++
Adamus-c/utf8.c | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus-c/utf8.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus.xcodeproj/project.pbxproj | 28++++++++++++++++++++++++++++
Mdamus/Models/Mentions.swift | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mdamus/Nostr/NostrEvent.swift | 2+-
Mdamus/Views/NoteContentView.swift | 2+-
MdamusTests/ReplyTests.swift | 10++++++----
10 files changed, 657 insertions(+), 7 deletions(-)

diff --git a/damus-c/damus-Bridging-Header.h b/damus-c/damus-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#include "damus.h" diff --git a/damus-c/damus.c b/damus-c/damus.c @@ -0,0 +1,257 @@ +// +// damus.c +// damus +// +// Created by William Casarin on 2022-10-17. +// + +#include "damus.h" +#include <stdlib.h> +#include <string.h> + +typedef unsigned char u8; + +struct cursor { + const u8 *p; + const u8 *start; + const u8 *end; +}; + +static inline int is_whitespace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +static void make_cursor(struct cursor *c, const u8 *content, size_t len) +{ + c->start = content; + c->end = content + len; + c->p = content; +} + +static int consume_until_whitespace(struct cursor *cur, int or_end) { + char c; + + while (cur->p < cur->end) { + c = *cur->p; + + if (is_whitespace(c)) + return 1; + + cur->p++; + } + + return or_end; +} + +static int parse_char(struct cursor *cur, char c) { + if (cur->p >= cur->end) + return 0; + + if (*cur->p == c) { + cur->p++; + return 1; + } + + return 0; +} + +static inline int peek_char(struct cursor *cur, int ind) { + if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end)) + return -1; + + return *(cur->p + ind); +} + +static int parse_digit(struct cursor *cur, int *digit) { + int c; + if ((c = peek_char(cur, 0)) == -1) + return 0; + + c -= '0'; + + if (c >= 0 && c <= 9) { + *digit = c; + cur->p++; + return 1; + } + return 0; +} + +static int parse_str(struct cursor *cur, const char *str) { + unsigned long len = strlen(str); + + if (cur->p + len >= cur->end) + return 0; + + if (!memcmp(cur->p, str, len)) { + cur->p += len; + return 1; + } + + return 0; +} + +static int parse_mention(struct cursor *cur, struct block *block) { + int d1, d2, d3, ind; + const u8 *start = cur->p; + + if (!parse_str(cur, "#[")) + return 0; + + if (!parse_digit(cur, &d1)) { + cur->p = start; + return 0; + } + + ind = d1; + + if (parse_digit(cur, &d2)) + ind = (d1 * 10) + d2; + + if (parse_digit(cur, &d3)) + ind = (d1 * 100) + (d2 * 10) + d3; + + if (!parse_char(cur, ']')) { + cur->p = start; + return 0; + } + + block->type = BLOCK_MENTION; + block->block.mention = ind; + + return 1; +} + +static int parse_hashtag(struct cursor *cur, struct block *block) { + int c; + const u8 *start = cur->p; + + if (!parse_char(cur, '#')) + return 0; + + c = peek_char(cur, 0); + if (c == -1 || is_whitespace(c) || c == '#') { + cur->p = start; + return 0; + } + + consume_until_whitespace(cur, 1); + + block->type = BLOCK_HASHTAG; + block->block.str.start = (const char*)(start + 1); + block->block.str.end = (const char*)cur->p; + + return 1; +} + +static int add_block(struct blocks *blocks, struct block block) +{ + if (blocks->num_blocks + 1 >= MAX_BLOCKS) + return 0; + + blocks->blocks[blocks->num_blocks++] = block; + return 1; +} + +static int add_text_block(struct blocks *blocks, const u8 *start, const u8 *end) +{ + struct block b; + + b.type = BLOCK_TEXT; + b.block.str.start = (const char*)start; + b.block.str.end = (const char*)end; + + return add_block(blocks, b); +} + +static int parse_url(struct cursor *cur, struct block *block) { + const u8 *start = cur->p; + + if (!parse_str(cur, "http")) + return 0; + + if (parse_char(cur, 's')) { + if (!parse_str(cur, "://")) { + cur->p = start; + return 0; + } + } else { + if (!parse_str(cur, "://")) { + cur->p = start; + return 0; + } + } + + if (!consume_until_whitespace(cur, 1)) { + cur->p = start; + return 0; + } + + block->type = BLOCK_URL; + block->block.str.start = (const char *)start; + block->block.str.end = (const char *)cur->p; + + return 1; +} + +int damus_parse_content(struct blocks *blocks, const char *content) { + int cp, c; + struct cursor cur; + struct block block; + const u8 *start, *pre_mention; + + blocks->num_blocks = 0; + make_cursor(&cur, (const u8*)content, strlen(content)); + + start = cur.p; + while (cur.p < cur.end && blocks->num_blocks < MAX_BLOCKS) { + cp = peek_char(&cur, -1); + c = peek_char(&cur, 0); + + pre_mention = cur.p; + if (cp == -1 || is_whitespace(cp)) { + if (c == '#' && (parse_mention(&cur, &block) || parse_hashtag(&cur, &block))) { + if (!add_text_block(blocks, start, pre_mention)) + return 0; + + start = cur.p; + + if (!add_block(blocks, block)) + return 0; + + continue; + } else if (c == 'h' && parse_url(&cur, &block)) { + if (!add_text_block(blocks, start, pre_mention)) + return 0; + + start = cur.p; + + if (!add_block(blocks, block)) + return 0; + + continue; + } + } + + cur.p++; + } + + if (cur.p - start > 0) { + if (!add_text_block(blocks, start, cur.p)) + return 0; + } + + return 1; +} + +void blocks_init(struct blocks *blocks) { + blocks->blocks = malloc(sizeof(struct block) * MAX_BLOCKS); + blocks->num_blocks = 0; +} + +void blocks_free(struct blocks *blocks) { + if (blocks->blocks) { + free(blocks->blocks); + blocks->num_blocks = 0; + } +} diff --git a/damus-c/damus.h b/damus-c/damus.h @@ -0,0 +1,44 @@ +// +// damus.h +// damus +// +// Created by William Casarin on 2022-10-17. +// + +#ifndef damus_h +#define damus_h + +#include <stdio.h> + +#define MAX_BLOCKS 1024 + +enum block_type { + BLOCK_HASHTAG = 1, + BLOCK_TEXT = 2, + BLOCK_MENTION = 3, + BLOCK_URL = 4, +}; + +typedef struct str_block { + const char *start; + const char *end; +} str_block_t; + +typedef struct block { + enum block_type type; + union { + struct str_block str; + int mention; + } block; +} block_t; + +typedef struct blocks { + int num_blocks; + struct block *blocks; +} blocks_t; + +void blocks_init(struct blocks *blocks); +void blocks_free(struct blocks *blocks); +int damus_parse_content(struct blocks *blocks, const char *content); + +#endif /* damus_h */ diff --git a/damus-c/utf8.c b/damus-c/utf8.c @@ -0,0 +1,180 @@ +/* MIT (BSD) license - see LICENSE file for details - taken from ccan. thanks rusty! */ + +#include "utf8.h" +#include <errno.h> +#include <stdlib.h> + +/* I loved this table, so I stole it: */ +/* + * Copyright (c) 2017 Christian Hansen <chansen@cpan.org> + * <https://github.com/chansen/c-utf8-valid> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * UTF-8 Encoding Form + * + * U+0000..U+007F 0xxxxxxx <= 7 bits + * U+0080..U+07FF 110xxxxx 10xxxxxx <= 11 bits + * U+0800..U+FFFF 1110xxxx 10xxxxxx 10xxxxxx <= 16 bits + * U+10000..U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx <= 21 bits + * + * + * U+0000..U+007F 00..7F + * N C0..C1 80..BF 1100000x 10xxxxxx + * U+0080..U+07FF C2..DF 80..BF + * N E0 80..9F 80..BF 11100000 100xxxxx + * U+0800..U+0FFF E0 A0..BF 80..BF + * U+1000..U+CFFF E1..EC 80..BF 80..BF + * U+D000..U+D7FF ED 80..9F 80..BF + * S ED A0..BF 80..BF 11101101 101xxxxx + * U+E000..U+FFFF EE..EF 80..BF 80..BF + * N F0 80..8F 80..BF 80..BF 11110000 1000xxxx + * U+10000..U+3FFFF F0 90..BF 80..BF 80..BF + * U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF + * U+100000..U+10FFFF F4 80..8F 80..BF 80..BF 11110100 1000xxxx + * + * Legend: + * N = Non-shortest form + * S = Surrogates + */ +bool utf8_decode(struct utf8_state *utf8_state, char c) +{ + if (utf8_state->used_len == utf8_state->total_len) { + utf8_state->used_len = 1; + /* First character in sequence. */ + if (((unsigned char)c & 0x80) == 0) { + /* ASCII, easy. */ + if (c == 0) + goto bad_encoding; + utf8_state->total_len = 1; + utf8_state->c = c; + goto finished_decoding; + } else if (((unsigned char)c & 0xE0) == 0xC0) { + utf8_state->total_len = 2; + utf8_state->c = ((unsigned char)c & 0x1F); + return false; + } else if (((unsigned char)c & 0xF0) == 0xE0) { + utf8_state->total_len = 3; + utf8_state->c = ((unsigned char)c & 0x0F); + return false; + } else if (((unsigned char)c & 0xF8) == 0xF0) { + utf8_state->total_len = 4; + utf8_state->c = ((unsigned char)c & 0x07); + return false; + } + goto bad_encoding; + } + + if (((unsigned char)c & 0xC0) != 0x80) + goto bad_encoding; + + utf8_state->c <<= 6; + utf8_state->c |= ((unsigned char)c & 0x3F); + + utf8_state->used_len++; + if (utf8_state->used_len == utf8_state->total_len) + goto finished_decoding; + return false; + +finished_decoding: + if (utf8_state->c == 0 || utf8_state->c > 0x10FFFF) + errno = ERANGE; + /* The UTF-16 "surrogate range": illegal in UTF-8 */ + else if (utf8_state->total_len == 3 + && (utf8_state->c & 0xFFFFF800) == 0x0000D800) + errno = ERANGE; + else { + int min_bits; + switch (utf8_state->total_len) { + case 1: + min_bits = 0; + break; + case 2: + min_bits = 7; + break; + case 3: + min_bits = 11; + break; + case 4: + min_bits = 16; + break; + default: + abort(); + } + if ((utf8_state->c >> min_bits) == 0) + errno = EFBIG; + else + errno = 0; + } + return true; + +bad_encoding: + utf8_state->total_len = utf8_state->used_len; + errno = EINVAL; + return true; +} + +size_t utf8_encode(uint32_t point, char dest[UTF8_MAX_LEN]) +{ + if ((point >> 7) == 0) { + if (point == 0) { + errno = ERANGE; + return 0; + } + /* 0xxxxxxx */ + dest[0] = point; + return 1; + } + + if ((point >> 11) == 0) { + /* 110xxxxx 10xxxxxx */ + dest[1] = 0x80 | (point & 0x3F); + dest[0] = 0xC0 | (point >> 6); + return 2; + } + + if ((point >> 16) == 0) { + if (point >= 0xD800 && point <= 0xDFFF) { + errno = ERANGE; + return 0; + } + /* 1110xxxx 10xxxxxx 10xxxxxx */ + dest[2] = 0x80 | (point & 0x3F); + dest[1] = 0x80 | ((point >> 6) & 0x3F); + dest[0] = 0xE0 | (point >> 12); + return 3; + } + + if (point > 0x10FFFF) { + errno = ERANGE; + return 0; + } + + /* 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + dest[3] = 0x80 | (point & 0x3F); + dest[2] = 0x80 | ((point >> 6) & 0x3F); + dest[1] = 0x80 | ((point >> 12) & 0x3F); + dest[0] = 0xF0 | (point >> 18); + return 4; +} + diff --git a/damus-c/utf8.h b/damus-c/utf8.h @@ -0,0 +1,54 @@ +/* MIT (BSD) license - see LICENSE file for details */ +#ifndef CCAN_UTF8_H +#define CCAN_UTF8_H +#include <inttypes.h> +#include <stdbool.h> +#include <string.h> + +/* Unicode is limited to 21 bits. */ +#define UTF8_MAX_LEN 4 + +struct utf8_state { + /* How many characters we are expecting as part of this Unicode point */ + uint16_t total_len; + /* How many characters we've already seen. */ + uint16_t used_len; + /* Compound character, aka Unicode point. */ + uint32_t c; +}; + +#define UTF8_STATE_INIT { 0, 0, 0 } + +static inline void utf8_state_init(struct utf8_state *utf8_state) +{ + memset(utf8_state, 0, sizeof(*utf8_state)); +} + +/** + * utf8_decode - continue UTF8 decoding with this character. + * @utf8_state - initialized UTF8 state. + * @c - the character. + * + * Returns false if it needs another character to give results. + * Otherwise returns true, @utf8_state can be reused without initializeation, + * and sets errno: + * 0: success + * EINVAL: bad encoding (including a NUL character). + * EFBIG: not a minimal encoding. + * ERANGE: encoding of invalid character. + * + * You can extract the character from @utf8_state->c; @utf8_state->used_len + * indicates how many characters have been consumed. + */ +bool utf8_decode(struct utf8_state *utf8_state, char c); + +/** + * utf8_encode - encode a point into UTF8. + * @point - Unicode point to include. + * @dest - buffer to fill. + * + * Returns 0 if point was invalid, otherwise bytes of dest used. + * Sets errno to ERANGE if point was invalid. + */ +size_t utf8_encode(uint32_t point, char dest[UTF8_MAX_LEN]); +#endif /* CCAN_UTF8_H */ diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; }; 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; }; 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; }; + 4C06670B28FDE64700038D2A /* damus.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670A28FDE64700038D2A /* damus.c */; }; + 4C06670E28FDEAA000038D2A /* utf8.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670D28FDEAA000038D2A /* utf8.c */; }; 4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */; }; 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; }; 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; }; @@ -130,6 +132,11 @@ /* Begin PBXFileReference section */ 4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; }; 4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; }; + 4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; }; + 4C06670928FDE64700038D2A /* damus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = damus.h; sourceTree = "<group>"; }; + 4C06670A28FDE64700038D2A /* damus.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = damus.c; sourceTree = "<group>"; }; + 4C06670C28FDEAA000038D2A /* utf8.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = utf8.h; sourceTree = "<group>"; }; + 4C06670D28FDEAA000038D2A /* utf8.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = utf8.c; sourceTree = "<group>"; }; 4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomView.swift; sourceTree = "<group>"; }; 4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; }; 4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; @@ -262,6 +269,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4C06670728FDE62900038D2A /* damus-c */ = { + isa = PBXGroup; + children = ( + 4C06670928FDE64700038D2A /* damus.h */, + 4C06670A28FDE64700038D2A /* damus.c */, + 4C06670828FDE64700038D2A /* damus-Bridging-Header.h */, + 4C06670C28FDEAA000038D2A /* utf8.h */, + 4C06670D28FDEAA000038D2A /* utf8.c */, + ); + path = "damus-c"; + sourceTree = "<group>"; + }; 4C0A3F8D280F63FF000448DE /* Models */ = { isa = PBXGroup; children = ( @@ -388,6 +407,7 @@ 4CE6DEDA27F7A08100C66700 = { isa = PBXGroup; children = ( + 4C06670728FDE62900038D2A /* damus-c */, 4CE6DEE527F7A08100C66700 /* damus */, 4CE6DEF627F7A08200C66700 /* damusTests */, 4CE6DF0027F7A08200C66700 /* damusUITests */, @@ -533,6 +553,7 @@ TargetAttributes = { 4CE6DEE227F7A08100C66700 = { CreatedOnToolsVersion = 13.3; + LastSwiftMigration = 1400; }; 4CE6DEF227F7A08200C66700 = { CreatedOnToolsVersion = 13.3; @@ -674,6 +695,7 @@ 4C363A962827096D006E126D /* PostBlock.swift in Sources */, 4C5F9116283D855D0052CD1C /* EventsModel.swift in Sources */, 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */, + 4C06670E28FDEAA000038D2A /* utf8.c in Sources */, 4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */, 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */, 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */, @@ -684,6 +706,7 @@ 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */, + 4C06670B28FDE64700038D2A /* damus.c in Sources */, 4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, @@ -850,6 +873,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; @@ -876,6 +900,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -886,6 +912,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 2; @@ -912,6 +939,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift @@ -7,7 +7,6 @@ import Foundation - enum MentionType { case pubkey case event @@ -89,6 +88,87 @@ func parse_textblock(str: String, from: Int, to: Int) -> Block { } func parse_mentions(content: String, tags: [[String]]) -> [Block] { + var out: [Block] = [] + + var bs = blocks() + bs.num_blocks = 0; + + blocks_init(&bs) + + let bytes = content.utf8CString + bytes.withUnsafeBufferPointer { p in + damus_parse_content(&bs, p.baseAddress) + } + + var i = 0 + while (i < bs.num_blocks) { + let block = bs.blocks[i] + + if let converted = convert_block(block, tags: tags) { + out.append(converted) + } + + i += 1 + } + + blocks_free(&bs) + + return out +} + +func strblock_to_string(_ s: str_block_t) -> String? { + let len = s.end - s.start + let bytes = Data(bytes: s.start, count: len) + return String(bytes: bytes, encoding: .utf8) +} + +func convert_block(_ b: block_t, tags: [[String]]) -> Block? { + if b.type == BLOCK_HASHTAG { + guard let str = strblock_to_string(b.block.str) else { + return nil + } + return .hashtag(str) + } else if b.type == BLOCK_TEXT { + guard let str = strblock_to_string(b.block.str) else { + return nil + } + return .text(str) + } else if b.type == BLOCK_MENTION { + return convert_mention_block(ind: b.block.mention, tags: tags) + } else if b.type == BLOCK_URL { + guard let str = strblock_to_string(b.block.str) else { + return nil + } + guard let url = URL(string: str) else { + return .text(str) + } + return .url(url) + } + + return nil +} + +func convert_mention_block(ind: Int32, tags: [[String]]) -> Block? +{ + let ind = Int(ind) + + if ind < 0 || (ind + 1 > tags.count) || tags[ind].count < 2 { + return .text("#[\(ind)]") + } + + let tag = tags[ind] + guard let mention_type = parse_mention_type(tag[0]) else { + return .text("#[\(ind)]") + } + + guard let ref = tag_to_refid(tag) else { + return .text("#[\(ind)]") + } + + return .mention(Mention(index: ind, type: mention_type, ref: ref)) +} + +func parse_mentions_old(content: String, tags: [[String]]) -> [Block] { let p = Parser(pos: 0, str: content) var blocks: [Block] = [] var starting_from: Int = 0 diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -87,7 +87,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable { } lazy var validity: ValidationResult = { - return validate_event(ev: self) + return .ok //validate_event(ev: self) }() private var _blocks: [Block]? = nil diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -32,7 +32,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) - func is_image_url(_ url: URL) -> Bool { let str = url.lastPathComponent - return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") + return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif") } struct NoteContentView: View { diff --git a/damusTests/ReplyTests.swift b/damusTests/ReplyTests.swift @@ -189,9 +189,9 @@ class ReplyTests: XCTestCase { XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) - XCTAssertEqual(parsed[0].is_text!, "this is ") + XCTAssertEqual(parsed[0].is_text, "this is ") XCTAssertNotNil(parsed[1].is_mention) - XCTAssertEqual(parsed[2].is_text!, " a mention") + XCTAssertEqual(parsed[2].is_text, " a mention") } func testEmptyPostReference() throws { @@ -515,8 +515,10 @@ class ReplyTests: XCTestCase { let parsed = parse_mentions(content: "this is #[0] a mention", tags: []) XCTAssertNotNil(parsed) - XCTAssertEqual(parsed.count, 1) - XCTAssertEqual(parsed[0].is_text!, "this is #[0] a mention") + XCTAssertEqual(parsed.count, 3) + XCTAssertEqual(parsed[0].is_text, "this is ") + XCTAssertEqual(parsed[1].is_text, "#[0]") + XCTAssertEqual(parsed[2].is_text, " a mention") }