Skip to content

Commit

Permalink
Show steinsaltz images
Browse files Browse the repository at this point in the history
  • Loading branch information
ronshapiro committed Nov 11, 2024
1 parent 6e9cd88 commit bfddbe3
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 108 deletions.
32 changes: 19 additions & 13 deletions api_request_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ import {SefariaLinkSanitizer} from "./source_formatting/sefaria_link_sanitizer";
import {SefariaTopicCollector} from "./source_formatting/sefaria_topic_collector";
import {ShulchanArukhHeaderRemover} from "./source_formatting/shulchan_arukh_remove_header";
import {isPehSectionEnding, transformTanakhSpacing} from "./source_formatting/tanakh_spacing";
import {makeSteinsaltzCommentPairings} from "./steinsaltz";
import {
makeSteinsaltzCommentPairings,
getTextWithImages,
filterDuplicateImages,
} from "./steinsaltz";
import {formatDafInHebrew} from "./talmud";
import {hasMatchingProperty} from "./util/objects";
import {checkNotUndefined} from "./js/undefined";
Expand Down Expand Up @@ -1393,22 +1397,24 @@ class TalmudApiRequestHandler extends AbstractApiRequestHandler {
for (const [hebrew, english] of makeSteinsaltzCommentPairings(
steinsaltz.notesHeb, steinsaltz.notesEng)) {
i += 1;
const titles = {
english: english ? english.titleEng : "",
// the hebrew note only supplies the "title" field, and it's not vocalized.
hebrew: english ? english.titleHeb : hebrew!.title,
};
if (!hebrew) {
titles.english += ` (${titles.hebrew})`;
titles.hebrew = "";
}
filterDuplicateImages(hebrew, english);
segment.commentary.addComment(new Comment(
"Steinsaltz In-Depth",
hebrew ? hebrew.text : "",
english ? english.text : "",
getTextWithImages(hebrew, this.logger),
getTextWithImages(english, this.logger),
`Steinsaltz comment #${i} on ` + segment.ref,
english ? english.titleEng : "",
// the hebrew note only supplies the "title" field, and it's not vocalized.
english ? english.titleHeb : hebrew!.title,
titles.english,
titles.hebrew,
));
if (hebrew && hebrew.files.length > 0) {
this.logger.error(segment.ref, "heb", hebrew.files);
}
if (english && english.files.length > 0) {
this.logger.error(segment.ref, "eng", english.files);
}
// TODO: figure out how files are stored
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -695,3 +695,18 @@ h2, .title, .titleHebrew {
.keybindingSelectedButton {
background: #0f06;
}

.steinsaltz-comments img {
width: 300px;
padding-bottom: 10px;
}

.steinsaltz-comments .hebrew img {
float: left;
padding-right: 10px;
}

.steinsaltz-comments .english img {
float: right;
padding-left: 10px;
}
10 changes: 10 additions & 0 deletions express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ app.use((req, res, next) => {
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/stimg/:id/:filename", async (req, res) => {
const {id, filename} = req.params;
new RealRequestMaker()
.makeSteinsaltzImageRequest(id, filename)
.then(blob => {
res.setHeader("Content-Type", blob.type);
blob.stream().pipe(res);
});
});

app.get("/", (req, res) => res.render("homepage.html"));
app.get("/css/:ignored/:path", (req, res) => sendLazyStaticFile(res, `css/${req.params.path}`));

Expand Down
24 changes: 21 additions & 3 deletions js/CommentariesBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ function commentaryHighlightIndicators(commentary: Commentary): React.ReactEleme
return result;
}

function textTypeHasImage(text: sefaria.TextType): boolean {
if (!text) return false;
if (typeof text === "string") return text.includes("<img");
return text.some(x => textTypeHasImage(x));
}

function hasImage(commentary: Commentary): boolean {
for (const comment of commentary.comments) {
if (textTypeHasImage(comment.he) || textTypeHasImage(comment.en)) {
return true;
}
if (Object.values(comment.commentary || {}).some(hasImage)) {
return true;
}
}
return false;
}

function InternalTableRow(
{hebrew, extraClasses, id}: {
hebrew: React.ReactElement;
Expand Down Expand Up @@ -247,12 +265,12 @@ export function CommentariesBlock({
onKeyUp={onKeyUp}>
{commentaryKind.hebrewName}
</a>);
const highlightColors = !isShowing && commentaryHighlightIndicators(commentary);
const imageIndicator = !isShowing && hasImage(commentary) && "📸 🖼️";
return (
// Wrap in a span so that the commentary colors don't get their own flex spacing separate from
// the button.
<span key={commentaryKind.englishName}>
{button} {!isShowing && commentaryHighlightIndicators(commentary)}
</span>
<span key={commentaryKind.englishName}>{button} {highlightColors} {imageIndicator}</span>
);
};

Expand Down
6 changes: 5 additions & 1 deletion request_makers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {parse as urlParse} from "url";
import {fetch} from "./fetch";
import {readUtf8Async} from "./files";
import {steinsaltzApiUrl} from "./steinsaltz";
import {steinsaltzApiUrl, steinsaltzImageUrl} from "./steinsaltz";
import {writeJson} from "./util/json_files";

export abstract class RequestMaker {
Expand Down Expand Up @@ -35,6 +35,10 @@ export class RealRequestMaker extends RequestMaker {
.then(x => x.json())
.then(json => (json.error ? Promise.reject(json) : Promise.resolve(json)));
}

makeSteinsaltzImageRequest(id: string, filename: string): Promise<any> {
return fetch(steinsaltzImageUrl(id, filename), STEINSALTZ_OPTIONS).then(x => x.blob());
}
}

export const TEST_DATA_ROOT = `${__dirname}/test_data/api_request_handler`;
Expand Down
48 changes: 47 additions & 1 deletion steinsaltz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {zip} from "underscore";
import {books} from "./books";
import {ListMultimap} from "./multimap";
import {stripHebrewNonletters} from "./hebrew";
import {Logger} from "./logger";

/* eslint-disable quote-props */
const STEINSALTZ_MASECHET_NUMBER = {
Expand Down Expand Up @@ -72,10 +73,23 @@ export function steinsaltzApiUrl(masechet: string, daf: string): string {
);
}

export function steinsaltzImageUrl(id: string, filename: string): string {
return `https://api.steinsaltz.dev/v1/files/image/${id}/${filename}?preview=false`;
}

interface File {
id: number;
type: string;
filename: string;
size: number;
captionEng: string | null;
captionHeb: string | null;
}

interface Note {
text: string;
paired: boolean;
files: any[];
files: File[];
}

interface EnglishNote extends Note {
Expand All @@ -87,6 +101,38 @@ interface HebrewNote extends Note {
type: {id: number, name: string};
}

export function getTextWithImages(note: Note | undefined, logger: Logger): string {
if (!note) return "";

const text = [];
for (const file of note.files) {
if (file.type !== "image") {
logger.error(file);
continue;
}
const caption = file.captionHeb ?? file.captionEng;
text.push(
`<img src="/stimg/${file.id}/${file.filename}" alt="${caption}" /><br />`);
}
text.push(note.text);

return text.join("");
}

export function filterDuplicateImages(
hebrew: HebrewNote | undefined, english: EnglishNote | undefined): void {
if (!hebrew || !english) return;
english.files = english.files.filter(englishFile => {
for (const hebrewFile of hebrew.files) {
if (englishFile.id === hebrewFile.id
&& englishFile.filename === hebrewFile.filename) {
return false;
}
}
return true;
});
}

type HebrewEnglishPair = [HebrewNote | undefined, EnglishNote | undefined];

function splitFilter<T>(array: T[], filter: (t: T) => boolean): [T[], T[]] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@
"en": "The tablets are called: “A work of God” (Exodus 32:16), as they were written by God Himself. Since God knows all that will occur even though human beings have free choice, He did not use the term good with reference to the tablets, due to His foreknowledge that they would be shattered following the incident of the Golden Calf.",
"he": "",
"ref": "Steinsaltz comment #1 on Bava Kamma 55a:2",
"sourceHeRef": "הוֹאִיל וְסוֹפָן לְהִשְׁתַּבֵּר",
"sourceRef": "Since they were ultimately destined to be broken"
"sourceHeRef": "",
"sourceRef": "Since they were ultimately destined to be broken (הוֹאִיל וְסוֹפָן לְהִשְׁתַּבֵּר)"
}
]
},
Expand Down Expand Up @@ -945,8 +945,8 @@
"en": "These three birds are all members of the Phasianidae family, but each one belongs to a different genus of the family: The rooster is a member of the <i>Gallus </i>genus, the peacock of the <i>Pavo </i>genus, and the pheasant of the <i>Phasianus </i>genus. Despite the differences between them, their overall form and behavior shows that they are closely related, and they even mate with one another when placed together in captivity.",
"he": "",
"ref": "Steinsaltz comment #4 on Bava Kamma 55a:8",
"sourceHeRef": "תַּרְנְגוֹל טַוָּוס וּפַסְיוֹנֵי",
"sourceRef": "A cock, a peacock, and a pheasant"
"sourceHeRef": "",
"sourceRef": "A cock, a peacock, and a pheasant (תַּרְנְגוֹל טַוָּוס וּפַסְיוֹנֵי)"
}
]
},
Expand Down Expand Up @@ -1200,8 +1200,8 @@
"en": "Today, these two types of birds are treated as one species. The wild goose is classified as the <i>Anser</i> <i>anser</i>, also known as the greylag goose, and the domestic goose is classified as the <i>Anser</i> <i>anser</i> <i>domestica</i>. Although the differences between them are not always apparent, they do differ in several ways, such as in their appearance, voice, and behavior. In terms of their appearance, they differ in color, as the wild goose is usually gray and the domestic goose white, and the neck of the domestic goose is shorter than that of the wild goose. Another difference is that the male organ of the wild goose is more recognizable than that of the domestic goose. As for laying eggs, the wild goose lays fewer eggs than the domestic goose, which can lay more than ten eggs at a time.",
"he": "",
"ref": "Steinsaltz comment #4 on Bava Kamma 55a:10",
"sourceHeRef": "אַוָּוז הַבַּיִת וְאַוַּוז הַבָּר",
"sourceRef": "The domestic goose and the wild goose"
"sourceHeRef": "",
"sourceRef": "The domestic goose and the wild goose (אַוָּוז הַבַּיִת וְאַוַּוז הַבָּר)"
}
]
},
Expand Down Expand Up @@ -1504,8 +1504,8 @@
"en": "There is a tannaitic dispute as to whether the prohibition of diverse kinds applies to the descendants of Noah. The dispute hinges on the question whether the term: According to its species, as mentioned in Genesis (1:21), is equivalent to a mitzva that each species must remain separate and not mix together, or just a statement by God that they should issue forth in that manner. The<i> </i>halakhic conclusion is that the verse is not stating a mitzva. According to <i>Tosafot</i>, even if that verse is not expounded as a mitzva, it can still serve as the source to teach details of the prohibition of diverse kinds, in which the Jewish people are commanded. The Ra’avad suggests that the phrase: According to its species, indicates only that a positive commandment exists, and the verbal analogy demonstrates that a prohibition also exists.",
"he": "",
"ref": "Steinsaltz comment #4 on Bava Kamma 55a:12",
"sourceHeRef": "לְמִינֵהוּ…לְמִינֵהוּ",
"sourceRef": "According to its species…according to its species"
"sourceHeRef": "",
"sourceRef": "According to its species…according to its species (לְמִינֵהוּ…לְמִינֵהוּ)"
}
]
},
Expand Down
16 changes: 8 additions & 8 deletions test_data/api_request_handler/Berakhot.2a.expected-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@
"en": "Whenever the term <i>teruma </i>appears without qualification, it refers to <i>teruma gedola</i>. The Torah commands that “the first fruit of your oil, your wine, and your grain” be given to the priest (Numbers<i> </i>18:12). The Sages extended the scope of this commandment to include all produce. This mitzva applies only in Eretz Yisrael. After the first fruits have been set aside, a certain portion of the produce must be set aside for the priests. The Torah does not specify the amount of <i>teruma </i>that must be set aside; one may even theoretically fulfill his obligation by separating a single kernel of grain from an entire crop. The Sages established a measure: one-fortieth for a generous gift, one-fiftieth for an average gift, and one-sixtieth for a miserly gift. One may not set aside the other tithes (<i>ma’asrot</i>) until he has set aside <i>teruma</i>. <i>Teruma </i>is considered sacred and may be eaten only by a priest and his household while they are in a state of ritual purity (Leviticus<i> </i>22:9–15). To emphasize that state of ritual purity, the Sages obligated the priests to wash their hands before partaking of it. This is the source for the practice of washing one’s hands prior to a meal. A ritually impure priest or a non-priest who eats <i>teruma </i>is subject to the penalty of death at the hand of Heaven. If <i>teruma </i>contracts ritual impurity, it may no longer be eaten and must be destroyed. Nevertheless, it remains the property of the priest and he may benefit from its destruction. Nowadays, <i>teruma </i>is not given to the priests because they have no definite proof of their priestly lineage. Nevertheless, the obligation to separate <i>teruma </i>still remains, although only a small portion of the produce is separated.",
"he": "",
"ref": "Steinsaltz comment #4 on Berakhot 2a:1",
"sourceHeRef": "תְּרוּמָה",
"sourceRef": "<i>Teruma</i>"
"sourceHeRef": "",
"sourceRef": "<i>Teruma</i> (תְּרוּמָה)"
}
]
},
Expand Down Expand Up @@ -909,8 +909,8 @@
"en": "The first light of the sun before sunrise. With regard to many <i>halakhot</i>, such as the eating of sacrifices at night, the recitation of <i>Shema</i> at night, and the permissibility of eating before a fast, dawn is considered the time when night ends. The definition of the precise time of dawn is uncertain. Nowadays, it is generally accepted that, in Eretz Yisrael, dawn is between approximately one-and-a-quarter and one-and-a-half hours before sunrise.",
"he": "",
"ref": "Steinsaltz comment #2 on Berakhot 2a:3",
"sourceHeRef": "עַמּוּד הַשַּׁחַר",
"sourceRef": "Dawn"
"sourceHeRef": "",
"sourceRef": "Dawn (עַמּוּד הַשַּׁחַר)"
}
]
},
Expand Down Expand Up @@ -2025,8 +2025,8 @@
"en": "When reciting <i>Shema</i>, one recites blessings beforehand and thereafter. During the day one recites two blessings beforehand: Who forms light and A great love/An everlasting love, and one thereafter: Who redeemed Israel; and at night one recites two blessings beforehand: Who brings on evenings and An everlasting love, and two thereafter: Who redeemed Israel and Help us lie down. One who recites <i>Shema </i>without reciting its blessings fulfills his obligation, but is required to recite the blessings without again reciting <i>Shema</i>. The <i>Shulĥan Arukh </i>writes in that case: It seems to me that it is preferable to recite <i>Shema </i>with its blessings (Rambam<i> Sefer Ahava</i>,<i> Hilkhot Keriat Shema </i>1:5–6; <i>Shulĥan Arukh</i>,<i> Oraĥ Ĥayyim</i> 60:1–2, 236:1).",
"he": "",
"ref": "Steinsaltz comment #2 on Berakhot 2a:10",
"sourceHeRef": "בַּשַּׁחַר מְבָרֵךְ שְׁתַּיִם לְפָנֶיהָ…בָּעֶרֶב…",
"sourceRef": "In the morning one recites two blessings before <i>Shema</i>…and in the evening…"
"sourceHeRef": "",
"sourceRef": "In the morning one recites two blessings before <i>Shema</i>…and in the evening… (בַּשַּׁחַר מְבָרֵךְ שְׁתַּיִם לְפָנֶיהָ…בָּעֶרֶב…)"
}
]
},
Expand Down Expand Up @@ -2475,8 +2475,8 @@
"en": "The preceding verses mention (among those prohibited to eat <i>teruma</i>) a <i>zav </i>and leper, who are required to bring a sacrifice in order to complete their purification process. These verses also offer an explanation for the leniency that allows a priest to eat <i>teruma</i> even though he is not completely purified: “For it is his bread.” Since the <i>teruma </i>is the sustenance upon which his life depends, the Torah was not strict with him (<i>Seforno</i>).",
"he": "",
"ref": "Steinsaltz comment #4 on Berakhot 2a:13",
"sourceHeRef": "וְאֵין כַּפָּרָתוֹ מְעַכַּבְתּוֹ מִלֶּאֱכוֹל בִּתְרוּמָה",
"sourceRef": "Failure to bring an atonement offering does not prevent him from eating <i>teruma </i>"
"sourceHeRef": "",
"sourceRef": "Failure to bring an atonement offering does not prevent him from eating <i>teruma </i> (וְאֵין כַּפָּרָתוֹ מְעַכַּבְתּוֹ מִלֶּאֱכוֹל בִּתְרוּמָה)"
}
]
},
Expand Down
12 changes: 6 additions & 6 deletions test_data/api_request_handler/Berakhot.34b.expected-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,8 @@
"en": "Whenever <i>keria</i> is mentioned it refers to kneeling upon one’s knees; <i>kidda</i> refers to bowing one’s head; <i>hishtaĥva’a</i> refers to spreading out one’s hands and legs (Rambam <i>Sefer Ahava</i>, <i>Hilkhot Tefilla</i> 5:13).",
"he": "",
"ref": "Steinsaltz comment #2 on Berakhot 34b:3",
"sourceHeRef": "קִידָּה…כְּרִיעָה…הִשְׁתַּחֲוָאָה",
"sourceRef": "Bowing [<i>kidda</i>]…kneeling [<i>keria</i>]…prostrating [<i>hishtaĥava’a</i>]"
"sourceHeRef": "",
"sourceRef": "Bowing [<i>kidda</i>]…kneeling [<i>keria</i>]…prostrating [<i>hishtaĥava’a</i>] (קִידָּה…כְּרִיעָה…הִשְׁתַּחֲוָאָה)"
}
]
},
Expand Down Expand Up @@ -1352,8 +1352,8 @@
"en": "A <i>tanna</i> who lived at the end of the Second Temple period, Rabbi Ĥanina ben Dosa was a disciple of Rabban Yoĥanan ben Zakkai. However, he is best known as the performer of miraculous acts, a righteous man whose prayers were always heard on high. The Talmud is replete with stories of miracles that took place for him and those close to him. He was indigent and earned a very meager living. He is the paradigm of a full-fledged righteous man who received no reward in this world. In tractate <i>Avot</i>, several of his moral/ethical teachings are cited, which underscore the primacy of the fear of Heaven and the need for mutual affection in interpersonal relationships.",
"he": "",
"ref": "Steinsaltz comment #1 on Berakhot 34b:12",
"sourceHeRef": "רַבִּי חֲנִינָא בֶּן דּוֹסָא",
"sourceRef": "Rabbi Ĥanina ben Dosa"
"sourceHeRef": "",
"sourceRef": "Rabbi Ĥanina ben Dosa (רַבִּי חֲנִינָא בֶּן דּוֹסָא)"
}
]
},
Expand Down Expand Up @@ -2504,8 +2504,8 @@
"en": "Although different statements appear with regard to this issue, the ruling is that Messianic times will not necessarily herald a change in the world order. Primarily, the Messianic era will feature the restoration of the kingdom of Israel in a state guided by Torah law (Rambam <i>Sefer HaMadda</i>, <i>Hilkhot Teshuva</i> 9:2, <i>Sefer Shofetim</i>, <i>Hilkhot Melakhim</i> 12:2).",
"he": "",
"ref": "Steinsaltz comment #1 on Berakhot 34b:20",
"sourceHeRef": "יְמוֹת הַמָּשִׁיחַ‏",
"sourceRef": "The days of the Messiah"
"sourceHeRef": "",
"sourceRef": "The days of the Messiah (יְמוֹת הַמָּשִׁיחַ‏)"
}
]
},
Expand Down
Loading

0 comments on commit bfddbe3

Please sign in to comment.