307 lines
9.4 KiB
C++
307 lines
9.4 KiB
C++
#include "denote_frontmatter.h"
|
|
|
|
#include <QRegularExpression>
|
|
#include <QStringList>
|
|
|
|
namespace Denote {
|
|
|
|
FileType fileTypeFromExtension(const QString &ext)
|
|
{
|
|
const QString lower = ext.toLower();
|
|
if (lower == QStringLiteral(".md") || lower == QStringLiteral(".markdown"))
|
|
return FileType::MdYaml;
|
|
if (lower == QStringLiteral(".org"))
|
|
return FileType::Org;
|
|
if (lower == QStringLiteral(".txt"))
|
|
return FileType::Txt;
|
|
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
|