diff --git a/sefaria/sheetsUtils.js b/sefaria/sheetsUtils.js new file mode 100644 index 0000000000..77a2579853 --- /dev/null +++ b/sefaria/sheetsUtils.js @@ -0,0 +1,69 @@ +import Sefaria from "../static/js/sefaria/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 diff --git a/static/js/AddToSourceSheet.jsx b/static/js/AddToSourceSheet.jsx index 8efaccc60c..91cca73fd9 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..1f0b3ff2aa 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, @@ -1866,25 +1868,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 +1876,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 }) }; diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js index 593dc7d023..e3e71c024d 100644 --- a/static/js/sefaria/sefaria.js +++ b/static/js/sefaria/sefaria.js @@ -274,6 +274,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);