Rewrite note parsing in C

This eliminates any parsing choppyness

Fixes: #32
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2022-10-17 15:20:38 -07:00
parent 277ead6efc
commit eb99e6c323
10 changed files with 657 additions and 7 deletions

View File

@@ -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"

257
damus-c/damus.c Normal file
View File

@@ -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;
}
}

44
damus-c/damus.h Normal file
View File

@@ -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 */

180
damus-c/utf8.c Normal file
View File

@@ -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;
}

54
damus-c/utf8.h Normal file
View File

@@ -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 */

View File

@@ -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";
};

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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")
}