Merge pull request #826 from PR0M3TH3AN/codex/create-videotags-utility-function-and-tests

Add video tag collector utility and tests
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-23 11:20:14 -04:00
commit 21ea29bf18
2 files changed
+190

No files matched your search

+136
View File
@@ -0,0 +1,136 @@
// js/utils/videoTags.js
/**
* @module utils/videoTags
* @description Helpers for collecting and formatting normalized video tags from
* various metadata sources.
*/
const HASH_PREFIX_PATTERN = /^#+/;
function isObject(value) {
return Boolean(value) && typeof value === "object";
}
function asIterableStrings(value) {
if (Array.isArray(value)) {
return value;
}
if (typeof value === "string") {
return [value];
}
return [];
}
function normalizeTagValue(rawValue) {
if (typeof rawValue !== "string") {
return "";
}
const trimmed = rawValue.trim();
if (!trimmed) {
return "";
}
const withoutHashes = trimmed.replace(HASH_PREFIX_PATTERN, "").trim();
return withoutHashes;
}
/**
* Prefix a tag with `#` for display purposes, ensuring a single leading hash.
*
* @param {string} tag - The normalized tag to format.
* @returns {string} The tag prefixed with `#` when a tag exists, otherwise an
* empty string.
*/
export function formatTagDisplay(tag) {
if (typeof tag !== "string") {
return "";
}
const trimmed = tag.trim();
if (!trimmed) {
return "";
}
return `#${trimmed.replace(HASH_PREFIX_PATTERN, "").trim()}`;
}
/**
* Collect normalized hashtags from a video object, deduping across NIP-71
* metadata and traditional Nostr tag arrays. Results are sorted alphabetically
* in a case-insensitive manner while preserving the first-seen casing.
*
* @param {object} video - Video metadata that may contain NIP-71 structured
* fields or Nostr tag arrays.
* @param {object} [options]
* @param {boolean} [options.includeHashes=false] - When true, prefix `#` on the
* returned tag strings.
* @returns {string[]} Sorted list of unique tag strings.
*/
export function collectVideoTags(video, { includeHashes = false } = {}) {
if (!isObject(video)) {
return [];
}
const deduped = new Set();
const collected = [];
const pushTag = (rawValue) => {
const normalized = normalizeTagValue(rawValue);
if (!normalized) {
return;
}
const lower = normalized.toLowerCase();
if (deduped.has(lower)) {
return;
}
deduped.add(lower);
collected.push(normalized);
};
if (isObject(video.nip71)) {
const { hashtags, t } = video.nip71;
for (const candidate of asIterableStrings(hashtags)) {
pushTag(candidate);
}
for (const candidate of asIterableStrings(t)) {
pushTag(candidate);
}
}
if (Array.isArray(video.tags)) {
for (const tagTuple of video.tags) {
if (!Array.isArray(tagTuple) || tagTuple.length < 2) {
continue;
}
const [marker, value] = tagTuple;
if (typeof marker !== "string" || marker.toLowerCase() !== "t") {
continue;
}
pushTag(value);
}
}
collected.sort((a, b) => {
const lowerA = a.toLowerCase();
const lowerB = b.toLowerCase();
if (lowerA === lowerB) {
return a.localeCompare(b);
}
return lowerA.localeCompare(lowerB);
});
if (!includeHashes) {
return collected;
}
return collected.map((tag) => formatTagDisplay(tag));
}
export default collectVideoTags;
+54
View File
@@ -0,0 +1,54 @@
import test from "node:test";
import assert from "node:assert/strict";
import { collectVideoTags, formatTagDisplay } from "../../js/utils/videoTags.js";
test("collectVideoTags dedupes across metadata sources and respects casing", () => {
const video = {
nip71: {
hashtags: ["#Tag", "Another"],
t: ["Third"],
},
tags: [
["t", "tag"],
["t", "ANOTHER"],
["t", " #FOURTH "],
["x", "ignored"],
["t"],
],
};
const tags = collectVideoTags(video);
assert.deepEqual(tags, ["Another", "FOURTH", "Tag", "Third"]);
});
test("collectVideoTags sorts case-insensitively and adds hashes when requested", () => {
const video = {
nip71: {
hashtags: ["beta", "Alpha"],
},
tags: [["t", "gamma"], ["t", "alpha"]],
};
const tags = collectVideoTags(video, { includeHashes: true });
assert.deepEqual(tags, ["#Alpha", "#beta", "#gamma"]);
});
test("collectVideoTags handles malformed inputs safely", () => {
assert.deepEqual(collectVideoTags(null), []);
assert.deepEqual(collectVideoTags(undefined), []);
assert.deepEqual(collectVideoTags(42), []);
assert.deepEqual(
collectVideoTags({ nip71: { hashtags: [null, " "] }, tags: [["t"], ["t", null]] }),
[],
);
});
test("formatTagDisplay normalizes leading hashes", () => {
assert.equal(formatTagDisplay(" example"), "#example");
assert.equal(formatTagDisplay("##Example"), "#Example");
assert.equal(formatTagDisplay(""), "");
assert.equal(formatTagDisplay(null), "");
});