177 lines
7.4 KiB
JavaScript
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
|
|
}];
|
|
}
|