Add watch/phone icons and logo, reorder the interpretation report, and stamp every report with the day it's for
build / build (push) Successful in 30s
build / build (push) Successful in 30s
This commit is contained in:
+124
-60
@@ -126,6 +126,15 @@ static const char *sign_display_name(ZodiacSign sign) {
|
||||
return ui(key, fallback[sign]);
|
||||
}
|
||||
|
||||
/* "YYYY-MM-DD" for reading->utc_moment - the day this reading is for
|
||||
* (see reading_load_json()/parse_date() in reading_io.c), not today's
|
||||
* real date if the two ever differ (e.g. reading.json was generated
|
||||
* earlier and interpreted later). */
|
||||
static void format_date(time_t utc_moment, char *out, size_t out_size) {
|
||||
struct tm *utc = gmtime(&utc_moment);
|
||||
snprintf(out, out_size, "%04d-%02d-%02d", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
|
||||
}
|
||||
|
||||
static const char *day_level_name(DaySignificance level) {
|
||||
switch (level) {
|
||||
case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet");
|
||||
@@ -179,6 +188,32 @@ static char *capture_guidance(const DailyInterpretation *interp, const CelticCro
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Prints one significant-transit item's full detail (title line with
|
||||
* sign/house/orb/score, plus its narrative sentence) - shared between
|
||||
* the "Significant Transits" list and the day's top item, which is
|
||||
* repeated in full just above the guidance paragraph (see
|
||||
* interp_print_text) so the reader has that same detail in view right
|
||||
* next to the guidance text it informs. `index` is 1-based and prefixes
|
||||
* the line with "N. "; pass 0 to omit it, for the repeated top item,
|
||||
* which isn't part of the numbered list. */
|
||||
static void print_transit_item_text(FILE *out, const DailyReading *reading,
|
||||
const SignificantItem *item, int index) {
|
||||
const Aspect *a = &item->aspect;
|
||||
if (index > 0) fprintf(out, " %d. ", index);
|
||||
fprintf(out, "%s %s", 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);
|
||||
|
||||
char narrative_buf[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(narrative_buf, sizeof narrative_buf, " ", a,
|
||||
reading->transits.bodies[a->transiting_planet].house);
|
||||
fputs(narrative_buf, out);
|
||||
}
|
||||
|
||||
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:"));
|
||||
@@ -203,6 +238,10 @@ static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread)
|
||||
}
|
||||
|
||||
static void interp_print_text(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||
char date_buf[32];
|
||||
format_date(reading->utc_moment, date_buf, sizeof date_buf);
|
||||
fprintf(out, "%s %s\n\n", ui("interp.reading_date", "Reading for:"), date_buf);
|
||||
|
||||
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)) {
|
||||
@@ -211,41 +250,68 @@ static void interp_print_text(FILE *out, const DailyReading *reading, const Dail
|
||||
}
|
||||
fprintf(out, "\n");
|
||||
|
||||
fprintf(out, "%s\n", ui("interp.heading_guidance", "Guidance"));
|
||||
if (interp->top_item_count > 0) {
|
||||
print_transit_item_text(out, reading, &interp->top_items[0], 0);
|
||||
}
|
||||
char guidance_buf[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(guidance_buf, sizeof guidance_buf, "", interp, &reading->spread);
|
||||
fputs(guidance_buf, out);
|
||||
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);
|
||||
|
||||
char narrative_buf[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(narrative_buf, sizeof narrative_buf, " ", a,
|
||||
reading->transits.bodies[a->transiting_planet].house);
|
||||
fputs(narrative_buf, out);
|
||||
print_transit_item_text(out, reading, &interp->top_items[i], i + 1);
|
||||
}
|
||||
}
|
||||
fprintf(out, "\n");
|
||||
|
||||
print_celtic_cross_text(out, &reading->spread);
|
||||
fprintf(out, "\n");
|
||||
|
||||
char guidance_buf[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(guidance_buf, sizeof guidance_buf, "", interp, &reading->spread);
|
||||
fputs(guidance_buf, out);
|
||||
}
|
||||
|
||||
/* ===== --format html ===== */
|
||||
|
||||
/* HTML counterpart of print_transit_item_text() - same shared-between-
|
||||
* the-repeated-top-item-and-the-full-list purpose, one table row (title
|
||||
* + orb/score) plus a narrative row if non-empty. `index` is 1-based;
|
||||
* pass 0 to omit the "N." cell, for the repeated top item. */
|
||||
static void print_transit_item_html(FILE *out, const DailyReading *reading,
|
||||
const SignificantItem *item, int index) {
|
||||
const Aspect *a = &item->aspect;
|
||||
char index_buf[16] = "";
|
||||
if (index > 0) snprintf(index_buf, sizeof index_buf, "%d.", index);
|
||||
char *text = capture_narrative(a, reading->transits.bodies[a->transiting_planet].house);
|
||||
|
||||
fprintf(out, "<tr><td>%s</td><td>%s %s %s %s %s</td>"
|
||||
"<td>%s %.1f°, %s %.2f</td></tr>\n",
|
||||
index_buf, 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);
|
||||
}
|
||||
|
||||
/* One Celtic Cross card as a ".card" div - shared between the
|
||||
* Attitude/Outcome preview (right after guidance) and the full spread
|
||||
* further down, so both render identically. */
|
||||
static void print_card_html(FILE *out, CelticCrossPosition position, const TarotDraw *draw) {
|
||||
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(position), 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(position),
|
||||
tarot_card_meaning(draw->card, draw->reversed));
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -267,6 +333,11 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
|
||||
|
||||
fprintf(out, "<h1>%s</h1>\n", ui("interp.heading", "Daily Interpretation"));
|
||||
|
||||
char date_buf[32];
|
||||
format_date(reading->utc_moment, date_buf, sizeof date_buf);
|
||||
fprintf(out, "<p class=\"reading-date\">%s %s</p>\n",
|
||||
ui("interp.reading_date", "Reading for:"), date_buf);
|
||||
|
||||
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),
|
||||
@@ -277,48 +348,20 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
|
||||
}
|
||||
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);
|
||||
char *guidance = capture_guidance(interp, &reading->spread);
|
||||
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_guidance", "Guidance"));
|
||||
|
||||
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 class=\"spread attitude-outcome\">\n");
|
||||
print_card_html(out, POSITION_ATTITUDE, &reading->spread.positions[POSITION_ATTITUDE]);
|
||||
print_card_html(out, POSITION_OUTCOME, &reading->spread.positions[POSITION_OUTCOME]);
|
||||
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"));
|
||||
fprintf(out, "<div class=\"guidance\">\n");
|
||||
if (interp->top_item_count > 0) {
|
||||
fprintf(out, "<table>\n");
|
||||
print_transit_item_html(out, reading, &interp->top_items[0], 0);
|
||||
fprintf(out, "</table>\n");
|
||||
}
|
||||
const char *start = guidance;
|
||||
for (const char *p = guidance; ; p++) {
|
||||
if (*p == '\n' || *p == '\0') {
|
||||
@@ -330,6 +373,23 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
|
||||
free(guidance);
|
||||
fprintf(out, "</div>\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++) {
|
||||
print_transit_item_html(out, reading, &interp->top_items[i], i + 1);
|
||||
}
|
||||
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++) {
|
||||
print_card_html(out, (CelticCrossPosition)i, &reading->spread.positions[i]);
|
||||
}
|
||||
fprintf(out, "</div>\n");
|
||||
|
||||
fprintf(out, "</body></html>\n");
|
||||
}
|
||||
|
||||
@@ -356,6 +416,10 @@ static void json_string(FILE *out, const char *s) {
|
||||
static void interp_print_json(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||
fprintf(out, "{\n");
|
||||
|
||||
char date_buf[32];
|
||||
format_date(reading->utc_moment, date_buf, sizeof date_buf);
|
||||
fprintf(out, " \"date\": \"%s\",\n", date_buf);
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#define _DEFAULT_SOURCE /* for timegm, parsing "date" back into utc_moment */
|
||||
|
||||
#include "reading_io.h"
|
||||
#include "json.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n lookup-key
|
||||
@@ -133,6 +136,25 @@ static void parse_spread(const JsonValue *positions, CelticCrossSpread *out) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Parses the top-level "date" field ("YYYY-MM-DD", see reading_print_json
|
||||
* in engine/src/reading.c) back into out->utc_moment, as midnight UTC of
|
||||
* that date - reading_print_json only ever wrote the calendar date, not a
|
||||
* time of day, so that's the only value that round-trips through
|
||||
* gmtime() to the same date. Lenient like parse_bodies()/parse_spread():
|
||||
* a missing or malformed "date" just leaves out->utc_moment at 0 (from
|
||||
* reading_load_json's own memset), not a load failure - nothing here
|
||||
* depends on it for scoring, only for display. */
|
||||
static void parse_date(const JsonValue *date, DailyReading *out) {
|
||||
const char *date_str = date ? json_as_string(date) : NULL;
|
||||
if (!date_str) return;
|
||||
|
||||
struct tm tm_date = {0};
|
||||
if (sscanf(date_str, "%d-%d-%d", &tm_date.tm_year, &tm_date.tm_mon, &tm_date.tm_mday) != 3) return;
|
||||
tm_date.tm_year -= 1900;
|
||||
tm_date.tm_mon -= 1;
|
||||
out->utc_moment = timegm(&tm_date);
|
||||
}
|
||||
|
||||
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
@@ -174,6 +196,8 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||
}
|
||||
out->transits.aspect_count = filled;
|
||||
|
||||
parse_date(json_object_get(root, "date"), out);
|
||||
|
||||
const JsonValue *natal = json_object_get(root, "natal");
|
||||
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
||||
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
* 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[]
|
||||
* house per body, used only for display), 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.
|
||||
* guidance.c), and out->utc_moment (the top-level "date" field, parsed
|
||||
* back to midnight UTC of that date - used only for display, e.g. main.c
|
||||
* printing which day a reading is for). 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "../src/json.h"
|
||||
#include "../src/reading_io.h"
|
||||
@@ -48,6 +49,7 @@ static void test_json_parse_rejects_malformed(void) {
|
||||
* absent entirely, to prove partial spread data doesn't fail the load. */
|
||||
static const char *k_fixture =
|
||||
"{"
|
||||
" \"date\": \"2026-07-16\","
|
||||
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
||||
" \"transits\": {"
|
||||
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \"house\": 5}],"
|
||||
@@ -80,6 +82,30 @@ static void test_reading_load_json_extracts_aspects(void) {
|
||||
printf("PASS test_reading_load_json_extracts_aspects\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_extracts_date(void) {
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||
|
||||
assert(ok);
|
||||
struct tm *utc = gmtime(&reading.utc_moment);
|
||||
assert(utc->tm_year + 1900 == 2026);
|
||||
assert(utc->tm_mon + 1 == 7);
|
||||
assert(utc->tm_mday == 16);
|
||||
|
||||
printf("PASS test_reading_load_json_extracts_date\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_missing_date_defaults_to_zero(void) {
|
||||
const char *text = "{\"transits\": {\"aspects\": []}}";
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||
|
||||
assert(ok);
|
||||
assert(reading.utc_moment == 0);
|
||||
|
||||
printf("PASS test_reading_load_json_missing_date_defaults_to_zero\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_extracts_body_sign_and_house(void) {
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||
@@ -178,6 +204,8 @@ int main(void) {
|
||||
test_json_parse_basic_shapes();
|
||||
test_json_parse_rejects_malformed();
|
||||
test_reading_load_json_extracts_aspects();
|
||||
test_reading_load_json_extracts_date();
|
||||
test_reading_load_json_missing_date_defaults_to_zero();
|
||||
test_reading_load_json_extracts_body_sign_and_house();
|
||||
test_reading_load_json_extracts_spread_positions();
|
||||
test_reading_load_json_missing_spread_is_not_fatal();
|
||||
|
||||
Reference in New Issue
Block a user