- Batch formatted all Journals module files using Prettier with printWidth: 80. - Refactored preventDefault to prevent_default across all Svelte components. - Standardized line breaks for imports and long attribute lists for better readability. - Ensured consistent snake_case naming for internal identifiers.
248 lines
7.7 KiB
TypeScript
248 lines
7.7 KiB
TypeScript
import { ae_util } from '$lib/ae_utils/ae_utils';
|
|
import type { key_val } from '$lib/stores/ae_stores';
|
|
|
|
export interface AeJournalEntryInput {
|
|
name: string;
|
|
content: string;
|
|
tags: string[];
|
|
created_on?: string; // ISO string
|
|
updated_on?: string; // ISO string
|
|
import_id?: string;
|
|
external_id?: string;
|
|
type_code?: string;
|
|
original_filename: string;
|
|
}
|
|
|
|
/**
|
|
* Standard Parser
|
|
* - Treats the whole file as one entry.
|
|
* - First line is title (if it looks like a title).
|
|
* - Rest is content.
|
|
*/
|
|
export async function parse_standard_note(
|
|
file: File,
|
|
text: string
|
|
): Promise<AeJournalEntryInput[]> {
|
|
const lines = text.split('\n');
|
|
let name = file.name.replace(/\.md$/i, '').replace(/\.txt$/i, '');
|
|
let content = text;
|
|
const tags: string[] = [];
|
|
|
|
// Heuristic: If first line is a header, use it as name
|
|
if (lines.length > 0 && lines[0].startsWith('# ')) {
|
|
name = lines[0].substring(2).trim();
|
|
content = lines.slice(1).join('\n').trim();
|
|
} else if (
|
|
lines.length > 0 &&
|
|
lines[0].trim().length > 0 &&
|
|
lines[0].trim().length < 60
|
|
) {
|
|
// First line is short, treat as title if it doesn't look like frontmatter
|
|
if (lines[0].trim() !== '---') {
|
|
name = lines[0].trim();
|
|
content = lines.slice(1).join('\n').trim();
|
|
}
|
|
}
|
|
|
|
// Basic Frontmatter check (YAML style)
|
|
if (lines[0]?.trim() === '---') {
|
|
const endFrontmatter = lines.indexOf('---', 1);
|
|
if (endFrontmatter > -1) {
|
|
const frontmatter = lines.slice(1, endFrontmatter);
|
|
content = lines
|
|
.slice(endFrontmatter + 1)
|
|
.join('\n')
|
|
.trim();
|
|
|
|
// Extract tags or title from frontmatter (very basic parsing)
|
|
frontmatter.forEach((line) => {
|
|
if (line.startsWith('title:'))
|
|
name = line
|
|
.substring(6)
|
|
.trim()
|
|
.replace(/^['"]|['"]$/g, '');
|
|
if (line.startsWith('tags:')) {
|
|
// This is brittle, assumes inline tags like [a, b] or comma separated
|
|
const tagPart = line.substring(5).trim();
|
|
if (tagPart.startsWith('[') && tagPart.endsWith(']')) {
|
|
tagPart
|
|
.substring(1, tagPart.length - 1)
|
|
.split(',')
|
|
.forEach((t) => tags.push(t.trim()));
|
|
} else {
|
|
tagPart.split(',').forEach((t) => tags.push(t.trim()));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const lastModified = new Date(file.lastModified).toISOString();
|
|
|
|
return [
|
|
{
|
|
name,
|
|
content,
|
|
tags,
|
|
updated_on: lastModified,
|
|
created_on: lastModified, // We don't really know creation time from File object usually
|
|
original_filename: file.name,
|
|
type_code: 'note'
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Personal Log Parser
|
|
* - Splits file by dates: `## YYYY-MM-DD`
|
|
*/
|
|
export async function parse_personal_log(
|
|
file: File,
|
|
text: string
|
|
): Promise<AeJournalEntryInput[]> {
|
|
const entries: AeJournalEntryInput[] = [];
|
|
const dateRegex = /^##\s+(\d{4}-\d{2}-\d{2})(.*)$/;
|
|
|
|
const lines = text.split('\n');
|
|
let currentEntry: Partial<AeJournalEntryInput> | null = null;
|
|
let currentContent: string[] = [];
|
|
|
|
// Check if the whole file is just one entry (no date headers)
|
|
if (!lines.some((l) => dateRegex.test(l))) {
|
|
return parse_standard_note(file, text);
|
|
}
|
|
|
|
const fileBaseName = file.name.replace(/\.md$/i, '').replace(/\.txt$/i, '');
|
|
|
|
for (const line of lines) {
|
|
const match = line.match(dateRegex);
|
|
if (match) {
|
|
// Save previous entry
|
|
if (currentEntry) {
|
|
currentEntry.content = currentContent.join('\n').trim();
|
|
entries.push(currentEntry as AeJournalEntryInput);
|
|
}
|
|
|
|
// Start new entry
|
|
const dateStr = match[1];
|
|
const extraTitle = match[2].trim();
|
|
|
|
currentEntry = {
|
|
name: extraTitle
|
|
? `${dateStr} - ${extraTitle}`
|
|
: `${fileBaseName} - ${dateStr}`,
|
|
created_on: `${dateStr}T12:00:00`, // Noon on that day
|
|
updated_on: new Date(file.lastModified).toISOString(),
|
|
tags: ['log'],
|
|
type_code: 'log',
|
|
original_filename: file.name
|
|
// Reconstruct the header as part of content? Or just skip it?
|
|
// Python parser added it back: `## {date_str}\n\n{body}`
|
|
// Let's add it back for context.
|
|
};
|
|
currentContent = [`## ${dateStr} ${extraTitle}`];
|
|
} else {
|
|
if (currentEntry) {
|
|
currentContent.push(line);
|
|
} else {
|
|
// Preamble before first date header? Ignore or treat as separate?
|
|
// Ignoring for now or could be a "Header" entry.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Push last entry
|
|
if (currentEntry) {
|
|
currentEntry.content = currentContent.join('\n').trim();
|
|
entries.push(currentEntry as AeJournalEntryInput);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
/**
|
|
* Amazon Vine Review Parser (from Python logic)
|
|
* - Splits by `## Product Name`
|
|
* - Looks for URL and `### Review Title`
|
|
*/
|
|
export async function parse_amazon_vine(
|
|
file: File,
|
|
text: string
|
|
): Promise<AeJournalEntryInput[]> {
|
|
// Split by `\n## ` but we need to keep the delimiter or reconstruct
|
|
// JS split doesn't keep delimiter nicely unless captured.
|
|
// Let's iterate lines.
|
|
const entries: AeJournalEntryInput[] = [];
|
|
const productHeaderRegex = /^##\s+(.+)$/;
|
|
|
|
const lines = text.split('\n');
|
|
let currentEntry: any = null;
|
|
let currentBody: string[] = [];
|
|
|
|
for (const line of lines) {
|
|
const match = line.match(productHeaderRegex);
|
|
if (match) {
|
|
if (currentEntry) {
|
|
entries.push(
|
|
format_vine_entry(currentEntry, currentBody, file)
|
|
);
|
|
}
|
|
currentEntry = { productName: match[1].trim() };
|
|
currentBody = [];
|
|
} else {
|
|
if (currentEntry) {
|
|
currentBody.push(line);
|
|
}
|
|
}
|
|
}
|
|
if (currentEntry) {
|
|
entries.push(format_vine_entry(currentEntry, currentBody, file));
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
function format_vine_entry(
|
|
entry: any,
|
|
bodyLines: string[],
|
|
file: File
|
|
): AeJournalEntryInput {
|
|
let url = '';
|
|
let reviewTitle = '';
|
|
const cleanBody: string[] = [];
|
|
|
|
for (const line of bodyLines) {
|
|
const trimmed = line.trim();
|
|
if (!url && trimmed.startsWith('* http')) {
|
|
url = trimmed.replace(/^\*\s+/, '').split(' ')[0];
|
|
continue;
|
|
}
|
|
if (!reviewTitle && trimmed.startsWith('### ')) {
|
|
reviewTitle = trimmed.substring(4).trim();
|
|
continue;
|
|
}
|
|
cleanBody.push(line);
|
|
}
|
|
|
|
let content = '';
|
|
if (reviewTitle) content += `# ${reviewTitle}\n\n`;
|
|
content += cleanBody.join('\n').trim();
|
|
if (url) content += `\n\n**Product Link:** ${url}`;
|
|
|
|
return {
|
|
name: entry.productName,
|
|
content: content,
|
|
tags: ['amazon', 'vine', 'review'],
|
|
created_on: new Date(file.lastModified).toISOString(),
|
|
updated_on: new Date(file.lastModified).toISOString(),
|
|
original_filename: file.name,
|
|
type_code: 'review'
|
|
};
|
|
}
|
|
|
|
export const PARSERS = {
|
|
standard: parse_standard_note,
|
|
personal_log: parse_personal_log,
|
|
amazon_vine: parse_amazon_vine
|
|
};
|