Give the interpreter real Celtic Cross meanings and --format/--lang support
- guidance.c now prints last, after the full spread; both it and main.c's new Celtic Cross readout pull real Waite card meanings via tarot_data.c instead of just naming cards. - interpreter-cli gains --format text|html|json and --lang en|de, matching deck-engine - required linking engine/src/tarot_data.c and i18n.c into the interpreter binary (a narrow, documented exception to its earlier "no engine .c files" policy) and writing German translations for narrative.c's/guidance.c's own text.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
#include "guidance.h"
|
||||
|
||||
/* i18n_get() only - see narrative.c's own comment on why this one extra
|
||||
* engine header is needed despite guidance.h's otherwise header-only
|
||||
* engine dependency. */
|
||||
#include "../../engine/src/i18n.h"
|
||||
|
||||
/* Slugs for building "guidance.planet.<slug>"/i18n keys - mirrors
|
||||
* reading_io.c's own k_body_slug, same duplication policy (see
|
||||
* significance.c's comment for why each independently-testable module
|
||||
* keeps its own small table rather than linking astro.c). */
|
||||
static const char *const k_body_slug[NUM_BODIES] = {
|
||||
"sun", "moon", "mercury", "venus", "mars",
|
||||
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
||||
};
|
||||
|
||||
/* English fallback only - "the Sun"/"the Moon" take a definite article
|
||||
* that the other eight planets don't; see engine/i18n/en.lang's own
|
||||
* comment on the guidance.planet.* keys this backs. */
|
||||
static const char *const k_planet_name_fallback[NUM_BODIES] = {
|
||||
"the Sun", "the Moon", "Mercury", "Venus", "Mars",
|
||||
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
||||
};
|
||||
|
||||
static const char *planet_display_name(Body body) {
|
||||
char key[32];
|
||||
snprintf(key, sizeof key, "guidance.planet.%s", k_body_slug[body]);
|
||||
return i18n_get(key, k_planet_name_fallback[body]);
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
CHAR_HARMONIOUS = 0,
|
||||
CHAR_DISCORDANT,
|
||||
CHAR_NEUTRAL,
|
||||
NUM_TRANSIT_CHARACTERS
|
||||
} TransitCharacter;
|
||||
|
||||
typedef struct {
|
||||
const char *slug; /* builds "guidance.stance.<slug>.*" keys */
|
||||
const char *upright; /* Attitude card fell upright (English fallback) */
|
||||
const char *reversed; /* Attitude card fell reversed (English fallback) */
|
||||
} AttitudeStance;
|
||||
|
||||
/* Original guidance text for this project - see guidance.h's doc
|
||||
* comment for why. Fallback (English) text only; German translations
|
||||
* live in engine/i18n/de.lang under the matching "guidance.*" keys.
|
||||
* Indexed by TransitCharacter. */
|
||||
static const AttitudeStance k_stance[NUM_TRANSIT_CHARACTERS] = {
|
||||
[CHAR_HARMONIOUS] = {
|
||||
"harmonious",
|
||||
"You're already meeting the day in the right spirit - lean into "
|
||||
"it rather than second-guessing a favorable position.",
|
||||
"The day itself is working in your favor even though you don't "
|
||||
"feel settled yet - trust the momentum more than your current "
|
||||
"doubts.",
|
||||
},
|
||||
[CHAR_DISCORDANT] = {
|
||||
"discordant",
|
||||
"Your instincts are sound, but the day is testing them - hold "
|
||||
"your position without forcing the issue.",
|
||||
"Both the day and your own footing are unsettled right now - "
|
||||
"this is a moment to steady yourself before pushing forward.",
|
||||
},
|
||||
[CHAR_NEUTRAL] = {
|
||||
"neutral",
|
||||
"Today concentrates whatever you already bring to it - your "
|
||||
"current approach will be amplified, so make sure it's the one "
|
||||
"you want.",
|
||||
"Today intensifies things, including whatever is currently "
|
||||
"unresolved in your own approach - worth sorting out before "
|
||||
"acting.",
|
||||
},
|
||||
};
|
||||
|
||||
static TransitCharacter classify_transit(AspectType type) {
|
||||
if (type == ASPECT_TRINE || type == ASPECT_SEXTILE) return CHAR_HARMONIOUS;
|
||||
if (type == ASPECT_SQUARE || type == ASPECT_OPPOSITION) return CHAR_DISCORDANT;
|
||||
return CHAR_NEUTRAL;
|
||||
}
|
||||
|
||||
static const char *stance_text(TransitCharacter character, bool reversed) {
|
||||
const AttitudeStance *s = &k_stance[character];
|
||||
char key[48];
|
||||
snprintf(key, sizeof key, "guidance.stance.%s.%s", s->slug, reversed ? "reversed" : "upright");
|
||||
return i18n_get(key, reversed ? s->reversed : s->upright);
|
||||
}
|
||||
|
||||
/* How to introduce the top transit, tailored to how strongly it's
|
||||
* scoring (interp->day_level) - independent of the stance sentence
|
||||
* itself (k_stance, above), which is written to stay true regardless of
|
||||
* intensity. Each entry takes exactly one %s: the transiting planet's
|
||||
* display name, always as the sentence's grammatical subject (matters
|
||||
* for German, where the other three day levels put the planet name in
|
||||
* non-nominative position in a naive word-for-word translation - see
|
||||
* engine/i18n/de.lang's guidance.intro.* entries, which are phrased to
|
||||
* avoid that). Fallback (English) text only. */
|
||||
static const char *const k_intro_slug[DAY_SIGNIFICANCE_COUNT] = {
|
||||
[DAY_QUIET] = "quiet",
|
||||
[DAY_NOTABLE] = "notable",
|
||||
[DAY_SIGNIFICANT] = "significant",
|
||||
[DAY_MAJOR] = "major",
|
||||
};
|
||||
|
||||
static const char *const k_intro_fallback[DAY_SIGNIFICANCE_COUNT] = {
|
||||
[DAY_QUIET] = "%s is only faintly active today, but for what it's worth:",
|
||||
[DAY_NOTABLE] = "With a mild touch from %s today:",
|
||||
[DAY_SIGNIFICANT] = "With %s clearly active today:",
|
||||
[DAY_MAJOR] = "With %s as today's dominant influence:",
|
||||
};
|
||||
|
||||
static const char *intro_template(DaySignificance level) {
|
||||
char key[32];
|
||||
snprintf(key, sizeof key, "guidance.intro.%s", k_intro_slug[level]);
|
||||
return i18n_get(key, k_intro_fallback[level]);
|
||||
}
|
||||
|
||||
/* Fallback for a day with no aspects in orb at all (top_item_count ==
|
||||
* 0, always DAY_QUIET) - there's no transiting planet to name, so the
|
||||
* guidance falls back to the Attitude card's orientation alone. */
|
||||
static const char *no_transit_text(bool reversed) {
|
||||
static const char *const upright =
|
||||
"There's no standout transit today - it's an astrologically quiet "
|
||||
"day. That leaves things mostly about steadiness: trust your "
|
||||
"current footing and use the calm to make headway.";
|
||||
static const char *const reversed_text =
|
||||
"There's no standout transit today - it's an astrologically quiet "
|
||||
"day. That's a good moment to quietly re-settle your own "
|
||||
"footing, since nothing external is forcing the pace.";
|
||||
return i18n_get(reversed ? "guidance.no_transit.reversed" : "guidance.no_transit.upright",
|
||||
reversed ? reversed_text : upright);
|
||||
}
|
||||
|
||||
static const char *reversed_marker(bool reversed) {
|
||||
static char buf[32];
|
||||
if (!reversed) return "";
|
||||
snprintf(buf, sizeof buf, " %s", i18n_get("ui.reversed", "(Reversed)"));
|
||||
return buf;
|
||||
}
|
||||
|
||||
void guidance_print(FILE *out, const char *indent, const DailyInterpretation *interp,
|
||||
const CelticCrossSpread *spread) {
|
||||
const TarotDraw *attitude = &spread->positions[POSITION_ATTITUDE];
|
||||
const TarotDraw *outcome = &spread->positions[POSITION_OUTCOME];
|
||||
|
||||
if (interp->top_item_count == 0) {
|
||||
fprintf(out, "%s%s\n", indent, no_transit_text(attitude->reversed));
|
||||
} else {
|
||||
const Aspect *top = &interp->top_items[0].aspect;
|
||||
TransitCharacter character = classify_transit(top->type);
|
||||
const char *stance = stance_text(character, attitude->reversed);
|
||||
|
||||
char intro[192];
|
||||
snprintf(intro, sizeof(intro), intro_template(interp->day_level),
|
||||
planet_display_name(top->transiting_planet));
|
||||
fprintf(out, "%s%s %s\n", indent, intro, stance);
|
||||
}
|
||||
|
||||
fprintf(out, "%s%s %s%s: %s\n", indent, i18n_get("guidance.attitude_intro", "Your Attitude card is"),
|
||||
tarot_card_name(attitude->card), reversed_marker(attitude->reversed),
|
||||
tarot_card_meaning(attitude->card, attitude->reversed));
|
||||
fprintf(out, "%s%s %s%s: %s\n", indent, i18n_get("guidance.outcome_intro", "The spread's likely Outcome is"),
|
||||
tarot_card_name(outcome->card), reversed_marker(outcome->reversed),
|
||||
tarot_card_meaning(outcome->card, outcome->reversed));
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef DECK_GUIDANCE_H
|
||||
#define DECK_GUIDANCE_H
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
/* Header-only dependency on the engine for struct/enum *definitions*,
|
||||
* same policy as narrative.h/significance.h - see their comments for
|
||||
* why. Unlike those, though, the root Makefile *does* link
|
||||
* engine/src/tarot_data.c's *implementation* (tarot_card_name()/
|
||||
* tarot_card_meaning()) into the interpreter binary - a deliberate,
|
||||
* narrow exception; see the Makefile's own comment on
|
||||
* INTERP_TAROT_TEXT_OBJS for why that doesn't compromise this module's
|
||||
* independent testability. */
|
||||
#include "../../engine/src/tarot.h"
|
||||
#include "significance.h"
|
||||
|
||||
/* Ties the day's single most significant transit
|
||||
* (interp->top_items[0]) to the Celtic Cross spread, and prints a short
|
||||
* paragraph of concrete guidance on how to meet the day - the "combined
|
||||
* storytelling" piece docs/reading.md flags as future work, now built.
|
||||
* Prints on every day, quiet or major, tailored to interp->day_level -
|
||||
* callers can invoke it unconditionally after every
|
||||
* interpret_daily_reading() call.
|
||||
*
|
||||
* The core guidance keys off two things: the character of the top
|
||||
* transit's aspect (harmonious/discordant/neutral, same classification
|
||||
* narrative.c uses) and whether the spread's Attitude position - Waite's
|
||||
* "Himself: his position or attitude in the circumstances", the
|
||||
* position most directly about how the reader is meeting the day - fell
|
||||
* upright or reversed. That crossing (3 x 2 = 6 combinations) is
|
||||
* original text written for this project, not drawn from Waite or
|
||||
* Sepharial, since neither source discusses combining astrology and
|
||||
* tarot; it stays valid regardless of the day's intensity. What *does*
|
||||
* vary with interp->day_level is only the sentence introducing the top
|
||||
* transit (e.g. "With Saturn as today's dominant influence" on a Major
|
||||
* day vs. "Saturn is only faintly active today, but for what it's
|
||||
* worth" on a Quiet one). A day with no aspects in orb at all
|
||||
* (interp->top_item_count == 0, always DAY_QUIET) has no transiting
|
||||
* planet to introduce, so it falls back to a stance keyed on the
|
||||
* Attitude card's orientation alone.
|
||||
*
|
||||
* The paragraph also names the Attitude and Outcome cards and, via
|
||||
* tarot_card_meaning(), states their actual Waite meaning for the
|
||||
* orientation they landed in (e.g. what Wheel of Fortune reversed
|
||||
* means) - not a duplicated table, the real thing, since tarot_data.c
|
||||
* is linked (see above).
|
||||
*
|
||||
* Every piece of this module's own text (stance/intro/no-transit/label
|
||||
* strings) is looked up via i18n_get() under "guidance.*" keys
|
||||
* (engine/i18n's .lang files) before falling back to the English text baked
|
||||
* into guidance.c, so it respects whatever --lang the caller loaded
|
||||
* with i18n_load() (main.c does so before calling this). The German
|
||||
* guidance.intro.* templates are deliberately phrased so the planet
|
||||
* name placeholder is always the sentence's grammatical subject
|
||||
* (nominative case) across all four day levels - see guidance.c's own
|
||||
* comment on k_intro_fallback. */
|
||||
void guidance_print(FILE *out, const char *indent, const DailyInterpretation *interp,
|
||||
const CelticCrossSpread *spread);
|
||||
|
||||
#endif
|
||||
+400
-52
@@ -1,25 +1,63 @@
|
||||
#define _POSIX_C_SOURCE 200809L /* for open_memstream */
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../../engine/src/i18n.h"
|
||||
#include "guidance.h"
|
||||
#include "narrative.h"
|
||||
#include "reading_io.h"
|
||||
#include "significance.h"
|
||||
|
||||
typedef enum { FORMAT_TEXT, FORMAT_HTML, FORMAT_JSON } OutputFormat;
|
||||
|
||||
static void print_usage(const char *prog) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s [reading.json]\n\n"
|
||||
"Usage: %s [--format text|html|json] [--lang <code>] [--i18n-dir <path>]\n"
|
||||
" [reading.json]\n\n"
|
||||
"Reads a DailyReading in deck-engine's --format json shape - from the\n"
|
||||
"given file, or from stdin if no file is given (or it is \"-\") - and\n"
|
||||
"prints a significance report: the day's overall level and the top\n"
|
||||
"aspects driving it, each with a short narrative note.\n\n"
|
||||
"prints a full report: the day's overall significance level, the\n"
|
||||
"top aspects driving it (each with a short narrative note), a full\n"
|
||||
"readout of the Celtic Cross spread with every card's meaning, and\n"
|
||||
"finally guidance tying the day's top transit to the spread's\n"
|
||||
"Attitude/Outcome cards.\n\n"
|
||||
" --format Output format, defaults to text. html links card art via\n"
|
||||
" img/<file>, same convention as deck-engine's own html\n"
|
||||
" output - save/run this next to the img/ directory\n"
|
||||
" `make images` copies into dist/. json embeds the same\n"
|
||||
" narrative/guidance prose as text (in whatever --lang was\n"
|
||||
" loaded) alongside stable, language-independent slugs.\n"
|
||||
" --lang Reading language code, defaults to en. Matches a file\n"
|
||||
" named <code>.lang in --i18n-dir (see engine/i18n/) -\n"
|
||||
" note this is this binary's OWN output language, not\n"
|
||||
" related to whatever --lang deck-engine was run with to\n"
|
||||
" produce its --format json input (which is always\n"
|
||||
" language-independent - see docs/input-output-format.md).\n"
|
||||
" --i18n-dir Directory to look up <lang>.lang in. Defaults to the\n"
|
||||
" i18n/ directory next to this binary (`make i18n`\n"
|
||||
" copies engine/i18n/ there as dist/i18n/).\n\n"
|
||||
"Typical use:\n"
|
||||
" dist/deck-engine ... --format json | %s\n"
|
||||
" dist/deck-engine ... --format json > reading.json && %s reading.json\n",
|
||||
prog, prog, prog);
|
||||
}
|
||||
|
||||
/* Same convention as engine/src/main.c's own default_i18n_path() -
|
||||
* duplicated rather than shared, since the two binaries are never
|
||||
* linked together (see CLAUDE.md's "Interpretation" section). */
|
||||
static void default_i18n_path(const char *argv0, const char *lang, char *out, size_t out_size) {
|
||||
const char *slash = strrchr(argv0, '/');
|
||||
if (slash) {
|
||||
int dir_len = (int)(slash - argv0);
|
||||
snprintf(out, out_size, "%.*s/i18n/%s.lang", dir_len, argv0, lang);
|
||||
} else {
|
||||
snprintf(out, out_size, "i18n/%s.lang", lang);
|
||||
}
|
||||
}
|
||||
|
||||
static char *read_all(FILE *f, size_t *out_length) {
|
||||
size_t cap = 4096;
|
||||
size_t len = 0;
|
||||
@@ -39,63 +77,391 @@ static char *read_all(FILE *f, size_t *out_length) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Display names for the report - deliberately a small local table rather
|
||||
* than linking engine/src/astro.c's astro_body_name()/astro_aspect_name()
|
||||
* (which would pull in the vendored Astronomy Engine just for cosmetic
|
||||
* text). Same policy as reading_io.c's slug tables. */
|
||||
static const char *ui(const char *key, const char *fallback) { return i18n_get(key, fallback); }
|
||||
|
||||
/* Slugs for i18n key building (body.<slug>/sign.<slug>/aspect.<slug>,
|
||||
* mirroring astro.c's own tables) and reused as-is for --format json's
|
||||
* language-independent fields - deliberately a small local table rather
|
||||
* than linking astro.c (would pull in the vendored Astronomy Engine just
|
||||
* for cosmetic text/slugs). Same policy as reading_io.c's own tables. */
|
||||
static const char *const k_body_slug[NUM_BODIES] = {
|
||||
"sun", "moon", "mercury", "venus", "mars",
|
||||
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
||||
};
|
||||
static const char *const k_sign_slug[12] = {
|
||||
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
|
||||
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
||||
};
|
||||
static const char *const k_aspect_slug[5] = {
|
||||
"conjunction", "sextile", "square", "trine", "opposition",
|
||||
};
|
||||
static const char *const k_day_level_slug[DAY_SIGNIFICANCE_COUNT] = {
|
||||
"quiet", "notable", "significant", "major",
|
||||
};
|
||||
|
||||
static const char *body_display_name(Body body) {
|
||||
static const char *const names[NUM_BODIES] = {
|
||||
static const char *const fallback[NUM_BODIES] = {
|
||||
"Sun", "Moon", "Mercury", "Venus", "Mars",
|
||||
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
||||
};
|
||||
return names[body];
|
||||
char key[32];
|
||||
snprintf(key, sizeof key, "body.%s", k_body_slug[body]);
|
||||
return ui(key, fallback[body]);
|
||||
}
|
||||
|
||||
static const char *aspect_display_name(AspectType type) {
|
||||
static const char *const names[5] = {
|
||||
static const char *const fallback[5] = {
|
||||
"Conjunction", "Sextile", "Square", "Trine", "Opposition",
|
||||
};
|
||||
return names[type];
|
||||
char key[32];
|
||||
snprintf(key, sizeof key, "aspect.%s", k_aspect_slug[type]);
|
||||
return ui(key, fallback[type]);
|
||||
}
|
||||
|
||||
static const char *sign_display_name(ZodiacSign sign) {
|
||||
static const char *const names[12] = {
|
||||
static const char *const fallback[12] = {
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
};
|
||||
return names[sign];
|
||||
char key[32];
|
||||
snprintf(key, sizeof key, "sign.%s", k_sign_slug[sign]);
|
||||
return ui(key, fallback[sign]);
|
||||
}
|
||||
|
||||
static const char *day_level_name(DaySignificance level) {
|
||||
switch (level) {
|
||||
case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet");
|
||||
case DAY_NOTABLE: return ui("interp.day_level.notable", "Notable");
|
||||
case DAY_SIGNIFICANT: return ui("interp.day_level.significant", "Significant");
|
||||
case DAY_MAJOR: return ui("interp.day_level.major", "Major");
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *reversed_marker(bool reversed) {
|
||||
static char buf[32];
|
||||
if (!reversed) return "";
|
||||
snprintf(buf, sizeof buf, " %s", ui("ui.reversed", "(Reversed)"));
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* pos->house is 0 when reading_load_json had no sign/house data for this
|
||||
* body (see parse_bodies() in reading_io.c) - a valid whole-sign house
|
||||
* is always 1-12, so this is a safe "unknown, say nothing" sentinel. */
|
||||
static void print_body_in_sign(const PlanetPosition *pos) {
|
||||
static void print_body_in_sign_text(FILE *out, const PlanetPosition *pos) {
|
||||
if (pos->house < 1 || pos->house > 12) return;
|
||||
printf(" in %s (house %d)", sign_display_name(pos->sign), pos->house);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof buf, ui("interp.in_sign_house", "in %s (house %d)"),
|
||||
sign_display_name(pos->sign), pos->house);
|
||||
fprintf(out, " %s", buf);
|
||||
}
|
||||
|
||||
static const char *day_level_name(DaySignificance level) {
|
||||
switch (level) {
|
||||
case DAY_QUIET: return "Quiet";
|
||||
case DAY_NOTABLE: return "Notable";
|
||||
case DAY_SIGNIFICANT: return "Significant";
|
||||
case DAY_MAJOR: return "Major";
|
||||
/* Captures narrative_print()/guidance_print()'s output (they only know
|
||||
* how to write to a FILE*) into a heap string, for embedding into JSON -
|
||||
* avoids changing either module's public API just for this one caller.
|
||||
* Strips a single trailing newline; caller must free() the result. */
|
||||
static char *capture_narrative(const Aspect *aspect, int house) {
|
||||
char *buf = NULL;
|
||||
size_t size = 0;
|
||||
FILE *mem = open_memstream(&buf, &size);
|
||||
narrative_print(mem, "", aspect, house);
|
||||
fclose(mem);
|
||||
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
static char *capture_guidance(const DailyInterpretation *interp, const CelticCrossSpread *spread) {
|
||||
char *buf = NULL;
|
||||
size_t size = 0;
|
||||
FILE *mem = open_memstream(&buf, &size);
|
||||
guidance_print(mem, "", interp, spread);
|
||||
fclose(mem);
|
||||
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void print_top_transits_header(FILE *out, int count) {
|
||||
if (count == 1) {
|
||||
fprintf(out, "%s\n", ui("interp.top_transits.one", "Top significant transit:"));
|
||||
} else {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof buf, ui("interp.top_transits.many", "Top %d significant transits:"), count);
|
||||
fprintf(out, "%s\n", buf);
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
/* ===== --format text ===== */
|
||||
|
||||
static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread) {
|
||||
fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||
const TarotDraw *draw = &spread->positions[i];
|
||||
fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_name((CelticCrossPosition)i),
|
||||
tarot_card_name(draw->card), reversed_marker(draw->reversed));
|
||||
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
|
||||
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
|
||||
}
|
||||
}
|
||||
|
||||
static void interp_print_text(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||
fprintf(out, "%s %s (%d/%d)\n", ui("interp.day_significance", "Day significance:"),
|
||||
day_level_name(interp->day_level), interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||||
if (interpretation_deserves_framing(interp)) {
|
||||
fprintf(out, "%s\n", ui("interp.major_framing",
|
||||
"(a major transit today - worth a deeper Celtic Cross look)"));
|
||||
}
|
||||
fprintf(out, "\n");
|
||||
|
||||
if (interp->top_item_count == 0) {
|
||||
fprintf(out, "%s\n", ui("interp.no_notable_transits", "No notable transits today."));
|
||||
} else {
|
||||
print_top_transits_header(out, interp->top_item_count);
|
||||
for (int i = 0; i < interp->top_item_count; i++) {
|
||||
const SignificantItem *item = &interp->top_items[i];
|
||||
const Aspect *a = &item->aspect;
|
||||
|
||||
fprintf(out, " %d. %s %s", i + 1, ui("ui.transiting", "Transiting"),
|
||||
body_display_name(a->transiting_planet));
|
||||
print_body_in_sign_text(out, &reading->transits.bodies[a->transiting_planet]);
|
||||
fprintf(out, " %s %s %s", aspect_display_name(a->type), ui("ui.natal", "natal"),
|
||||
body_display_name(a->natal_planet));
|
||||
print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
|
||||
fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
|
||||
ui("interp.score", "score"), item->score);
|
||||
narrative_print(out, " ", a, reading->transits.bodies[a->transiting_planet].house);
|
||||
}
|
||||
}
|
||||
fprintf(out, "\n");
|
||||
|
||||
print_celtic_cross_text(out, &reading->spread);
|
||||
fprintf(out, "\n");
|
||||
|
||||
guidance_print(out, "", interp, &reading->spread);
|
||||
}
|
||||
|
||||
/* ===== --format html ===== */
|
||||
|
||||
static void interp_print_html(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||
fprintf(out,
|
||||
"<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
|
||||
"<title>%s</title>\n"
|
||||
"<style>\n"
|
||||
"body { font-family: sans-serif; max-width: 900px; margin: 2em auto; }\n"
|
||||
"table { border-collapse: collapse; margin-bottom: 1.5em; }\n"
|
||||
"td { padding: 2px 10px 2px 0; }\n"
|
||||
"h1, h2 { border-bottom: 1px solid #ccc; }\n"
|
||||
".spread { display: flex; flex-wrap: wrap; gap: 1.5em; }\n"
|
||||
".card { width: 160px; }\n"
|
||||
".card img { width: 140px; display: block; }\n"
|
||||
".card img.reversed { transform: rotate(180deg); }\n"
|
||||
".card .position { font-weight: bold; }\n"
|
||||
".card .name { font-style: italic; }\n"
|
||||
".guidance { background: #f6f6f6; padding: 1em; border-radius: 6px; }\n"
|
||||
"</style></head><body>\n",
|
||||
ui("interp.page_title", "Deck in a Dash - Interpretation"));
|
||||
|
||||
fprintf(out, "<h1>%s</h1>\n", ui("interp.heading", "Daily Interpretation"));
|
||||
|
||||
fprintf(out, "<h2>%s</h2>\n<p>%s %s (%d/%d)",
|
||||
ui("interp.heading_day_significance", "Day Significance"),
|
||||
ui("interp.day_significance", "Day significance:"), day_level_name(interp->day_level),
|
||||
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||||
if (interpretation_deserves_framing(interp)) {
|
||||
fprintf(out, "<br>%s", ui("interp.major_framing",
|
||||
"(a major transit today - worth a deeper Celtic Cross look)"));
|
||||
}
|
||||
fprintf(out, "</p>\n");
|
||||
|
||||
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits", "Significant Transits"));
|
||||
if (interp->top_item_count == 0) {
|
||||
fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits", "No notable transits today."));
|
||||
} else {
|
||||
fprintf(out, "<table>\n");
|
||||
for (int i = 0; i < interp->top_item_count; i++) {
|
||||
const SignificantItem *item = &interp->top_items[i];
|
||||
const Aspect *a = &item->aspect;
|
||||
char *text = capture_narrative(a, reading->transits.bodies[a->transiting_planet].house);
|
||||
|
||||
fprintf(out, "<tr><td>%d.</td><td>%s %s %s %s %s</td>"
|
||||
"<td>%s %.1f°, %s %.2f</td></tr>\n",
|
||||
i + 1, ui("ui.transiting", "Transiting"), body_display_name(a->transiting_planet),
|
||||
aspect_display_name(a->type), ui("ui.natal", "natal"),
|
||||
body_display_name(a->natal_planet), ui("ui.orb", "orb"), a->orb,
|
||||
ui("interp.score", "score"), item->score);
|
||||
if (text[0] != '\0') fprintf(out, "<tr><td></td><td colspan=\"2\">%s</td></tr>\n", text);
|
||||
free(text);
|
||||
}
|
||||
fprintf(out, "</table>\n");
|
||||
}
|
||||
|
||||
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||
const TarotDraw *draw = &reading->spread.positions[i];
|
||||
fprintf(out,
|
||||
"<div class=\"card\">\n"
|
||||
" <div class=\"position\">%s</div>\n"
|
||||
" <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
|
||||
" <div class=\"name\">%s%s</div>\n"
|
||||
" <div class=\"desc\">%s</div>\n"
|
||||
" <div class=\"meaning\">%s</div>\n"
|
||||
"</div>\n",
|
||||
tarot_position_name((CelticCrossPosition)i), draw->reversed ? "reversed" : "",
|
||||
tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
|
||||
reversed_marker(draw->reversed), tarot_position_description((CelticCrossPosition)i),
|
||||
tarot_card_meaning(draw->card, draw->reversed));
|
||||
}
|
||||
fprintf(out, "</div>\n");
|
||||
|
||||
char *guidance = capture_guidance(interp, &reading->spread);
|
||||
fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("interp.heading_guidance", "Guidance"));
|
||||
const char *start = guidance;
|
||||
for (const char *p = guidance; ; p++) {
|
||||
if (*p == '\n' || *p == '\0') {
|
||||
if (p > start) fprintf(out, "<p>%.*s</p>\n", (int)(p - start), start);
|
||||
if (*p == '\0') break;
|
||||
start = p + 1;
|
||||
}
|
||||
}
|
||||
free(guidance);
|
||||
fprintf(out, "</div>\n");
|
||||
|
||||
fprintf(out, "</body></html>\n");
|
||||
}
|
||||
|
||||
/* ===== --format json ===== */
|
||||
|
||||
static void json_string(FILE *out, const char *s) {
|
||||
fputc('"', out);
|
||||
for (; *s; s++) {
|
||||
unsigned char c = (unsigned char)*s;
|
||||
switch (c) {
|
||||
case '"': fputs("\\\"", out); break;
|
||||
case '\\': fputs("\\\\", out); break;
|
||||
case '\n': fputs("\\n", out); break;
|
||||
case '\r': fputs("\\r", out); break;
|
||||
case '\t': fputs("\\t", out); break;
|
||||
default:
|
||||
if (c < 0x20) fprintf(out, "\\u%04x", c);
|
||||
else fputc((char)c, out);
|
||||
}
|
||||
}
|
||||
fputc('"', out);
|
||||
}
|
||||
|
||||
static void interp_print_json(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||
fprintf(out, "{\n");
|
||||
|
||||
fprintf(out, " \"day_significance\": {\n");
|
||||
fprintf(out, " \"level\": \"%s\",\n", k_day_level_slug[interp->day_level]);
|
||||
fprintf(out, " \"rank\": %d,\n", interp->day_level + 1);
|
||||
fprintf(out, " \"count\": %d,\n", DAY_SIGNIFICANCE_COUNT);
|
||||
fprintf(out, " \"deserves_framing\": %s\n", interpretation_deserves_framing(interp) ? "true" : "false");
|
||||
fprintf(out, " },\n");
|
||||
|
||||
fprintf(out, " \"significant_transits\": [\n");
|
||||
for (int i = 0; i < interp->top_item_count; i++) {
|
||||
const SignificantItem *item = &interp->top_items[i];
|
||||
const Aspect *a = &item->aspect;
|
||||
const PlanetPosition *tp = &reading->transits.bodies[a->transiting_planet];
|
||||
const PlanetPosition *np = &reading->natal.bodies[a->natal_planet];
|
||||
char *text = capture_narrative(a, tp->house);
|
||||
|
||||
fprintf(out, " {\n");
|
||||
fprintf(out, " \"transiting_planet\": \"%s\",\n", k_body_slug[a->transiting_planet]);
|
||||
fprintf(out, " \"transiting_sign\": \"%s\",\n", k_sign_slug[tp->sign]);
|
||||
fprintf(out, " \"transiting_house\": %d,\n", tp->house);
|
||||
fprintf(out, " \"natal_planet\": \"%s\",\n", k_body_slug[a->natal_planet]);
|
||||
fprintf(out, " \"natal_sign\": \"%s\",\n", k_sign_slug[np->sign]);
|
||||
fprintf(out, " \"natal_house\": %d,\n", np->house);
|
||||
fprintf(out, " \"aspect\": \"%s\",\n", k_aspect_slug[a->type]);
|
||||
fprintf(out, " \"orb\": %.4f,\n", a->orb);
|
||||
fprintf(out, " \"score\": %.4f,\n", item->score);
|
||||
fprintf(out, " \"narrative\": ");
|
||||
json_string(out, text);
|
||||
fprintf(out, "\n }%s\n", i + 1 < interp->top_item_count ? "," : "");
|
||||
free(text);
|
||||
}
|
||||
fprintf(out, " ],\n");
|
||||
|
||||
fprintf(out, " \"celtic_cross\": [\n");
|
||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||
const TarotDraw *draw = &reading->spread.positions[i];
|
||||
fprintf(out, " {\"position\": \"%s\", \"card\": \"%s\", \"reversed\": %s, \"card_name\": ",
|
||||
tarot_position_slug((CelticCrossPosition)i), tarot_card_slug(draw->card),
|
||||
draw->reversed ? "true" : "false");
|
||||
json_string(out, tarot_card_name(draw->card));
|
||||
fprintf(out, ", \"meaning\": ");
|
||||
json_string(out, tarot_card_meaning(draw->card, draw->reversed));
|
||||
fprintf(out, "}%s\n", i + 1 < TAROT_SPREAD_SIZE ? "," : "");
|
||||
}
|
||||
fprintf(out, " ],\n");
|
||||
|
||||
char *guidance = capture_guidance(interp, &reading->spread);
|
||||
fprintf(out, " \"guidance\": ");
|
||||
json_string(out, guidance);
|
||||
fprintf(out, "\n");
|
||||
free(guidance);
|
||||
|
||||
fprintf(out, "}\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 2 || (argc == 2 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0))) {
|
||||
print_usage(argv[0]);
|
||||
return argc > 2 ? 1 : 0;
|
||||
const char *format_str = "text";
|
||||
const char *lang = "en", *i18n_dir_arg = NULL;
|
||||
const char *input_path = NULL;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char *arg = argv[i];
|
||||
if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else if (strcmp(arg, "--format") == 0) {
|
||||
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
||||
format_str = argv[i];
|
||||
} else if (strcmp(arg, "--lang") == 0) {
|
||||
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
||||
lang = argv[i];
|
||||
} else if (strcmp(arg, "--i18n-dir") == 0) {
|
||||
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
||||
i18n_dir_arg = argv[i];
|
||||
} else if (arg[0] == '-' && strcmp(arg, "-") != 0) {
|
||||
fprintf(stderr, "Unknown argument: %s\n", arg);
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
} else if (input_path == NULL) {
|
||||
input_path = arg;
|
||||
} else {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
OutputFormat format;
|
||||
if (strcmp(format_str, "text") == 0) format = FORMAT_TEXT;
|
||||
else if (strcmp(format_str, "html") == 0) format = FORMAT_HTML;
|
||||
else if (strcmp(format_str, "json") == 0) format = FORMAT_JSON;
|
||||
else {
|
||||
fprintf(stderr, "Invalid --format, expected text, html, or json\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
char i18n_path[512];
|
||||
if (i18n_dir_arg) {
|
||||
snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_dir_arg, lang);
|
||||
} else {
|
||||
default_i18n_path(argv[0], lang, i18n_path, sizeof i18n_path);
|
||||
}
|
||||
if (!i18n_load(i18n_path) && strcmp(lang, "en") != 0) {
|
||||
fprintf(stderr, "warning: could not load translations for '%s' from %s - "
|
||||
"falling back to built-in English\n", lang, i18n_path);
|
||||
}
|
||||
|
||||
FILE *in = stdin;
|
||||
bool opened_file = false;
|
||||
if (argc == 2 && strcmp(argv[1], "-") != 0) {
|
||||
in = fopen(argv[1], "r");
|
||||
if (input_path && strcmp(input_path, "-") != 0) {
|
||||
in = fopen(input_path, "r");
|
||||
if (!in) {
|
||||
fprintf(stderr, "error: could not open %s\n", argv[1]);
|
||||
fprintf(stderr, "error: could not open %s\n", input_path);
|
||||
return 1;
|
||||
}
|
||||
opened_file = true;
|
||||
@@ -116,30 +482,12 @@ int main(int argc, char **argv) {
|
||||
DailyInterpretation interp;
|
||||
interpret_daily_reading(&reading, &interp);
|
||||
|
||||
printf("Day significance: %s (%d/%d)\n", day_level_name(interp.day_level),
|
||||
interp.day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||||
if (interpretation_deserves_framing(&interp)) {
|
||||
printf("(a major transit today - worth a deeper Celtic Cross look)\n");
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
if (interp.top_item_count == 0) {
|
||||
printf("No notable transits today.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Top %d significant transit%s:\n", interp.top_item_count,
|
||||
interp.top_item_count == 1 ? "" : "s");
|
||||
for (int i = 0; i < interp.top_item_count; i++) {
|
||||
const SignificantItem *item = &interp.top_items[i];
|
||||
const Aspect *a = &item->aspect;
|
||||
|
||||
printf(" %d. Transiting %s", i + 1, body_display_name(a->transiting_planet));
|
||||
print_body_in_sign(&reading.transits.bodies[a->transiting_planet]);
|
||||
printf(" %s natal %s", aspect_display_name(a->type), body_display_name(a->natal_planet));
|
||||
print_body_in_sign(&reading.natal.bodies[a->natal_planet]);
|
||||
printf(" (orb %.1f\xc2\xb0, score %.2f)\n", a->orb, item->score);
|
||||
narrative_print(stdout, " ", a, reading.transits.bodies[a->transiting_planet].house);
|
||||
if (format == FORMAT_HTML) {
|
||||
interp_print_html(stdout, &reading, &interp);
|
||||
} else if (format == FORMAT_JSON) {
|
||||
interp_print_json(stdout, &reading, &interp);
|
||||
} else {
|
||||
interp_print_text(stdout, &reading, &interp);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+90
-59
@@ -1,86 +1,103 @@
|
||||
#include "narrative.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* i18n_get() only - narrative.h already pulls in astro.h; this is the
|
||||
* one additional engine header needed to route k_narratives/
|
||||
* k_house_area through the same translation catalog tarot_data.c uses
|
||||
* (see the Makefile's INTERP_TAROT_TEXT_OBJS comment for why i18n.c is
|
||||
* linked into this binary). */
|
||||
#include "../../engine/src/i18n.h"
|
||||
|
||||
typedef struct {
|
||||
const char *slug; /* builds "narrative.<slug>.*" i18n keys */
|
||||
const char *base; /* always relevant, may be "" (see the Sun) */
|
||||
const char *harmonious; /* shown only under a trine/sextile; may be "" */
|
||||
const char *discordant; /* shown only under a square/opposition; may be "" */
|
||||
} TransitNarrative;
|
||||
|
||||
/* See narrative.h's doc comment for sourcing (Sepharial, "Transits and
|
||||
* Planetary Periods", 1920, Chapter VIII) and the Moon/Pluto exception. */
|
||||
/* Fallback (English) text, also the source of truth when no translation
|
||||
* catalog is loaded - see narrative.h's doc comment for sourcing
|
||||
* (Sepharial, "Transits and Planetary Periods", 1920, Chapter VIII) and
|
||||
* the Moon/Pluto exception. German translations live in
|
||||
* engine/i18n/de.lang under the same "narrative.<slug>.*" keys. */
|
||||
static const TransitNarrative k_narratives[NUM_BODIES] = {
|
||||
[PLANET_SUN] = {
|
||||
.base = "",
|
||||
.harmonious = "Benefits from superiors and advancement in your sphere of "
|
||||
"life and work - honours, emoluments, and successful new "
|
||||
"associations.",
|
||||
.discordant = "Degradation and dishonour, loss of position, and adverse "
|
||||
"judgement from superiors.",
|
||||
"sun", "",
|
||||
"Benefits from superiors and advancement in your sphere of "
|
||||
"life and work - honours, emoluments, and successful new "
|
||||
"associations.",
|
||||
"Degradation and dishonour, loss of position, and adverse "
|
||||
"judgement from superiors.",
|
||||
},
|
||||
[PLANET_MOON] = {
|
||||
/* Original text, not from Sepharial - see narrative.h. */
|
||||
.base = "Colours the everyday and domestic sphere of life, often "
|
||||
"coinciding with the opening of new avenues.",
|
||||
.harmonious = "These changes tend to be advantageous.",
|
||||
.discordant = "These changes tend to be adverse, with some indisposition "
|
||||
"or domestic friction.",
|
||||
"moon",
|
||||
"Colours the everyday and domestic sphere of life, often "
|
||||
"coinciding with the opening of new avenues.",
|
||||
"These changes tend to be advantageous.",
|
||||
"These changes tend to be adverse, with some indisposition "
|
||||
"or domestic friction.",
|
||||
},
|
||||
[PLANET_MERCURY] = {
|
||||
.base = "Affects writings, journeys, commerce, and everyday activities - "
|
||||
"a neutral messenger whose effect follows the nature of the "
|
||||
"aspect it makes.",
|
||||
.harmonious = "",
|
||||
.discordant = "",
|
||||
"mercury",
|
||||
"Affects writings, journeys, commerce, and everyday activities - "
|
||||
"a neutral messenger whose effect follows the nature of the "
|
||||
"aspect it makes.",
|
||||
"", "",
|
||||
},
|
||||
[PLANET_VENUS] = {
|
||||
.base = "Brings domestic and social affairs to the fore - happiness, "
|
||||
"comforts, and favours.",
|
||||
.harmonious = "Success in love affairs and artistic pursuits is likely.",
|
||||
.discordant = "Grief and disappointment are more likely.",
|
||||
"venus",
|
||||
"Brings domestic and social affairs to the fore - happiness, "
|
||||
"comforts, and favours.",
|
||||
"Success in love affairs and artistic pursuits is likely.",
|
||||
"Grief and disappointment are more likely.",
|
||||
},
|
||||
[PLANET_MARS] = {
|
||||
.base = "A strenuous time of quarrels, contention, strife and anger, with "
|
||||
"some risk of hurts or injuries depending on the sign it "
|
||||
"occupies.",
|
||||
.harmonious = "Can bring benefits from doctors, surgeons, or new projects "
|
||||
"and enterprises.",
|
||||
.discordant = "",
|
||||
"mars",
|
||||
"A strenuous time of quarrels, contention, strife and anger, with "
|
||||
"some risk of hurts or injuries depending on the sign it "
|
||||
"occupies.",
|
||||
"Can bring benefits from doctors, surgeons, or new projects "
|
||||
"and enterprises.",
|
||||
"",
|
||||
},
|
||||
[PLANET_JUPITER] = {
|
||||
.base = "Brings increase and expansion - fullness of fortune and health, "
|
||||
"and a generally fortunate time.",
|
||||
.harmonious = "",
|
||||
.discordant = "",
|
||||
"jupiter",
|
||||
"Brings increase and expansion - fullness of fortune and health, "
|
||||
"and a generally fortunate time.",
|
||||
"", "",
|
||||
},
|
||||
[PLANET_SATURN] = {
|
||||
.base = "Brings depression, stagnation, hindrances and obstacles, and "
|
||||
"some deprivation of the usual benefits.",
|
||||
.harmonious = "Favours from older connections and past associations are "
|
||||
"still possible.",
|
||||
.discordant = "",
|
||||
"saturn",
|
||||
"Brings depression, stagnation, hindrances and obstacles, and "
|
||||
"some deprivation of the usual benefits.",
|
||||
"Favours from older connections and past associations are "
|
||||
"still possible.",
|
||||
"",
|
||||
},
|
||||
[PLANET_URANUS] = {
|
||||
.base = "Brings separations, estrangements, sudden dislocations and "
|
||||
"violent upsets.",
|
||||
.harmonious = "Success through official or civic channels, and "
|
||||
"beneficial changes or appointments, are possible.",
|
||||
.discordant = "",
|
||||
"uranus",
|
||||
"Brings separations, estrangements, sudden dislocations and "
|
||||
"violent upsets.",
|
||||
"Success through official or civic channels, and "
|
||||
"beneficial changes or appointments, are possible.",
|
||||
"",
|
||||
},
|
||||
[PLANET_NEPTUNE] = {
|
||||
.base = "Brings a state of chaos and confusion - an involved, uncertain "
|
||||
"condition of affairs, with plots, subtlety, or unseen "
|
||||
"influences at work.",
|
||||
.harmonious = "",
|
||||
.discordant = "",
|
||||
"neptune",
|
||||
"Brings a state of chaos and confusion - an involved, uncertain "
|
||||
"condition of affairs, with plots, subtlety, or unseen "
|
||||
"influences at work.",
|
||||
"", "",
|
||||
},
|
||||
[PLANET_PLUTO] = {
|
||||
/* Original text, not from Sepharial - see narrative.h. */
|
||||
.base = "Brings deep, often hidden transformation - the surfacing or "
|
||||
"dismantling of something that has outgrown its old form.",
|
||||
.harmonious = "",
|
||||
.discordant = "",
|
||||
"pluto",
|
||||
"Brings deep, often hidden transformation - the surfacing or "
|
||||
"dismantling of something that has outgrown its old form.",
|
||||
"", "",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -96,7 +113,8 @@ static bool aspect_is_discordant(AspectType type) {
|
||||
* house governs. This is centuries-old, uncredited astrological
|
||||
* convention (the same status as the sign/aspect names already baked
|
||||
* into engine/src/astro.c), not text drawn from Sepharial or any other
|
||||
* single source. Indexed house - 1. */
|
||||
* single source. Indexed house - 1; fallback (English) text only, keyed
|
||||
* as "narrative.house.<1-12>" in engine/i18n's .lang files. */
|
||||
static const char *const k_house_area[12] = {
|
||||
"your sense of self, identity, and outward appearance",
|
||||
"money, possessions, and personal values",
|
||||
@@ -112,22 +130,35 @@ static const char *const k_house_area[12] = {
|
||||
"solitude, the subconscious, and hidden matters",
|
||||
};
|
||||
|
||||
static const char *narrative_field(const char *slug, const char *field, const char *fallback) {
|
||||
char key[48];
|
||||
snprintf(key, sizeof key, "narrative.%s.%s", slug, field);
|
||||
return i18n_get(key, fallback);
|
||||
}
|
||||
|
||||
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
||||
int transiting_house) {
|
||||
const TransitNarrative *n = &k_narratives[aspect->transiting_planet];
|
||||
const char *base = narrative_field(n->slug, "base", n->base);
|
||||
|
||||
const char *extra = "";
|
||||
if (aspect_is_harmonious(aspect->type)) extra = n->harmonious;
|
||||
else if (aspect_is_discordant(aspect->type)) extra = n->discordant;
|
||||
if (aspect_is_harmonious(aspect->type)) extra = narrative_field(n->slug, "harmonious", n->harmonious);
|
||||
else if (aspect_is_discordant(aspect->type)) extra = narrative_field(n->slug, "discordant", n->discordant);
|
||||
|
||||
if (n->base[0] == '\0' && extra[0] == '\0') return;
|
||||
if (base[0] == '\0' && extra[0] == '\0') return;
|
||||
|
||||
fprintf(out, "%s", indent);
|
||||
if (transiting_house >= 1 && transiting_house <= 12) {
|
||||
fprintf(out, "In matters of %s (house %d). ",
|
||||
k_house_area[transiting_house - 1], transiting_house);
|
||||
char house_key[24];
|
||||
snprintf(house_key, sizeof house_key, "narrative.house.%d", transiting_house);
|
||||
const char *area = i18n_get(house_key, k_house_area[transiting_house - 1]);
|
||||
|
||||
char frame[256];
|
||||
snprintf(frame, sizeof frame, i18n_get("narrative.house_frame", "In matters of %s (house %d)."),
|
||||
area, transiting_house);
|
||||
fprintf(out, "%s ", frame);
|
||||
}
|
||||
if (n->base[0] != '\0') fprintf(out, "%s", n->base);
|
||||
if (extra[0] != '\0') fprintf(out, "%s%s", n->base[0] != '\0' ? " " : "", extra);
|
||||
if (base[0] != '\0') fprintf(out, "%s", base);
|
||||
if (extra[0] != '\0') fprintf(out, "%s%s", base[0] != '\0' ? " " : "", extra);
|
||||
fprintf(out, "\n");
|
||||
}
|
||||
|
||||
@@ -39,7 +39,13 @@
|
||||
* personalized way transits are read (see astro.h's own house-field
|
||||
* comment). Pass a value outside 1-12 (e.g. the "no data" sentinel `0`
|
||||
* used elsewhere - see reading_io.c's parse_bodies()) to omit that
|
||||
* framing and print the planet's narrative on its own. */
|
||||
* framing and print the planet's narrative on its own.
|
||||
*
|
||||
* Every string here is looked up via i18n_get() under "narrative.*"
|
||||
* keys (engine/i18n's .lang files) before falling back to the English text
|
||||
* baked into narrative.c, the same catalog tarot_data.c uses - so this
|
||||
* text respects whatever --lang the caller loaded with i18n_load()
|
||||
* (main.c does so before calling this). */
|
||||
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
||||
int transiting_house);
|
||||
|
||||
|
||||
@@ -22,6 +22,20 @@ static const char *const k_sign_slug[12] = {
|
||||
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
||||
};
|
||||
|
||||
/* Mirrors tarot_data.c's own k_card_slug/k_position_slug, same
|
||||
* duplication policy as k_body_slug above. */
|
||||
static const char *const k_card_slug[TAROT_DECK_SIZE] = {
|
||||
"fool", "magician", "high_priestess", "empress", "emperor", "hierophant",
|
||||
"lovers", "chariot", "strength", "hermit", "wheel_of_fortune", "justice",
|
||||
"hanged_man", "death", "temperance", "devil", "tower", "star", "moon",
|
||||
"sun", "judgement", "world",
|
||||
};
|
||||
|
||||
static const char *const k_position_slug[TAROT_SPREAD_SIZE] = {
|
||||
"present", "challenge", "crown", "foundation", "recent_past",
|
||||
"near_future", "attitude", "environment", "hopes_and_fears", "outcome",
|
||||
};
|
||||
|
||||
static bool body_from_slug(const char *slug, Body *out) {
|
||||
for (int i = 0; i < NUM_BODIES; i++) {
|
||||
if (strcmp(slug, k_body_slug[i]) == 0) {
|
||||
@@ -77,6 +91,48 @@ static bool aspect_type_from_slug(const char *slug, AspectType *out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool card_from_slug(const char *slug, TarotCard *out) {
|
||||
for (int i = 0; i < TAROT_DECK_SIZE; i++) {
|
||||
if (strcmp(slug, k_card_slug[i]) == 0) {
|
||||
*out = (TarotCard)i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool position_from_slug(const char *slug, CelticCrossPosition *out) {
|
||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||
if (strcmp(slug, k_position_slug[i]) == 0) {
|
||||
*out = (CelticCrossPosition)i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Fills out->positions[] from a "spread"."positions" JSON array. Used only
|
||||
* for guidance_print()'s Attitude/Outcome framing, never for scoring, so
|
||||
* this is deliberately lenient like parse_bodies(): an entry with an
|
||||
* unrecognized/missing "position" or "card" slug is skipped, and a
|
||||
* missing "spread" key entirely just leaves every position zeroed
|
||||
* (card = the first enum value, reversed = false) rather than failing
|
||||
* the whole load. */
|
||||
static void parse_spread(const JsonValue *positions, CelticCrossSpread *out) {
|
||||
int count = json_array_count(positions);
|
||||
for (int i = 0; i < count; i++) {
|
||||
const JsonValue *item = json_array_get(positions, i);
|
||||
CelticCrossPosition position;
|
||||
TarotCard card;
|
||||
if (!position_from_slug(json_as_string(json_object_get(item, "position")), &position)) continue;
|
||||
if (!card_from_slug(json_as_string(json_object_get(item, "card")), &card)) continue;
|
||||
|
||||
out->positions[position].card = card;
|
||||
const JsonValue *reversed = json_object_get(item, "reversed");
|
||||
out->positions[position].reversed = reversed && reversed->type == JSON_BOOL && reversed->as.boolean;
|
||||
}
|
||||
}
|
||||
|
||||
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
@@ -122,6 +178,9 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
||||
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
|
||||
|
||||
const JsonValue *spread = json_object_get(root, "spread");
|
||||
parse_spread(spread ? json_object_get(spread, "positions") : NULL, &out->spread);
|
||||
|
||||
json_free(root);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -10,18 +10,21 @@
|
||||
|
||||
/* Parses deck-engine's `--format json` output (text, null-terminated at
|
||||
* text[length]) and fills *out with just enough of a DailyReading for
|
||||
* interpret_daily_reading() to work on and for reporting sign/house
|
||||
* context around each significant event: out->transits.aspects[]/
|
||||
* aspect_count (used for scoring - see significance.c), and
|
||||
* out->natal.bodies[]/out->transits.bodies[] (sign + house per body,
|
||||
* used only for display). out->natal.ascendant_longitude/houses[] and
|
||||
* out->spread are left zeroed - nothing reads them yet.
|
||||
* interpret_daily_reading() to work on, for reporting sign/house context
|
||||
* around each significant event, and for guidance_print()'s tarot
|
||||
* framing: out->transits.aspects[]/aspect_count (used for scoring - see
|
||||
* significance.c), out->natal.bodies[]/out->transits.bodies[] (sign +
|
||||
* house per body, used only for display), and out->spread.positions[]
|
||||
* (card + reversed per Celtic Cross position, used only by
|
||||
* guidance.c). out->natal.ascendant_longitude/houses[] are left zeroed -
|
||||
* nothing reads them yet.
|
||||
*
|
||||
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
||||
* present as an array (an empty array is fine - that's a real "no
|
||||
* aspects today" reading, not an error). A missing/unrecognized entry
|
||||
* in "natal"."bodies"/"transits"."bodies" is not an error - see
|
||||
* parse_bodies() in reading_io.c. */
|
||||
* aspects today" reading, not an error). A missing/unrecognized entry in
|
||||
* "natal"."bodies"/"transits"."bodies"/"spread"."positions", or a
|
||||
* missing "spread" key entirely, is not an error - see parse_bodies()/
|
||||
* parse_spread() in reading_io.c. */
|
||||
bool reading_load_json(const char *text, size_t length, DailyReading *out);
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user