feat(watchlist): yaml helpers + schema types
This commit is contained in:
parent
e7714736ce
commit
cade7104db
3 changed files with 118 additions and 0 deletions
40
packages/obsidian/src/watchlist/schema.ts
Normal file
40
packages/obsidian/src/watchlist/schema.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
export type WatchCategory = 'Movie' | 'Series' | 'Anime';
|
||||||
|
export type WatchMediaType = 'Movie' | 'TV Series';
|
||||||
|
|
||||||
|
export interface WatchlistRecord {
|
||||||
|
title: string;
|
||||||
|
engName: string;
|
||||||
|
mediaType: WatchMediaType;
|
||||||
|
category: WatchCategory;
|
||||||
|
watchStatus: string;
|
||||||
|
rating: string;
|
||||||
|
ratingStars: string;
|
||||||
|
year: string;
|
||||||
|
runtime: number | null;
|
||||||
|
seasons: number | null;
|
||||||
|
episodes: number | null;
|
||||||
|
vod: string[];
|
||||||
|
genre: string[];
|
||||||
|
status: string;
|
||||||
|
language: string;
|
||||||
|
country: string;
|
||||||
|
director: string[];
|
||||||
|
writer: string[];
|
||||||
|
producer: string[];
|
||||||
|
contentRating: string;
|
||||||
|
tmdbRating: number | null;
|
||||||
|
tmdbId: string;
|
||||||
|
imdbId: string;
|
||||||
|
releaseDate: string | null;
|
||||||
|
lastAirDate: string | null;
|
||||||
|
nextAirDate: string | null;
|
||||||
|
lastEpisode: string | null;
|
||||||
|
upcomingEpisode: string | null;
|
||||||
|
poster: string | null;
|
||||||
|
trailer: string;
|
||||||
|
homepage: string;
|
||||||
|
imdbPage: string;
|
||||||
|
notionUrl: string;
|
||||||
|
synopsis: string;
|
||||||
|
cast: string;
|
||||||
|
}
|
||||||
25
packages/obsidian/src/watchlist/yaml.ts
Normal file
25
packages/obsidian/src/watchlist/yaml.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
const NEEDS_QUOTE = /[:,#\[\]{}"&*!|>%@`]/;
|
||||||
|
const KEYWORDS = new Set(['null', 'true', 'false', 'yes', 'no']);
|
||||||
|
|
||||||
|
export function yamlScalar(v: unknown): string {
|
||||||
|
if (v === null || v === undefined || v === '') return '';
|
||||||
|
const s = String(v);
|
||||||
|
if (NEEDS_QUOTE.test(s) || s.trim() !== s || KEYWORDS.has(s.toLowerCase())) {
|
||||||
|
return '"' + s.replace(/"/g, '\\"') + '"';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yamlList(items: string[]): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const raw of items) {
|
||||||
|
const g = String(raw).trim();
|
||||||
|
if (!g) continue;
|
||||||
|
parts.push(/^[A-Za-z0-9 ]+$/.test(g) ? g : '"' + g.replace(/"/g, '\\"') + '"');
|
||||||
|
}
|
||||||
|
return '[' + parts.join(', ') + ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function quotedOrNull(v: string | null | undefined): string {
|
||||||
|
return v ? '"' + v + '"' : 'null';
|
||||||
|
}
|
||||||
53
tests/watchlist-yaml.test.ts
Normal file
53
tests/watchlist-yaml.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
import { yamlScalar, yamlList, quotedOrNull } from 'packages/obsidian/src/watchlist/yaml';
|
||||||
|
|
||||||
|
describe('yamlScalar', () => {
|
||||||
|
test('plain string passes through', () => {
|
||||||
|
expect(yamlScalar('English')).toBe('English');
|
||||||
|
});
|
||||||
|
test('empty/null/undefined → empty string', () => {
|
||||||
|
expect(yamlScalar('')).toBe('');
|
||||||
|
expect(yamlScalar(null)).toBe('');
|
||||||
|
expect(yamlScalar(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
test('special chars → quoted', () => {
|
||||||
|
expect(yamlScalar('S2, E6: Glorious Purpose')).toBe('"S2, E6: Glorious Purpose"');
|
||||||
|
expect(yamlScalar('Aaron Moorhead, Justin Benson')).toBe('"Aaron Moorhead, Justin Benson"');
|
||||||
|
});
|
||||||
|
test('comma alone does NOT quote (matches python regex)', () => {
|
||||||
|
// python regex [:#\[\]{}}&*!|>%@`] has no comma; "a, b" quotes via ":"? no — verify: comma not in class, so "Alter, Turtle" stays plain
|
||||||
|
expect(yamlScalar('Rachel Alter and Tommy Turtle')).toBe('Rachel Alter and Tommy Turtle');
|
||||||
|
});
|
||||||
|
test('yaml keywords → quoted', () => {
|
||||||
|
expect(yamlScalar('null')).toBe('"null"');
|
||||||
|
expect(yamlScalar('No')).toBe('"No"');
|
||||||
|
});
|
||||||
|
test('embedded quotes escaped', () => {
|
||||||
|
expect(yamlScalar('He said "hi"')).toBe('"He said \\"hi\\""');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('yamlList', () => {
|
||||||
|
test('plain items unquoted, special quoted', () => {
|
||||||
|
expect(yamlList(['Drama', 'Sci-Fi & Fantasy'])).toBe('[Drama, "Sci-Fi & Fantasy"]');
|
||||||
|
});
|
||||||
|
test('empty list', () => {
|
||||||
|
expect(yamlList([])).toBe('[]');
|
||||||
|
});
|
||||||
|
test('blank items dropped', () => {
|
||||||
|
expect(yamlList(['', 'Drama', ' '])).toBe('[Drama]');
|
||||||
|
});
|
||||||
|
test('Disney+ quoted', () => {
|
||||||
|
expect(yamlList(['Disney+'])).toBe('["Disney+"]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('quotedOrNull', () => {
|
||||||
|
test('value → quoted', () => {
|
||||||
|
expect(quotedOrNull('https://x.y/z')).toBe('"https://x.y/z"');
|
||||||
|
});
|
||||||
|
test('empty/null → null literal', () => {
|
||||||
|
expect(quotedOrNull('')).toBe('null');
|
||||||
|
expect(quotedOrNull(null)).toBe('null');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue