morphit/node_modules/@beblurt/dblurt/lib/content.js

177 lines
7.4 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_CONTENT_TAG_LIMIT = void 0;
exports.normalizePermlink = normalizePermlink;
exports.normalizeContentTags = normalizeContentTags;
exports.buildPostPermlink = buildPostPermlink;
exports.buildReplyPermlink = buildReplyPermlink;
exports.buildCommentMetadata = buildCommentMetadata;
exports.parseCommentMetadata = parseCommentMetadata;
exports.buildPostOperation = buildPostOperation;
exports.buildReplyOperation = buildReplyOperation;
exports.buildUpdateOperation = buildUpdateOperation;
exports.buildDeleteCommentOperation = buildDeleteCommentOperation;
const errors_1 = require("./errors");
exports.DEFAULT_CONTENT_TAG_LIMIT = 8;
const DEFAULT_METADATA_FORMAT = 'markdown';
const MAX_PERMLINK_LENGTH = 255; // Layer 1 requires size < BLURT_MAX_PERMLINK_LENGTH (256)
function slugify(input, fallback) {
const normalized = input
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
.replace(/[^a-z0-9-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
return normalized || fallback;
}
/** Normalize a string into a conservative Blurt permlink/category/tag slug. */
function normalizePermlink(input, options = {}) {
const fallback = options.fallback ?? 'post';
const maxLength = options.maxLength ?? MAX_PERMLINK_LENGTH;
const normalized = slugify(input, fallback);
return normalized.slice(0, maxLength).replace(/-$/g, '') || fallback.slice(0, maxLength);
}
/** Normalize and de-duplicate content tags for json_metadata and top-level post categories. */
function normalizeContentTags(tags, options = {}) {
const maxTags = options.maxTags ?? exports.DEFAULT_CONTENT_TAG_LIMIT;
const seen = new Set();
const result = [];
for (const tag of tags) {
const normalized = normalizePermlink(tag, { fallback: '', maxLength: MAX_PERMLINK_LENGTH });
if (!normalized || seen.has(normalized))
continue;
seen.add(normalized);
result.push(normalized);
if (result.length >= maxTags)
break;
}
return result;
}
function appendSuffix(base, suffix) {
if (!suffix)
return base;
const normalizedSuffix = normalizePermlink(suffix, { fallback: '', maxLength: MAX_PERMLINK_LENGTH });
if (!normalizedSuffix)
return base;
const maxBaseLength = Math.max(1, MAX_PERMLINK_LENGTH - normalizedSuffix.length - 1);
const trimmedBase = base.slice(0, maxBaseLength).replace(/-$/g, '') || 'post';
return `${trimmedBase}-${normalizedSuffix}`;
}
function defaultPermlinkSuffix() {
return Date.now().toString(36);
}
/** Build a post permlink from a supplied permlink or title plus optional deterministic suffix. */
function buildPostPermlink(options) {
const suffix = options.suffix ?? (options.permlink ? undefined : defaultPermlinkSuffix());
return appendSuffix(normalizePermlink(options.permlink || options.title, { fallback: 'post' }), suffix);
}
/** Build a reply permlink from a supplied permlink or parent permlink plus optional deterministic suffix. */
function buildReplyPermlink(options) {
const base = options.permlink || `re-${normalizePermlink(options.parentPermlink, { fallback: 'post' })}`;
const suffix = options.suffix ?? (options.permlink ? undefined : defaultPermlinkSuffix());
return appendSuffix(normalizePermlink(base, { fallback: 'reply' }), suffix);
}
/** Build a JSON-encoded comment metadata string from normalized ecosystem fields. */
function buildCommentMetadata(options = {}) {
const metadata = {};
if (options.app)
metadata.app = options.app;
metadata.format = options.format || DEFAULT_METADATA_FORMAT;
const tags = normalizeContentTags(options.tags || [], options);
if (tags.length > 0)
metadata.tags = tags;
Object.assign(metadata, options.extra || {});
if (options.app)
metadata.app = options.app;
metadata.format = options.format || DEFAULT_METADATA_FORMAT;
if (tags.length > 0)
metadata.tags = tags;
return JSON.stringify(metadata);
}
/** Parse comment json_metadata, returning an empty object for blank/invalid/non-object values. */
function parseCommentMetadata(jsonMetadata) {
if (!jsonMetadata)
return {};
try {
const parsed = JSON.parse(jsonMetadata);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed;
}
}
catch {
return {};
}
return {};
}
/**
* Build a top-level `comment` operation for a post without signing or broadcasting.
*
* The first normalized tag becomes the Layer 1 post category (`parent_permlink`).
* The returned operation is suitable for preview, tests, external signing or a
* later broadcast workflow. Throws {@link ValidationError} when no valid tag can
* be used as the top-level category.
*/
function buildPostOperation(options) {
const tags = normalizeContentTags(options.tags, options);
if (tags.length === 0) {
throw new errors_1.ValidationError('A top-level post requires at least one valid tag/category.', {
field: 'tags',
path: ['comment', 'json_metadata', 'tags']
});
}
return ['comment', {
parent_author: '',
parent_permlink: tags[0],
author: options.author,
permlink: buildPostPermlink({ title: options.title, permlink: options.permlink, suffix: options.permlinkSuffix }),
title: options.title,
body: options.body,
json_metadata: buildCommentMetadata({ app: options.app, format: options.format, tags, extra: options.extra, maxTags: options.maxTags })
}];
}
/**
* Build a reply `comment` operation without signing or broadcasting.
*
* The caller supplies the parent author/permlink identity. The returned payload is
* only an operation tuple; authority checks, signing and broadcast remain separate
* application steps.
*/
function buildReplyOperation(options) {
return ['comment', {
parent_author: options.parentAuthor,
parent_permlink: options.parentPermlink,
author: options.author,
permlink: buildReplyPermlink({ parentPermlink: options.parentPermlink, permlink: options.permlink, suffix: options.permlinkSuffix }),
title: '',
body: options.body,
json_metadata: buildCommentMetadata({ app: options.app, format: options.format, tags: options.tags, extra: options.extra, maxTags: options.maxTags })
}];
}
/**
* Build an update `comment` operation from caller-supplied identity fields.
*
* Use this when the target post/reply identity is already known. The helper does
* not fetch existing content, check authority, sign or broadcast.
*/
function buildUpdateOperation(options) {
return ['comment', {
parent_author: options.parentAuthor,
parent_permlink: options.parentPermlink,
author: options.author,
permlink: options.permlink,
title: options.title,
body: options.body,
json_metadata: typeof options.metadata === 'string'
? options.metadata
: JSON.stringify(options.metadata || {})
}];
}
/** Build a `delete_comment` operation without signing or broadcasting. */
function buildDeleteCommentOperation(options) {
return ['delete_comment', {
author: options.author,
permlink: options.permlink
}];
}