diff --git a/build/ci/integration-values.yaml b/build/ci/integration-values.yaml index 007a28c360..1127d6e5cc 100644 --- a/build/ci/integration-values.yaml +++ b/build/ci/integration-values.yaml @@ -57,7 +57,6 @@ localSettings: DEBUG: true DOMAIN_LANGUAGE: {} APSCHEDULER_NAME: "apscheduler-{{ .Values.deployEnv }}" - TURN_SERVER: '' USE_CLOUDFLARE: false FRONT_END_URL: "http://${NAME}.integration.sefaria.org" OFFLINE: false diff --git a/build/ci/production-values.yaml b/build/ci/production-values.yaml index 138910a0f7..aaf6ed61c7 100644 --- a/build/ci/production-values.yaml +++ b/build/ci/production-values.yaml @@ -235,7 +235,6 @@ localSettings: } MONGO_HOST: "mongo" APSCHEDULER_NAME: "apscheduler-{{ .Values.deployEnv }}" - TURN_SERVER: '' USE_CLOUDFLARE: false FRONT_END_URL: "http://www.sefaria.org" OFFLINE: false diff --git a/build/ci/sandbox-values.yaml b/build/ci/sandbox-values.yaml index f6984b77f0..21dc2b826b 100644 --- a/build/ci/sandbox-values.yaml +++ b/build/ci/sandbox-values.yaml @@ -53,7 +53,6 @@ localSettings: DEBUG: false DOMAIN_LANGUAGE: {} APSCHEDULER_NAME: "apscheduler-{{ .Values.deployEnv }}" - TURN_SERVER: '' USE_CLOUDFLARE: false FRONT_END_URL: "http://${NAME}.cauldron.sefaria.org" OFFLINE: false diff --git a/build/notify/notifyEnd.js b/build/notify/notifyEnd.js index 88f4d8d6f3..1edc5d3a29 100644 --- a/build/notify/notifyEnd.js +++ b/build/notify/notifyEnd.js @@ -21,7 +21,7 @@ console.log(` const jobKeys = [ "Jest", - "PyTest", + "Continuous Testing: PyTest", "Playwright", ]; diff --git a/helm-chart/sefaria-project/templates/configmap/local-settings-file.yaml b/helm-chart/sefaria-project/templates/configmap/local-settings-file.yaml index d59daf235f..ee9d51ff01 100644 --- a/helm-chart/sefaria-project/templates/configmap/local-settings-file.yaml +++ b/helm-chart/sefaria-project/templates/configmap/local-settings-file.yaml @@ -144,10 +144,6 @@ data: SEARCH_INDEX_NAME_TEXT = 'text' # name of the ElasticSearch index to use SEARCH_INDEX_NAME_SHEET = 'sheet' - TURN_SERVER = os.getenv("TURN_SERVER") #coturn.cauldron.sefaria.org - TURN_SECRET= os.getenv("TURN_SECRET") - TURN_USER = os.getenv("TURN_USER") - USE_NODE = True NODE_HOST = "http://{}:3000".format(nodejsHost) NODE_TIMEOUT = 5 diff --git a/helm-chart/sefaria-project/templates/configmap/local-settings.yaml b/helm-chart/sefaria-project/templates/configmap/local-settings.yaml index efab2f1294..7c3cf997b0 100644 --- a/helm-chart/sefaria-project/templates/configmap/local-settings.yaml +++ b/helm-chart/sefaria-project/templates/configmap/local-settings.yaml @@ -9,7 +9,6 @@ data: DEBUG: "{{ .Values.localSettings.DEBUG }}" DOMAIN_LANGUAGE: {{ .Values.localSettings.DOMAIN_LANGUAGE | toJson | quote }} APSCHEDULER_NAME: {{ tpl .Values.localSettings.APSCHEDULER_NAME . | quote }} - TURN_SERVER: {{ .Values.localSettings.TURN_SERVER | quote }} USE_CLOUDFLARE: "{{ .Values.localSettings.USE_CLOUDFLARE }}" FRONT_END_URL: {{ .Values.localSettings.FRONT_END_URL | quote }} OFFLINE: "{{ .Values.localSettings.OFFLINE }}" diff --git a/helm-chart/sefaria-project/values.yaml b/helm-chart/sefaria-project/values.yaml index 11fd51195f..040e37a7ee 100644 --- a/helm-chart/sefaria-project/values.yaml +++ b/helm-chart/sefaria-project/values.yaml @@ -357,8 +357,6 @@ secrets: # SEFARIA_DB_USER: # SEFARIA_DB_PASSWORD: # SEARCH_URL - # TURN_SECRET: - # TURN_USER: # SEFARIA_BOT_API_KEY: # CLOUDFLARE_ZONE: # CLOUDFLARE_EMAIL: @@ -451,7 +449,6 @@ localSettings: # https://www.sefaria.org: english # https://www.sefaria.org.il: hebrew APSCHEDULER_NAME: "apscheduler-{{ .Values.deployEnv }}" - TURN_SERVER: '' USE_CLOUDFLARE: false FRONT_END_URL: "http://www.sefaria.org" # Use "http://${ENV_NAME}.cauldron.sefaria.org" in cauldrons OFFLINE: "False" diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 77cb1f4f07..ca075ab46c 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -2646,7 +2646,7 @@ class AddressPerek(AddressInteger): } section_patterns = { "en": r"""(?:(?:[Cc]h(apters?|\.)|[Pp]erek|s\.)?\s*)""", # the internal ? is a hack to allow a non match, even if 'strict' - "he": fr"""(?:\u05d1?{AddressType.reish_samekh_reg}\u05e4((?:"|\u05f4|''|'\s)|(?=[\u05d0-\u05ea]+(?:"|\u05f4|''|'\s))) # Peh (for 'perek') maybe followed by a quote of some sort OR lookahead for some letters followed by a quote (e.g. פי״א for chapter 11) + "he": fr"""(?:\u05d1?{AddressType.reish_samekh_reg}\u05e4((?:"|\u05f4|''|'\s|\s)|(?=[\u05d0-\u05ea]+(?:"|\u05f4|''|'\s))) # Peh (for 'perek') maybe followed by a quote of some sort OR lookahead for some letters followed by a quote (e.g. פי״א for chapter 11) |\u05e4\u05bc?\u05b6?\u05e8\u05b6?\u05e7(?:\u05d9\u05b4?\u05dd)?\s* # or 'perek(ym)' spelled out, followed by space )""" } diff --git a/static/css/s2.css b/static/css/s2.css index 27ebaf7302..29b4f4ea93 100644 --- a/static/css/s2.css +++ b/static/css/s2.css @@ -10283,17 +10283,21 @@ span.purim-emoji img{ .interface-english .spacer:only-of-type.empty:before, .interface-english .spacer:only-of-type.empty:before { content: "Write something... "; + margin-inline-start: 50px; } .interface-hebrew .sheetItem:only-of-type.empty .SheetOutsideText:before, .interface-hebrew .spacer:only-of-type.empty:before, .interface-hebrew .spacer:only-of-type.empty:before { content: "לכתוב משהו..."; + margin-inline-start: 50px; } .editorAddInterface { position: relative; pointer-events:none; background-color: transparent; + margin-inline-start: 50px; } + .editorAddInterface:before { content: ""; margin-inline-start:-46px; @@ -10313,6 +10317,34 @@ span.purim-emoji img{ box-sizing: border-box; box-shadow: 0px 1px 3px 0px #00000040; } +.editorAddLineButton { + position: relative; + background-color: transparent; + margin-inline-start: 50px; +} +.hidden.editorAddLineButton::before { + display: none; +} + +.editorAddLineButton:before { + content: ""; + margin-inline-start:-46px; + position: absolute; + width: 30px; + height: 30px; + transform: rotate(45deg); + background-color: white; + background-image: url("/static/icons/heavy-x-dark.svg"); + border: 1px solid var(--light-grey); + background-size: 14px; + border-radius: 50%; + /*pointer-events:auto;*/ + cursor: pointer; + background-repeat: no-repeat; + background-position: center; + box-sizing: border-box; + box-shadow: 0px 1px 3px 0px #00000040; +} .editorAddInterface:hover::before { background-color: var(--lighter-grey); @@ -10336,6 +10368,10 @@ background-color: white; pointer-events: none; display: inline-block; } +.hidden.editorAddInterface::before { + display: none; +} + .addInterfaceInput .textPreview { border-inline-start: 4px solid; diff --git a/static/js/AddToSourceSheet.jsx b/static/js/AddToSourceSheet.jsx index 8efaccc60c..3b456e1a11 100644 --- a/static/js/AddToSourceSheet.jsx +++ b/static/js/AddToSourceSheet.jsx @@ -12,6 +12,7 @@ import Component from 'react-class'; import sanitizeHtml from 'sanitize-html'; import { SignUpModalKind } from './sefaria/signupModalContent'; import { GDocAdvertBox } from './Promotions'; +import * as sheetsUtils from './sefaria/sheetsUtils' @@ -89,7 +90,34 @@ class AddToSourceSheetBox extends Component { }, this.confirmAdd); } } - addToSourceSheet() { + + //return the initial index of the suffix of string1 which also constitutes a prefix for string2 + longestSuffixPrefixIndex(string1, string2) { + let longestSuffixIndex = 0; + for (let i = 0; i < string1.length; i++){ + let suffix = string1.slice(i); + if (string2.startsWith(suffix)) { + longestSuffixIndex = i; + } + } + return longestSuffixIndex; + } + //return the final index of the prefix of string1 which also constitutes a suffix for string2 + longestPrefixSuffixIndex(string1, string2) { + let longestPrefixIndex = 0; + for (let i = 0; i < string1.length; i++) { + let prefix = string1.slice(0, i + 1); + if (string2.endsWith(prefix)) { + longestPrefixIndex = i + 1; + } + } + return longestPrefixIndex; + } + + normalize(text){ + return(text.replaceAll(/()+/g, ' ').replace(/\u2009/g, ' ').replace(/<[^>]*>/g, '')); + } + async addToSourceSheet() { if (!Sefaria._uid) { this.props.toggleSignUpModal(SignUpModalKind.AddToSheet); } @@ -111,15 +139,40 @@ class AddToSourceSheetBox extends Component { } } else if (this.props.srefs) { //regular use - this is currently the case when the component is loaded in the sidepanel or in the modal component via profiles and notes pages source.refs = this.props.srefs; + + + const { en, he } = this.props.currVersions ? this.props.currVersions : {"en": null, "he": null}; //the text we are adding may be non-default version if (he) { source["version-he"] = he; } if (en) { source["version-en"] = en; } // If something is highlighted and main panel language is not bilingual: // Use passed in language to determine which version this highlight covers. - var selectedWords = this.props.selectedWords; //if there was highlighted single panel + let selectedWords = this.props.selectedWords; //if there was highlighted single panel if (selectedWords && language != "bilingual") { - source[language.slice(0,2)] = selectedWords; + let lan = language.slice(0,2); + let segments = await sheetsUtils.getSegmentObjs(source.refs); + selectedWords = this.normalize(selectedWords); + segments = segments.map(segment => ({ + ...segment, + [lan]: this.normalize(segment[lan]) + })); + for (let iSegment = 0; iSegment < segments.length; iSegment++) { + const segment = segments[iSegment]; + if (iSegment == 0){ + let criticalIndex = this.longestSuffixPrefixIndex(segment[lan], selectedWords); + const ellipse = criticalIndex == 0 ? "" : "..."; + segment[lan] = ellipse + segment[lan].slice(criticalIndex); + } + else if (iSegment == segments.length-1){ + let criticalIndex = this.longestPrefixSuffixIndex(segment[lan], selectedWords); + const ellipse = criticalIndex == segment[lan].length-1 ? "" : "..."; + const chunk = segment[lan].slice(0, criticalIndex) + segment[lan] = chunk + ellipse; + } + } + + source[lan] = sheetsUtils.segmentsToSourceText(segments, lan); } } if (this.checkContentForImages(source.refs)) { diff --git a/static/js/Editor.jsx b/static/js/Editor.jsx index cd7a7ec624..027d5c5ec8 100644 --- a/static/js/Editor.jsx +++ b/static/js/Editor.jsx @@ -5,6 +5,8 @@ import {Editor, createEditor, Range, Node, Transforms, Path, Text, Point, Elemen import {Slate, Editable, ReactEditor, withReact, useSlate, useSelected, useFocused} from 'slate-react' import isHotkey from 'is-hotkey' import Sefaria from './sefaria/sefaria'; +import * as sheetsUtils from './sefaria/sheetsUtils' + import { SheetMetaDataBox, @@ -147,6 +149,59 @@ const format_to_html_lookup = format_tag_pairs.reduce((obj, item) => { return {node: bottom, path: bottomPath} }; +const isMultiNodeSelection = (editor) => { + if (!editor.selection) {return false} + + const [start, end] = Range.edges(editor.selection); + + const startPath = start.path; + const endPath = end.path; + + // If the start and end paths are different, it means multiple nodes are selected + return !Path.equals(startPath, endPath); +}; +const moveAnchorToEndOfClosestParagraph = (editor) => { + const { selection } = editor; + + if (selection && Range.isCollapsed(selection)) { + const { anchor } = selection; + const [closestParagraphNode, closestParagraphPath] = Editor.above(editor, { + at: anchor.path, + match: (n) => n.type === 'paragraph', + }) || []; + + if (closestParagraphNode) { + const endPoint = Editor.end(editor, closestParagraphPath); + + Transforms.select(editor, { + anchor: endPoint, + focus: endPoint, + }); + } + } +}; +const moveAnchorToEndOfCurrentNode = (editor) => { + const { selection } = editor; + + if (selection && Range.isCollapsed(selection)) { + const { anchor } = selection; + const node = Editor.node(editor, anchor); + + if (node) { + const [, path] = node; + const endPoint = Editor.end(editor, path); + + Transforms.select(editor, { + anchor: endPoint, + focus: endPoint + }); + } + } +}; +const insertNewLine = (editor) => { + moveAnchorToEndOfClosestParagraph(editor); + editor.insertBreak(); +} export const deserialize = el => { if (el.nodeType === 3) { @@ -1062,6 +1117,9 @@ const AddInterface = ({ attributes, children, element }) => { const Element = (props) => { const { attributes, children, element } = props; + const editor = useSlate(); + + const sheetItemClasses = { sheetItem: 1, empty: !(Node.string(element)), @@ -1180,11 +1238,32 @@ const Element = (props) => {
{children}
) case 'paragraph': + const selected = useSelected(); + + const addNewLineClasses = { + hidden: isMultiNodeSelection(editor) || !selected, + editorAddLineButton: 1, + }; + const handleClick = (event, editor) => { + // a way to check if the click was on the 'pseudo' ::before element or on the actual div + //this relies on the event bubbling mechanism doing sth iffy, we should findd a more deterministic way to implement this check + if (event.target.matches('.editorAddLineButton')) { //if click was on ::before + insertNewLine(editor); + } else { + return; + } + }; + const pClasses = {center: element["text-align"] == "center" }; return ( -
- {element.loading ?
: null} - {children} +
handleClick(event, editor)} + > +
+ {element.loading ?
: null} + {children} +
); case 'bulleted-list': @@ -1866,25 +1945,7 @@ const insertMedia = (editor, mediaUrl) => { Transforms.move(editor); } - -function placed_segment_mapper(lang, segmented, includeNumbers, s) { - if (!s[lang]) {return ""} - - let numStr = ""; - if (includeNumbers) { - const num = (lang=="he") ? Sefaria.hebrew.encodeHebrewNumeral(s.number) : s.number; - numStr = "(" + num + ") "; - } - let str = "" + numStr + s[lang] + " "; - if (segmented) { - str = "

" + str + "

"; - } - str = str.replace(/()+/g, ' ') - return str; -} - - -const insertSource = (editor, ref) => { +const insertSource = async (editor, ref) => { const path = editor.selection.anchor.path; Transforms.setNodes(editor, { loading: true }, {at: path}); @@ -1892,47 +1953,39 @@ const insertSource = (editor, ref) => { const nodeAbove = getNodeAbove(path, editor) const nodeBelow = getNodeBelow(path, editor) - Sefaria.getText(ref, {stripItags: 1}).then(text => { - let segments = Sefaria.makeSegments(text, false); - segments = Sefaria.stripImagesFromSegments(segments); - - let includeNumbers = $.inArray("Talmud", text.categories) == -1; - includeNumbers = text.indexTitle === "Pesach Haggadah" ? false : includeNumbers; - const segmented = !(text.categories[0] in {"Tanakh": 1, "Talmud": 1}); - - const enText = segments.map(placed_segment_mapper.bind(this, "en", segmented, includeNumbers)) - .filter(Boolean) - .join(""); - const heText = segments.map(placed_segment_mapper.bind(this, "he", segmented, includeNumbers)) - .filter(Boolean) - .join(""); - - let fragment = [{ - type: "SheetSource", - node: editor.children[0].nextNode, - ref: text.ref, - heRef: text.heRef, - heText: parseSheetItemHTML(heText), - enText: parseSheetItemHTML(enText), - title: null, - children: [ - {text: ""}, - ] - }]; - - if (!(nodeBelow.node && (nodeBelow.node.type == "SheetOutsideText" || nodeBelow.node.type == "paragraph" ) )) { - fragment.push({type: 'spacer', children: [{text: ""}]}) - } - Transforms.setNodes(editor, { loading: false }, { at: path }); - addItemToSheet(editor, fragment); - checkAndFixDuplicateSheetNodeNumbers(editor) - if (nodeAbove.node && (nodeAbove.node.type == "SheetOutsideText" || nodeAbove.node.type == "paragraph" ) ) { - Transforms.delete(editor, {at: path}) - } - - - Transforms.move(editor, { unit: 'block', distance: 1 }) - }); + const {en: normalEnRef, he: normalHeRef} = await sheetsUtils.getNormalRef(ref); + + let segments = await sheetsUtils.getSegmentObjs([ref]) + + const enText = sheetsUtils.segmentsToSourceText(segments, 'en'); + + const heText = sheetsUtils.segmentsToSourceText(segments, 'he'); + + let fragment = [{ + type: "SheetSource", + node: editor.children[0].nextNode, + ref: normalEnRef, + heRef: normalHeRef, + heText: parseSheetItemHTML(heText), + enText: parseSheetItemHTML(enText), + title: null, + children: [ + {text: ""}, + ] + }]; + + if (!(nodeBelow.node && (nodeBelow.node.type == "SheetOutsideText" || nodeBelow.node.type == "paragraph" ) )) { + fragment.push({type: 'spacer', children: [{text: ""}]}) + } + Transforms.setNodes(editor, { loading: false }, { at: path }); + addItemToSheet(editor, fragment); + checkAndFixDuplicateSheetNodeNumbers(editor) + if (nodeAbove.node && (nodeAbove.node.type == "SheetOutsideText" || nodeAbove.node.type == "paragraph" ) ) { + Transforms.delete(editor, {at: path}) + } + + + Transforms.move(editor, { unit: 'block', distance: 1 }) }; @@ -2488,7 +2541,7 @@ const SefariaEditor = (props) => { const editorContainer = useRef(); const [sheet, setSheet] = useState(props.data); const initValue = [{type: "sheet", children: [{text: ""}]}]; - const renderElement = useCallback(props => , []); + const renderElement = useCallback(props => , []); const [value, setValue] = useState(initValue); const [currentDocument, setCurrentDocument] = useState(initValue); const [unsavedChanges, setUnsavedChanges] = useState(false); @@ -2948,7 +3001,6 @@ const SefariaEditor = (props) => { [] ); - return (
{ diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index e408603ddf..735b24afd3 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -1506,9 +1506,9 @@ const AiInfoTooltip = () => {
setShowMessage(true)} onMouseLeave={() => setShowMessage(false)}>
- Some of the text on this page has been AI generated and reviewed by our editors. Learn more. + Some of the text on this page has been AI generated and reviewed by our editors. Learn more. חלק מהטקסטים בדף זה נוצרו על ידי בינה מלאכותית ועברו הגהה על ידי צוות העורכים שלנו.  - לפרטים נוספים + לפרטים נוספים
@@ -3303,12 +3303,8 @@ const AppStoreButton = ({ platform, href, altText }) => { const handleAnalyticsOnMarkdown = (e, gtag_fxn, rank, product, cta, label, link_type, analytics_event) => { - // e.preventDefault() - // https://github.com/STRML/react-router-component/blob/master/lib/CaptureClicks.js - // Get the element. - - //get the lowest level parent element of an event target that is an HTML link tag. Or Null. + // get the lowest level parent element of an event target that is an HTML link tag. Or Null. let target = e.target; let linkTarget = null; let parent = target; @@ -3336,7 +3332,7 @@ const handleAnalyticsOnMarkdown = (e, gtag_fxn, rank, product, cta, label, link_ return; } else { - gtag_fxn(rank, product, href, label, link_type, analytics_event); + gtag_fxn(rank, product, text, label, link_type, analytics_event); } } diff --git a/static/js/UserProfile.jsx b/static/js/UserProfile.jsx index 37969a2c49..424acacd51 100644 --- a/static/js/UserProfile.jsx +++ b/static/js/UserProfile.jsx @@ -342,7 +342,7 @@ class UserProfile extends Component { return (
- {this.props.profile.show_editor_toggle ? : null} + {(this.props.profile.id === Sefaria._uid && this.props.profile.show_editor_toggle) ? : null}
{ !this.props.profile.id ? :
diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js index e310d85f25..4bbcf901a5 100644 --- a/static/js/sefaria/sefaria.js +++ b/static/js/sefaria/sefaria.js @@ -276,6 +276,12 @@ Sefaria = extend(Sefaria, { let index = Sefaria.index(pRef.index); return index && index.categories ? index.categories : []; }, + refIndexTitle: function(ref) { + let pRef = Sefaria.parseRef(ref); + if ("error" in pRef) { return null; } + let index = Sefaria.index(pRef.index); + return index?.title + }, sectionRef: function(ref, deriveIfNotFound=false) { // Returns the section level ref for `ref` or null if no data is available const oref = this.getRefFromCache(ref); @@ -465,31 +471,64 @@ Sefaria = extend(Sefaria, { }); }, _bulkTexts: {}, + partitionArrayForURL: function(arr, urlMaxLength, dividerToken) { + const result = []; + const dividerTokenLength = encodeURIComponent(dividerToken).length; + let currentPartition = []; + let currentLength = 0; + + for (let i = 0; i < arr.length; i++) { + // Calculate the length of the new item when added to the partition + const item = arr[i]; + const encodedItem = encodeURIComponent(item); + const newLength = currentPartition.length === 0 + ? encodedItem.length + : currentLength + encodedItem.length + dividerTokenLength; // consider dividerToken length + + // Check if adding this item exceeds the max length + if (newLength > urlMaxLength) { + // If it does, push the current partition to the result and start a new one + result.push(currentPartition); + currentPartition = []; + currentLength = 0; + currentPartition.push(item); + continue + } + + // Add the item to the current partition + currentPartition.push(item); + currentLength = newLength; + } + + // Add the last partition to the result + if (currentPartition.length > 0) { + result.push(currentPartition); + } + + return result; +}, + getBulkText: function(refs, asSizedString=false, minChar=null, maxChar=null, transLangPref=null) { if (refs.length === 0) { return Promise.resolve({}); } const MAX_URL_LENGTH = 3800; - const hostStr = `${Sefaria.apiHost}/api/bulktext/`; + const ASSUMED_HOSTNAME_LENGTH_BOUND = 50; + const hostStr = encodeURI(`${Sefaria.apiHost}/api/bulktext/`); let paramStr = ''; for (let [paramKey, paramVal] of Object.entries({asSizedString, minChar, maxChar, transLangPref})) { paramStr = !!paramVal ? paramStr + `&${paramKey}=${paramVal}` : paramStr; } paramStr = paramStr.replace(/&/,'?'); + paramStr = encodeURI(paramStr); // Split into multiple requests if URL length goes above limit - let refStrs = [""]; - refs.map(ref => { - let last = refStrs[refStrs.length-1]; - const encodedFullURL = encodeURI(`${hostStr}${last}|${ref}${paramStr}`); - if (encodedFullURL.length > MAX_URL_LENGTH) { - refStrs.push(ref) - } else { - refStrs[refStrs.length-1] += last.length ? `|${ref}` : ref; - } - }); + const limit = MAX_URL_LENGTH-(hostStr+paramStr).length-ASSUMED_HOSTNAME_LENGTH_BOUND + const refsSubArrays = this.partitionArrayForURL( refs, limit, '|'); + const refStrs = refsSubArrays.map(refsSubArray => refsSubArray.join('|')); + let promises = refStrs.map(refStr => this._cachedApiPromise({ - url: encodeURI(`${hostStr}${refStr}${paramStr}`), + url: `${hostStr}${encodeURIComponent(refStr)}${paramStr}`, key: refStr + paramStr, store: this._bulkTexts })); diff --git a/static/js/sefaria/sheetsUtils.js b/static/js/sefaria/sheetsUtils.js new file mode 100644 index 0000000000..850f9167b4 --- /dev/null +++ b/static/js/sefaria/sheetsUtils.js @@ -0,0 +1,69 @@ +import Sefaria from "./sefaria"; + +export async function getSegmentObjs(refs) { + /* + Given an array of ref-strings (could also be ranged refs), + turn each ref to segment object and return an array of all segments + */ + const segments = []; + + for (const ref of refs) { + const text = await Sefaria.getText(ref, { stripItags: 1 }); + const newSegments = Sefaria.makeSegments(text, false); + segments.push(...newSegments); + } + return segments; +} +export async function getNormalRef(ref) { + /* + Given a ref-string, get he and en normal ref-string + */ + const refObj = await Sefaria.getRef(ref); + return {en: refObj.ref, he: refObj.heRef} +} +function placedSegmentMapper(lang, segmented, includeNumbers, s) { + /* + Map each segment object to a formatted text string + */ + if (!s[lang]) {return ""} + + let numStr = ""; + if (includeNumbers) { + const num = (lang=="he") ? Sefaria.hebrew.encodeHebrewNumeral(s.number) : s.number; + numStr = "(" + num + ") "; + } + let str = "" + numStr + s[lang] + " "; + if (segmented) { + str = "

" + str + "

"; + } + str = str.replace(/()+/g, ' ') + return str; +} +export const segmentsToSourceText = (segments, lan) => { + /* + Turn array of segment objects into one chunk of formatted text + */ + const segmented = shouldBeSegmented(segments[0].ref); + const includeNumbers = shouldIncludeSegmentNums(segments[0].ref); + return(segments.map(placedSegmentMapper.bind(this, lan, segmented, includeNumbers)) + .filter(Boolean) + .join("")); +} +function shouldIncludeSegmentNums(ref){ + /* + Decide if segment of this ref should have segment numbers when turned into text chunk + */ + const indexTitle = Sefaria.refIndexTitle(ref); + const categories = Sefaria.refCategories(ref); + if (categories.includes("Talmud")) {return false} + if (indexTitle === "Pesach Haggadah") {return false} + if (categories === 1) {return false} + return true; +} +function shouldBeSegmented(ref){ + /* + Decide if segment of this ref should be followed by new line when turned into text chunk + */ + const categories = Sefaria.refCategories(ref); + return !(categories[0] in {"Tanakh": 1, "Talmud": 1}); +} \ No newline at end of file