kdenote/lib/denote_frontmatter.cpp

316 lines
9.7 KiB
C++

#include "denote_frontmatter.h"
#include <QRegularExpression>
#include <QStringList>
namespace Denote {
FileType fileTypeFromExtension(const QString &ext)
{
const QString lower = ext.toLower();
// Check the full extension first, then the inner extension for compound types
// e.g. ".md.gpg" -> check ".md.gpg", then ".md"
if (lower == QStringLiteral(".md") || lower == QStringLiteral(".markdown"))
return FileType::MdYaml;
if (lower == QStringLiteral(".org"))
return FileType::Org;
if (lower == QStringLiteral(".txt"))
return FileType::Txt;
// For compound extensions, check if the inner part is supported
int dot = lower.indexOf(QLatin1Char('.'), 1);
if (dot > 0) {
return fileTypeFromExtension(lower.left(dot));
}
return FileType::Unknown;
}
bool supportsFrontMatter(const QString &ext)
{
return fileTypeFromExtension(ext) != FileType::Unknown;
}
// --- YAML frontmatter (Markdown) ---
static FrontMatter parseYamlFrontMatter(const QString &text)
{
FrontMatter fm;
// Match --- delimited block at start of file
static const QRegularExpression reFm(
QStringLiteral("\\A---\\n(.*?)\\n---"),
QRegularExpression::DotMatchesEverythingOption);
auto m = reFm.match(text);
if (!m.hasMatch())
return fm;
const QString block = m.captured(1);
// title: value or title: "value"
static const QRegularExpression reTitle(
QStringLiteral("(?:^|\\n)title:\\s*\"?([^\"\\n]+)\"?"));
auto mt = reTitle.match(block);
if (mt.hasMatch())
fm.title = mt.captured(1).trimmed();
// identifier: value
static const QRegularExpression reId(
QStringLiteral("(?:^|\\n)identifier:\\s*\"?(\\d{8}T\\d{6})\"?"));
auto mi = reId.match(block);
if (mi.hasMatch())
fm.identifier = mi.captured(1);
// signature: value
static const QRegularExpression reSig(
QStringLiteral("(?:^|\\n)signature:\\s*\"?([^\"\\n]+)\"?"));
auto ms = reSig.match(block);
if (ms.hasMatch())
fm.signature = ms.captured(1).trimmed();
// tags: [tag1, tag2] or tags:\n- tag1\n- tag2
// Inline form first
static const QRegularExpression reTagsInline(
QStringLiteral("(?:^|\\n)tags:\\s*\\[([^\\]]*?)\\]"));
auto mtags = reTagsInline.match(block);
if (mtags.hasMatch()) {
const QString raw = mtags.captured(1);
const auto parts = raw.split(QLatin1Char(','), Qt::SkipEmptyParts);
for (const auto &p : parts) {
QString tag = p.trimmed();
tag.remove(QLatin1Char('"'));
if (!tag.isEmpty())
fm.tags.append(tag);
}
} else {
// List form: tags:\n - tag1\n - tag2
static const QRegularExpression reTagsList(
QStringLiteral("(?:^|\\n)tags:\\s*\\n((?:\\s*-\\s*[^\\n]+\\n?)+)"));
auto mlist = reTagsList.match(block);
if (mlist.hasMatch()) {
static const QRegularExpression reItem(
QStringLiteral("\\s*-\\s*\"?([^\"\\n]+)\"?"));
auto it = reItem.globalMatch(mlist.captured(1));
while (it.hasNext()) {
auto item = it.next();
QString tag = item.captured(1).trimmed();
if (!tag.isEmpty())
fm.tags.append(tag);
}
}
}
return fm;
}
static QByteArray marshalYaml(const FrontMatter &fm)
{
QString out;
out += QStringLiteral("---\n");
out += QStringLiteral("title: ") + fm.title + QStringLiteral("\n");
out += QStringLiteral("tags: [");
for (int i = 0; i < fm.tags.size(); ++i) {
if (i > 0)
out += QStringLiteral(", ");
out += fm.tags[i];
}
out += QStringLiteral("]\n");
out += QStringLiteral("identifier: ") + fm.identifier + QStringLiteral("\n");
if (!fm.signature.isEmpty())
out += QStringLiteral("signature: ") + fm.signature + QStringLiteral("\n");
out += QStringLiteral("---\n");
return out.toUtf8();
}
// --- Org-mode frontmatter ---
static FrontMatter parseOrgFrontMatter(const QString &text)
{
FrontMatter fm;
// #+title: value
static const QRegularExpression reTitle(
QStringLiteral("(?:^|\\n)#\\+title:\\s*(.+)"),
QRegularExpression::CaseInsensitiveOption);
auto m = reTitle.match(text);
if (m.hasMatch())
fm.title = m.captured(1).trimmed();
// #+identifier: value
static const QRegularExpression reId(
QStringLiteral("(?:^|\\n)#\\+identifier:\\s*(\\d{8}T\\d{6})"),
QRegularExpression::CaseInsensitiveOption);
auto mi = reId.match(text);
if (mi.hasMatch())
fm.identifier = mi.captured(1);
// #+filetags: :tag1:tag2:
static const QRegularExpression reTags(
QStringLiteral("(?:^|\\n)#\\+filetags:\\s*:?([^\\n]+)"),
QRegularExpression::CaseInsensitiveOption);
auto mt = reTags.match(text);
if (mt.hasMatch()) {
QString raw = mt.captured(1).trimmed();
// Remove surrounding colons
if (raw.startsWith(QLatin1Char(':')))
raw = raw.mid(1);
if (raw.endsWith(QLatin1Char(':')))
raw.chop(1);
fm.tags = raw.split(QLatin1Char(':'), Qt::SkipEmptyParts);
}
// #+signature: value
static const QRegularExpression reSig(
QStringLiteral("(?:^|\\n)#\\+signature:\\s*(.+)"),
QRegularExpression::CaseInsensitiveOption);
auto ms = reSig.match(text);
if (ms.hasMatch())
fm.signature = ms.captured(1).trimmed();
return fm;
}
static QByteArray marshalOrg(const FrontMatter &fm)
{
QString out;
out += QStringLiteral("#+title: ") + fm.title + QStringLiteral("\n");
if (!fm.tags.isEmpty()) {
out += QStringLiteral("#+filetags: :") + fm.tags.join(QLatin1Char(':'))
+ QStringLiteral(":\n");
}
out += QStringLiteral("#+identifier: ") + fm.identifier + QStringLiteral("\n");
if (!fm.signature.isEmpty())
out += QStringLiteral("#+signature: ") + fm.signature + QStringLiteral("\n");
return out.toUtf8();
}
// --- Plain text frontmatter ---
static FrontMatter parseTxtFrontMatter(const QString &text)
{
FrontMatter fm;
// title: value
static const QRegularExpression reTitle(
QStringLiteral("(?:^|\\n)title:\\s*(.+)"),
QRegularExpression::CaseInsensitiveOption);
auto m = reTitle.match(text);
if (m.hasMatch())
fm.title = m.captured(1).trimmed();
// identifier: value
static const QRegularExpression reId(
QStringLiteral("(?:^|\\n)identifier:\\s*(\\d{8}T\\d{6})"),
QRegularExpression::CaseInsensitiveOption);
auto mi = reId.match(text);
if (mi.hasMatch())
fm.identifier = mi.captured(1);
// tags: tag1, tag2
static const QRegularExpression reTags(
QStringLiteral("(?:^|\\n)tags:\\s*(.+)"),
QRegularExpression::CaseInsensitiveOption);
auto mt = reTags.match(text);
if (mt.hasMatch()) {
fm.tags = mt.captured(1).split(QLatin1Char(','), Qt::SkipEmptyParts);
for (auto &t : fm.tags)
t = t.trimmed();
}
// signature: value
static const QRegularExpression reSig(
QStringLiteral("(?:^|\\n)signature:\\s*(.+)"),
QRegularExpression::CaseInsensitiveOption);
auto ms = reSig.match(text);
if (ms.hasMatch())
fm.signature = ms.captured(1).trimmed();
return fm;
}
static QByteArray marshalTxt(const FrontMatter &fm)
{
QString out;
out += QStringLiteral("title: ") + fm.title + QStringLiteral("\n");
if (!fm.tags.isEmpty())
out += QStringLiteral("tags: ") + fm.tags.join(QStringLiteral(", ")) + QStringLiteral("\n");
out += QStringLiteral("identifier: ") + fm.identifier + QStringLiteral("\n");
if (!fm.signature.isEmpty())
out += QStringLiteral("signature: ") + fm.signature + QStringLiteral("\n");
out += QStringLiteral("--\n");
return out.toUtf8();
}
// --- Public API ---
FrontMatter parseFrontMatter(const QByteArray &content, FileType type)
{
const QString text = QString::fromUtf8(content);
switch (type) {
case FileType::MdYaml:
return parseYamlFrontMatter(text);
case FileType::Org:
return parseOrgFrontMatter(text);
case FileType::Txt:
return parseTxtFrontMatter(text);
case FileType::Unknown:
return {};
}
return {};
}
QByteArray marshalFrontMatter(const FrontMatter &fm, FileType type)
{
switch (type) {
case FileType::MdYaml:
return marshalYaml(fm);
case FileType::Org:
return marshalOrg(fm);
case FileType::Txt:
return marshalTxt(fm);
case FileType::Unknown:
return {};
}
return {};
}
QString extractBody(const QByteArray &content, FileType type)
{
const QString text = QString::fromUtf8(content);
switch (type) {
case FileType::MdYaml: {
// Body is everything after closing ---
static const QRegularExpression reFm(
QStringLiteral("\\A---\\n.*?\\n---\\n?"),
QRegularExpression::DotMatchesEverythingOption);
auto m = reFm.match(text);
if (m.hasMatch())
return text.mid(m.capturedEnd());
return text;
}
case FileType::Org: {
// Body starts after the last #+key: line at the top
static const QRegularExpression reHeader(
QStringLiteral("\\A((?:#\\+[^\\n]+\\n)*)"));
auto m = reHeader.match(text);
if (m.hasMatch())
return text.mid(m.capturedEnd());
return text;
}
case FileType::Txt: {
// Body starts after the -- separator line
int idx = text.indexOf(QStringLiteral("\n--\n"));
if (idx >= 0)
return text.mid(idx + 4);
return text;
}
case FileType::Unknown:
return text;
}
return text;
}
} // namespace Denote