diff --git a/c-sharp-tests/DummyPapiClient.cs b/c-sharp-tests/DummyPapiClient.cs index f839334ee9..965e46ff66 100644 --- a/c-sharp-tests/DummyPapiClient.cs +++ b/c-sharp-tests/DummyPapiClient.cs @@ -151,7 +151,7 @@ public override void SendRequest( // handle, and no translation is needed. var ourSettingName = (string)details[1]; var pbSettingName = - ProjectSettings.GetPlatformBibleSettingNameFromParatextSettingName( + ProjectSettingsNames.GetPlatformBibleSettingNameFromParatextSettingName( ourSettingName ); diff --git a/c-sharp-tests/Services/ProjectSettingsServicesTests.cs b/c-sharp-tests/Services/ProjectSettingsServicesTests.cs index 1aab2981ad..7f7235beaf 100644 --- a/c-sharp-tests/Services/ProjectSettingsServicesTests.cs +++ b/c-sharp-tests/Services/ProjectSettingsServicesTests.cs @@ -14,7 +14,7 @@ public void IsValid_ValidLanguage_ReturnsTrue() var newValueJson = JsonConvert.SerializeObject("Spanish"); var currentValueJson = JsonConvert.SerializeObject("German"); papiClient.AddSettingValueToTreatAsValid( - ProjectSettings.PB_LANGUAGE, + ProjectSettingsNames.PB_LANGUAGE, newValueJson, currentValueJson ); @@ -22,7 +22,7 @@ public void IsValid_ValidLanguage_ReturnsTrue() papiClient, newValueJson, currentValueJson, - ProjectSettings.PT_LANGUAGE, + ProjectSettingsNames.PT_LANGUAGE, "" ); @@ -39,7 +39,7 @@ public void IsValid_InvalidSetting_ReturnsFalse() papiClient, newValueJson, currentValueJson, - ProjectSettings.PT_LANGUAGE, + ProjectSettingsNames.PT_LANGUAGE, "" ); @@ -50,9 +50,9 @@ public void IsValid_InvalidSetting_ReturnsFalse() public void GetDefault_KnownProperty_ReturnsDefaultValue() { DummyPapiClient papiClient = new DummyPapiClient(); - var result = ProjectSettingsService.GetDefault(papiClient, ProjectSettings.PT_LANGUAGE); + var result = ProjectSettingsService.GetDefault(papiClient, ProjectSettingsNames.PT_LANGUAGE); - Assert.That(result, Is.EqualTo($"default value for {ProjectSettings.PB_LANGUAGE}")); + Assert.That(result, Is.EqualTo($"default value for {ProjectSettingsNames.PB_LANGUAGE}")); } [Test] diff --git a/c-sharp/ParanextDataProvider.sln.DotSettings b/c-sharp/ParanextDataProvider.sln.DotSettings index aeae995a65..4c16aefdef 100644 --- a/c-sharp/ParanextDataProvider.sln.DotSettings +++ b/c-sharp/ParanextDataProvider.sln.DotSettings @@ -1,11 +1,15 @@  ID True + True True True True True + True + True True + True True True True diff --git a/c-sharp/Projects/LocalParatextProjects.cs b/c-sharp/Projects/LocalParatextProjects.cs index 3688e785bc..ce3f5cf86c 100644 --- a/c-sharp/Projects/LocalParatextProjects.cs +++ b/c-sharp/Projects/LocalParatextProjects.cs @@ -12,7 +12,7 @@ internal class LocalParatextProjects { #region Constructors, consts, and fields - // Inside of each project's "home" directory, these are the subdirectories and files + // Inside each project's "home" directory, these are the subdirectories and files protected const string PROJECT_SETTINGS_FILE = "Settings.xml"; /// @@ -21,10 +21,18 @@ internal class LocalParatextProjects public const string EXTENSION_DATA_SUBDIRECTORY = "shared/platform.bible/extensions"; private bool _isInitialized = false; + private readonly object _initializationLock = new(); - private List _requiredProjectRootFiles = ["usfm.sty", "Attribution.md"]; + private readonly List _requiredProjectRootFiles = ["usfm.sty", "Attribution.md"]; - private static readonly List _paratextProjectInterfaces = [ProjectInterfaces.Base, ProjectInterfaces.USFM_BookChapterVerse, ProjectInterfaces.USX_Chapter]; + private static readonly List s_paratextProjectInterfaces = [ + ProjectInterfaces.BASE, + ProjectInterfaces.USFM_BOOK, + ProjectInterfaces.USFM_CHAPTER, + ProjectInterfaces.USFM_VERSE, + ProjectInterfaces.USX_BOOK, + ProjectInterfaces.USX_CHAPTER, + ProjectInterfaces.USX_VERSE]; public LocalParatextProjects() { @@ -42,46 +50,56 @@ public LocalParatextProjects() #region Public properties and methods - public virtual void Initialize(bool shouldIncludePT9ProjectsOnWindows) { if (_isInitialized) return; - // Make sure the necessary directory and files exist for the project root folder - SetUpProjectRootFolder(); + lock (_initializationLock) + { + if (_isInitialized) + return; + + // Make sure the necessary directory and files exist for the project root folder + SetUpProjectRootFolder(); - // Set up the ScrTextCollection and read the projects in that folder - ParatextGlobals.Initialize(ProjectRootFolder); + // Set up the ScrTextCollection and read the projects in that folder + ParatextGlobals.Initialize(ProjectRootFolder); - Console.WriteLine($"Projects loaded from {ProjectRootFolder}: {GetScrTexts().Select(scrText => scrText.Name)}"); + Console.WriteLine( + $"Projects loaded from {ProjectRootFolder}: {string.Join(",", GetScrTexts().Select(scrText => scrText.Name))}"); - // Read the projects in any locations other than project root folder - IEnumerable otherProjectDetails = LoadOtherProjectDetails(shouldIncludePT9ProjectsOnWindows); + // Read the projects in any locations other than project root folder + IEnumerable otherProjectDetails = + LoadOtherProjectDetails(shouldIncludePT9ProjectsOnWindows); - if (otherProjectDetails.Any()) - Console.WriteLine($"Projects found in other locations: {otherProjectDetails.Select(projectDetails => projectDetails.Name)}"); + if (otherProjectDetails.Any()) + Console.WriteLine( + $"Projects found in other locations: {string.Join(",", otherProjectDetails.Select(projectDetails => projectDetails.Name))}"); - foreach (ProjectDetails projectDetails in otherProjectDetails) - { - try + foreach (ProjectDetails projectDetails in otherProjectDetails) { - AddProjectToScrTextCollection(projectDetails); - Console.WriteLine($"Loaded project details: {projectDetails}"); + try + { + AddProjectToScrTextCollection(projectDetails); + Console.WriteLine($"Loaded project details: {projectDetails}"); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to load project for {projectDetails}: {ex}"); + } } - catch (Exception ex) + + // If there are no projects available anywhere, throw in the sample WEB one + if (!GetScrTexts().Any()) { - Console.WriteLine($"Failed to load project for {projectDetails}: {ex}"); - } - } + Console.WriteLine("No projects found. Setting up sample WEB project"); + SetUpSampleProject(); - // If there are no projects available anywhere, throw in the sample WEB one - if (!GetScrTexts().Any()) - { - Console.WriteLine("No projects found. Setting up sample WEB project"); - SetUpSampleProject(); + ScrTextCollection.RefreshScrTexts(); + } - ScrTextCollection.RefreshScrTexts(); + _isInitialized = true; } } @@ -102,7 +120,7 @@ public static ScrText GetParatextProject(string projectId) public static List GetParatextProjectInterfaces() { - return new List(_paratextProjectInterfaces); + return [..s_paratextProjectInterfaces]; } #endregion @@ -152,7 +170,7 @@ private static void AddProjectToScrTextCollection(ProjectDetails projectDetails) /// Enumeration of (ProjectMetadata, project directory) tuples for all projects private IEnumerable LoadOtherProjectDetails(bool shouldIncludePT9ProjectsOnWindows) { - // Get project info for projects outside of the normal project root folder + // Get project info for projects outside the normal project root folder List nonPT9ProjectRootFolders = []; if (OperatingSystem.IsWindows() && shouldIncludePT9ProjectsOnWindows && Directory.Exists(Paratext9ProjectsFolder)) nonPT9ProjectRootFolders.Add(Paratext9ProjectsFolder); diff --git a/c-sharp/Projects/ParatextProjectDataProvider.cs b/c-sharp/Projects/ParatextProjectDataProvider.cs index 06ebcdc5cf..78887ac733 100644 --- a/c-sharp/Projects/ParatextProjectDataProvider.cs +++ b/c-sharp/Projects/ParatextProjectDataProvider.cs @@ -12,7 +12,6 @@ using Paratext.Data; using Paratext.Data.ProjectSettingsAccess; using SIL.Scripture; -using ProjectSettings = Paranext.DataProvider.Services.ProjectSettings; namespace Paranext.DataProvider.Projects; @@ -27,7 +26,9 @@ internal class ParatextProjectDataProvider : ProjectDataProvider ProjectDataType.BOOK_USFM, ProjectDataType.CHAPTER_USFM, ProjectDataType.VERSE_USFM, - ProjectDataType.CHAPTER_USX + ProjectDataType.BOOK_USX, + ProjectDataType.CHAPTER_USX, + ProjectDataType.VERSE_USX ]; private readonly LocalParatextProjects _paratextProjects; @@ -46,12 +47,16 @@ LocalParatextProjects paratextProjects { _paratextProjects = paratextProjects; Getters.Add("getBookUSFM", GetBookUsfm); + Setters.Add("setBookUSFM", SetBookUsfm); Getters.Add("getChapterUSFM", GetChapterUsfm); Setters.Add("setChapterUSFM", SetChapterUsfm); Getters.Add("getVerseUSFM", GetVerseUsfm); + Getters.Add("getBookUSX", GetBookUsx); + Setters.Add("setBookUSX", SetBookUsx); Getters.Add("getChapterUSX", GetChapterUsx); Setters.Add("setChapterUSX", SetChapterUsx); + Getters.Add("getVerseUSX", GetVerseUsx); Getters.Add("getSetting", GetProjectSetting); Setters.Add("setSetting", SetProjectSetting); @@ -148,7 +153,7 @@ public override ResponseToRequest SetExtensionData(ProjectDataScope scope, strin try { RunWithinLock( - WriteScope.ProjectData(scrText), + WriteScope.EntireProject(scrText), writeLock => { if (!writeLock.Active) @@ -187,9 +192,14 @@ protected virtual IProjectStreamManager CreateStreamManager(ProjectDetails proje #endregion #region Settings + public static string VisibilitySettingName => Setting.Visibility.ToString(); private void RegisterSettingsValidators() { + ProjectSettingsService.RegisterValidator(PapiClient, VisibilitySettingName, + VisibilityValidator); + return; + (bool result, string? error) VisibilityValidator((string newValueJson, string currentValueJson, string allChangesJson) data) { @@ -204,7 +214,7 @@ private void RegisterSettingsValidators() error = $"New {VisibilitySettingName} value cannot be null."; } else if (value is not ProjectVisibility && (value is not string valueStr || - !Enum.TryParse(valueStr, out var visibility))) + !Enum.TryParse(valueStr, out _))) { result = false; error = $"New {VisibilitySettingName} value is not valid."; @@ -216,16 +226,13 @@ private void RegisterSettingsValidators() return (false, ex.Message); } } - - ProjectSettingsService.RegisterValidator(PapiClient, VisibilitySettingName, - VisibilityValidator); } public ResponseToRequest GetProjectSetting(string jsonKey) { var settingName = JToken.Parse(jsonKey).ToString(); settingName = - ProjectSettings.GetParatextSettingNameFromPlatformBibleSettingName(settingName) ?? + ProjectSettingsNames.GetParatextSettingNameFromPlatformBibleSettingName(settingName) ?? settingName; var scrText = LocalParatextProjects.GetParatextProject(ProjectDetails.Metadata.ID); @@ -233,12 +240,12 @@ public ResponseToRequest GetProjectSetting(string jsonKey) // accessing scrText.Settings.Name. So we're copying Paratext's functionality here and using // the folder name instead of Settings.Name. // https://github.com/ubsicap/Paratext/blob/aaadecd828a9b02e6f55d18e4c5dda8703ce2429/ParatextData/ProjectSettingsAccess/ProjectSettings.cs#L1438 - if (settingName == ProjectSettings.PT_NAME) + if (settingName == ProjectSettingsNames.PT_NAME) return ResponseToRequest.Succeeded(scrText.Name); if (scrText.Settings.ParametersDictionary.TryGetValue(settingName, out string? settingValue)) { // Paratext project setting value found, so return the value with the appropriate type - if (ProjectSettings.IsParatextSettingABoolean(settingName)) + if (ProjectSettingsNames.IsParatextSettingABoolean(settingName)) { return settingValue switch { @@ -283,7 +290,7 @@ public ResponseToRequest SetProjectSetting(string jsonKey, string value) return ResponseToRequest.Failed($"Validation failed for {settingName}"); // Figure out which setting name to use - var paratextSettingName = ProjectSettings.GetParatextSettingNameFromPlatformBibleSettingName( + var paratextSettingName = ProjectSettingsNames.GetParatextSettingNameFromPlatformBibleSettingName( settingName) ?? settingName; // Now actually write the setting @@ -293,14 +300,14 @@ public ResponseToRequest SetProjectSetting(string jsonKey, string value) // accessing scrText.Settings.Name. So we're copying Paratext's functionality here and using // the folder name instead of Settings.Name. // https://github.com/ubsicap/Paratext/blob/aaadecd828a9b02e6f55d18e4c5dda8703ce2429/ParatextData/ScrText.cs#L259 - if (paratextSettingName == ProjectSettings.PT_NAME) + if (paratextSettingName == ProjectSettingsNames.PT_NAME) { // Lock the whole project because this is literally moving the whole folder (chances // this will actually succeed are very slim as the project must only have Settings.xml // and the ldml file for this not to instantly throw) // https://github.com/ubsicap/Paratext/blob/aaadecd828a9b02e6f55d18e4c5dda8703ce2429/ParatextData/ScrText.cs#L1793 RunWithinLock( - WriteScope.AllSettingsFiles(), + WriteScope.EntireProject(scrText), _ => { try { @@ -315,12 +322,14 @@ public ResponseToRequest SetProjectSetting(string jsonKey, string value) else { RunWithinLock( - WriteScope.AllSettingsFiles(), + WriteScope.EntireProject(scrText), _ => { try { scrText.Settings.SetSetting(paratextSettingName, value); - scrText.Settings.Save(); + // We are notifying when we release our lock, so don't automatically + // notify in `Save` + scrText.Settings.Save(false); } catch (Exception ex) { @@ -373,6 +382,44 @@ public ResponseToRequest GetVerseUsfm(string jsonString) (ScrText scrText, VerseRef verseRef) => scrText.Parser.GetVerseUsfmText(verseRef)); } + public ResponseToRequest SetBookUsfm(string jsonString, string data) + { + if (!VerseRefConverter.TryCreateVerseRef(jsonString, out var verseRef, out var error)) + return ResponseToRequest.Failed(error); + verseRef.ChapterNum = 0; + + try + { + var scrText = LocalParatextProjects.GetParatextProject(ProjectDetails.Metadata.ID); + RunWithinLock( + WriteScope.EntireProject(scrText), + _ => + { + BookSet localBooksPresentSet = scrText.Settings.LocalBooksPresentSet; + if (!localBooksPresentSet.IsSelected(verseRef.BookNum) && !scrText.Creatable(verseRef.BookNum)) + throw new Exception($"{verseRef.Book} cannot be created"); + if (!scrText.Writable(verseRef.BookNum, 0)) + throw new Exception($"{verseRef.Book} is not writable"); + if (!scrText.Settings.Editable) + throw new Exception($"{verseRef.Book} is not editable"); + byte[] rawData = scrText.Settings.Encoder.Convert(data, out string errorMessage); + if (!string.IsNullOrEmpty(errorMessage)) + throw new Exception(errorMessage); + string bookFilePath = scrText.Settings.BookFileName(verseRef.BookNum, true); + File.WriteAllBytes(bookFilePath, rawData); + scrText.Reload(); + } + ); + } + catch (Exception e) + { + return ResponseToRequest.Failed(e.ToString()); + } + + // The value of returned strings are case-sensitive and cannot change unless data provider subscriptions change + return ResponseToRequest.Succeeded(AllScriptureDataTypes); + } + public ResponseToRequest SetChapterUsfm(string jsonString, string data) { if (!VerseRefConverter.TryCreateVerseRef(jsonString, out var verseRef, out var error)) @@ -382,7 +429,7 @@ public ResponseToRequest SetChapterUsfm(string jsonString, string data) { var scrText = LocalParatextProjects.GetParatextProject(ProjectDetails.Metadata.ID); RunWithinLock( - WriteScope.ProjectText(scrText, verseRef.BookNum, verseRef.ChapterNum), + WriteScope.EntireProject(scrText), writeLock => { scrText.PutText( @@ -408,23 +455,39 @@ public ResponseToRequest SetChapterUsfm(string jsonString, string data) #region USX + public ResponseToRequest GetBookUsx(string jsonString) + { + return GetFromScrText(jsonString, + (ScrText scrText, VerseRef verseRef) => ConvertUsfmToUsx(scrText, verseRef, false)); + } + public ResponseToRequest GetChapterUsx(string jsonString) { return GetFromScrText(jsonString, - (ScrText scrText, VerseRef verseRef) => - { - var scrStylesheet = scrText.ScrStylesheet(verseRef.BookNum); - // Tokenize usfm - List tokens = UsfmToken.Tokenize(scrStylesheet, scrText.GetText(verseRef, true, true) ?? string.Empty, false); + (ScrText scrText, VerseRef verseRef) => ConvertUsfmToUsx(scrText, verseRef, true)); + } - XmlDocument doc = new(); - using (XmlWriter xmlWriter = doc.CreateNavigator()!.AppendChild()) - { - UsfmToUsx.ConvertToXmlWriter(scrStylesheet, tokens, xmlWriter, false); - xmlWriter.Flush(); - } - return doc.OuterXml; - }); + public ResponseToRequest GetVerseUsx(string jsonString) + { + return GetFromScrText(jsonString, ExtractVerseUsx); + } + + public ResponseToRequest SetBookUsx(string jsonString, string data) + { + if (!VerseRefConverter.TryCreateVerseRef(jsonString, out var verseRef, out var error)) + return ResponseToRequest.Failed(error); + + try + { + // Don't need to take a write lock in this function because SetBookUsfm will do it + var scrText = LocalParatextProjects.GetParatextProject(ProjectDetails.Metadata.ID); + string usfm = ConvertUsxToUsfm(scrText, verseRef, data); + return SetBookUsfm(jsonString, usfm); + } + catch (Exception e) + { + return ResponseToRequest.Failed(e.ToString()); + } } public ResponseToRequest SetChapterUsx(string jsonString, string data) @@ -437,27 +500,10 @@ public ResponseToRequest SetChapterUsx(string jsonString, string data) { var scrText = LocalParatextProjects.GetParatextProject(ProjectDetails.Metadata.ID); RunWithinLock( - WriteScope.ProjectText(scrText, verseRef.BookNum, verseRef.ChapterNum), + WriteScope.EntireProject(scrText), writeLock => { - XDocument doc; - using (TextReader reader = new StringReader(data)) - doc = XDocument.Load(reader, LoadOptions.PreserveWhitespace); - - if (doc.Root?.Name != "usx") - { - failedMessage = "Invalid USX"; - return; - } - - UsxFragmenter.FindFragments( - scrText.ScrStylesheet(verseRef.BookNum), - doc.CreateNavigator(), - XPathExpression.Compile("*[false()]"), - out string usfm - ); - - usfm = UsfmToken.NormalizeUsfm(scrText, verseRef.BookNum, usfm); + var usfm = ConvertUsxToUsfm(scrText, verseRef, data); scrText.PutText(verseRef.BookNum, verseRef.ChapterNum, true, usfm, writeLock); }); } @@ -491,6 +537,71 @@ private ResponseToRequest GetFromScrText(string verseRefJson, Func action) { var myLock = diff --git a/c-sharp/Projects/ProjectDataType.cs b/c-sharp/Projects/ProjectDataType.cs index a32b0053ce..4811fa8a91 100644 --- a/c-sharp/Projects/ProjectDataType.cs +++ b/c-sharp/Projects/ProjectDataType.cs @@ -3,8 +3,10 @@ namespace Paranext.DataProvider.Projects; public sealed class ProjectDataType { public const string BOOK_USFM = "BookUSFM"; + public const string BOOK_USX = "BookUSX"; public const string CHAPTER_USFM = "ChapterUSFM"; public const string CHAPTER_USX = "ChapterUSX"; public const string SETTING = "Setting"; public const string VERSE_USFM = "VerseUSFM"; + public const string VERSE_USX = "VerseUSX"; } diff --git a/c-sharp/Projects/ProjectInterfaces.cs b/c-sharp/Projects/ProjectInterfaces.cs index 3f1eb151a2..440ba350f4 100644 --- a/c-sharp/Projects/ProjectInterfaces.cs +++ b/c-sharp/Projects/ProjectInterfaces.cs @@ -9,7 +9,11 @@ public static class ProjectInterfaces /// The name of the `projectInterface` representing the essential methods every Base Project Data /// Provider must implement /// - public const string Base = "platform.base"; - public const string USFM_BookChapterVerse = "platformScripture.USFM_BookChapterVerse"; - public const string USX_Chapter = "platformScripture.USX_Chapter"; + public const string BASE = "platform.base"; + public const string USFM_BOOK = "platformScripture.USFM_Book"; + public const string USFM_CHAPTER = "platformScripture.USFM_Chapter"; + public const string USFM_VERSE = "platformScripture.USFM_Verse"; + public const string USX_BOOK = "platformScripture.USX_Book"; + public const string USX_CHAPTER = "platformScripture.USX_Chapter"; + public const string USX_VERSE = "platformScripture.USX_Verse"; } diff --git a/c-sharp/Services/ProjectSettings.cs b/c-sharp/Services/ProjectSettingsNames.cs similarity index 92% rename from c-sharp/Services/ProjectSettings.cs rename to c-sharp/Services/ProjectSettingsNames.cs index 2fda42f142..6e1e061c6a 100644 --- a/c-sharp/Services/ProjectSettings.cs +++ b/c-sharp/Services/ProjectSettingsNames.cs @@ -1,6 +1,6 @@ namespace Paranext.DataProvider.Services; -public sealed class ProjectSettings +public sealed class ProjectSettingsNames { public const string PB_BOOKS_PRESENT = "platformScripture.booksPresent"; public const string PT_BOOKS_PRESENT = "BooksPresent"; @@ -23,7 +23,7 @@ public sealed class ProjectSettings /// /// Paratext setting names that are either T or F and need to be converted to booleans /// - private static readonly HashSet _ptSettingBooleans = ["Editable", "MatchBasedOnStems", "AllowReadAccess", "AllowSharingWithSLDR", ]; + private static readonly HashSet s_ptSettingBooleans = ["Editable", "MatchBasedOnStems", "AllowReadAccess", "AllowSharingWithSLDR", ]; // Make sure this dictionary gets updated whenever new settings are added private static readonly Dictionary s_platformBibleToParatextSettingsNames = @@ -71,6 +71,6 @@ public sealed class ProjectSettings /// public static bool IsParatextSettingABoolean(string ptSettingName) { - return _ptSettingBooleans.Contains(ptSettingName); + return s_ptSettingBooleans.Contains(ptSettingName); } } diff --git a/extensions/src/hello-world/src/web-views/hello-world.web-view.tsx b/extensions/src/hello-world/src/web-views/hello-world.web-view.tsx index 13859ee988..e0e3d23333 100644 --- a/extensions/src/hello-world/src/web-views/hello-world.web-view.tsx +++ b/extensions/src/hello-world/src/web-views/hello-world.web-view.tsx @@ -105,7 +105,7 @@ globalThis.webViewComponent = function HelloWorld({ // Test ref parameter properly getting latest value currentRender: currentRender.current, optionsSource: 'hook', - includeProjectInterfaces: ['platformScripture.USFM_BookChapterVerse'], + includeProjectInterfaces: ['platformScripture.USFM_Verse'], }, useCallback( (selectedProject, _dialogType, { currentRender: dialogRender, optionsSource }) => { @@ -150,7 +150,7 @@ globalThis.webViewComponent = function HelloWorld({ iconUrl: 'papi-extension://helloWorld/assets/offline.svg', title: 'Select List of Hello World Projects', selectedProjectIds: projects, - includeProjectInterfaces: ['platformScripture.USFM_BookChapterVerse'], + includeProjectInterfaces: ['platformScripture.USFM_Verse'], }), [projects], ), @@ -194,7 +194,7 @@ globalThis.webViewComponent = function HelloWorld({ const [personAge] = useData('helloSomeone.people').Age(name, -1); const [currentProjectVerse] = useProjectData( - 'platformScripture.USFM_BookChapterVerse', + 'platformScripture.USFM_Verse', projectId ?? undefined, ).VerseUSFM(verseRef, 'Loading Verse'); diff --git a/extensions/src/platform-scripture/cspell.json b/extensions/src/platform-scripture/cspell.json index e65b0a364e..723d4dc86d 100644 --- a/extensions/src/platform-scripture/cspell.json +++ b/extensions/src/platform-scripture/cspell.json @@ -16,6 +16,7 @@ "appdata", "asyncs", "autodocs", + "biblionexus", "dockbox", "electronmon", "endregion", @@ -24,6 +25,10 @@ "guids", "hopkinson", "iframes", + "iife", + "iusfm", + "iusj", + "iusx", "localstorage", "maximizable", "networkable", @@ -34,6 +39,7 @@ "papis", "paranext", "paratext", + "pdpe", "pdpf", "pdps", "plusplus", diff --git a/extensions/src/platform-scripture/src/project-data-provider/platform-scripture-extender-pdpe.model.ts b/extensions/src/platform-scripture/src/project-data-provider/platform-scripture-extender-pdpe.model.ts index 401a7c16af..ab12187a84 100644 --- a/extensions/src/platform-scripture/src/project-data-provider/platform-scripture-extender-pdpe.model.ts +++ b/extensions/src/platform-scripture/src/project-data-provider/platform-scripture-extender-pdpe.model.ts @@ -6,22 +6,38 @@ import { DataProviderUpdateInstructions, IProjectDataProviderEngine } from '@pap import { VerseRef } from '@sillsdev/scripture'; import type { ProjectDataProviderInterfaces } from 'papi-shared-types'; import { UnsubscriberAsync, UnsubscriberAsyncList } from 'platform-bible-utils'; -import { USJChapterProjectInterfaceDataTypes } from 'platform-scripture'; +import { + USJBookProjectInterfaceDataTypes, + USJChapterProjectInterfaceDataTypes, + USJVerseProjectInterfaceDataTypes, +} from 'platform-scripture'; import { Usj, usjToUsxString, usxStringToUsj } from '@biblionexus-foundation/scripture-utilities'; /** The `projectInterface`s the Scripture Extender PDPF serves */ // TypeScript is upset without `satisfies` here because `as const` makes the array readonly but it // needs to be used in ProjectMetadata as not readonly :p export const SCRIPTURE_EXTENDER_PROJECT_INTERFACES = [ + 'platformScripture.USJ_Book', 'platformScripture.USJ_Chapter', -] as const satisfies ['platformScripture.USJ_Chapter']; + 'platformScripture.USJ_Verse', +] as const satisfies [ + 'platformScripture.USJ_Book', + 'platformScripture.USJ_Chapter', + 'platformScripture.USJ_Verse', +]; /** The project interfaces the Scripture Extender Layering PDPF layers over */ // TypeScript is upset without `satisfies` here because `as const` makes the array readonly but it // needs to be used in ProjectMetadata as not readonly :p export const SCRIPTURE_EXTENDER_OVERLAY_PROJECT_INTERFACES = [ + 'platformScripture.USX_Book', + 'platformScripture.USX_Chapter', + 'platformScripture.USX_Verse', +] as const satisfies [ + 'platformScripture.USX_Book', 'platformScripture.USX_Chapter', -] as const satisfies ['platformScripture.USX_Chapter']; + 'platformScripture.USX_Verse', +]; export type ScriptureExtenderOverlayPDPs = { [ProjectInterface in (typeof SCRIPTURE_EXTENDER_OVERLAY_PROJECT_INTERFACES)[number]]: ProjectDataProviderInterfaces[ProjectInterface]; @@ -34,7 +50,9 @@ class ScriptureExtenderProjectDataProviderEngine /** The PDPs this layering PDP needs to function */ private readonly pdps: ScriptureExtenderOverlayPDPs; + private bookUSXUnsubscriberPromise: Promise; private chapterUSXUnsubscriberPromise: Promise; + private verseUSXUnsubscriberPromise: Promise; constructor( private readonly projectId: string, @@ -43,6 +61,26 @@ class ScriptureExtenderProjectDataProviderEngine super(); this.pdps = pdpsToOverlay; + this.bookUSXUnsubscriberPromise = this.pdps['platformScripture.USX_Book'].subscribeBookUSX( + // Just picked a key for no reason in particular because we don't need anything in particular + // here because we're listening for all updates + new VerseRef(1, 1, 1), + () => { + this.notifyUpdate('BookUSJ'); + }, + { whichUpdates: '*', retrieveDataImmediately: false }, + ); + + // Synchronously set up an error logger because an IIFE says this.bookUSXUnsubscriberPromise + // is being used before it is defined (most likely because it's a separate function running + // inside the constructor) + // code-review-disable-next-line complain-about-promise-chains ;) + this.bookUSXUnsubscriberPromise.catch((e) => { + logger.error( + `Scripture Extender PDP for project ${this.projectId} failed to subscribe to BookUSX! ${e}`, + ); + }); + // Set up subscriptions to listen for changes to the project data to overlay and update // our own subscribers this.chapterUSXUnsubscriberPromise = this.pdps[ @@ -66,6 +104,41 @@ class ScriptureExtenderProjectDataProviderEngine `Scripture Extender PDP for project ${this.projectId} failed to subscribe to ChapterUSX! ${e}`, ); }); + + this.verseUSXUnsubscriberPromise = this.pdps['platformScripture.USX_Verse'].subscribeVerseUSX( + // Just picked a key for no reason in particular because we don't need anything in particular + // here because we're listening for all updates + new VerseRef(1, 1, 1), + () => { + this.notifyUpdate('VerseUSJ'); + }, + { whichUpdates: '*', retrieveDataImmediately: false }, + ); + + // Synchronously set up an error logger because an IIFE says this.verseUSXUnsubscriberPromise + // is being used before it is defined (most likely because it's a separate function running + // inside the constructor) + // code-review-disable-next-line complain-about-promise-chains ;) + this.verseUSXUnsubscriberPromise.catch((e) => { + logger.error( + `Scripture Extender PDP for project ${this.projectId} failed to subscribe to VerseUSX! ${e}`, + ); + }); + } + + // Do not emit update events when running this method because we are subscribing to data updates + // and sending out update events in the constructor + @papi.dataProviders.decorators.doNotNotify + async setBookUSJ( + verseRef: VerseRef, + bookUsj: Usj, + ): Promise> { + const didSucceed = await this.pdps['platformScripture.USX_Book'].setBookUSX( + verseRef, + usjToUsxString(bookUsj), + ); + if (didSucceed) return true; + return false; } // Do not emit update events when running this method because we are subscribing to data updates @@ -83,11 +156,26 @@ class ScriptureExtenderProjectDataProviderEngine return false; } + // eslint-disable-next-line class-methods-use-this + async setVerseUSJ(): Promise> { + throw new Error('Cannot call setVerseUSJ, use setChapterUSJ or setBookUSJ instead'); + } + + async getBookUSJ(verseRef: VerseRef): Promise { + const usx = await this.pdps['platformScripture.USX_Book'].getBookUSX(verseRef); + return usx ? usxStringToUsj(usx) : undefined; + } + async getChapterUSJ(verseRef: VerseRef): Promise { const usx = await this.pdps['platformScripture.USX_Chapter'].getChapterUSX(verseRef); return usx ? usxStringToUsj(usx) : undefined; } + async getVerseUSJ(verseRef: VerseRef): Promise { + const usx = await this.pdps['platformScripture.USX_Verse'].getVerseUSX(verseRef); + return usx ? usxStringToUsj(usx) : undefined; + } + /** * Disposes of this Project Data Provider Engine. Unsubscribes from listening to overlaid PDPs * @@ -98,7 +186,11 @@ class ScriptureExtenderProjectDataProviderEngine `Scripture Extender PDP Engine ${this.projectId} Overlaid PDP Unsubscribers`, ); - unsubscriberList.add(await this.chapterUSXUnsubscriberPromise); + unsubscriberList.add( + await this.bookUSXUnsubscriberPromise, + await this.chapterUSXUnsubscriberPromise, + await this.verseUSXUnsubscriberPromise, + ); return unsubscriberList.runAllUnsubscribers(); } diff --git a/extensions/src/platform-scripture/src/project-data-provider/platform-scripture-extender-pdpef.model.ts b/extensions/src/platform-scripture/src/project-data-provider/platform-scripture-extender-pdpef.model.ts index dd1703a8b0..56f2c94023 100644 --- a/extensions/src/platform-scripture/src/project-data-provider/platform-scripture-extender-pdpef.model.ts +++ b/extensions/src/platform-scripture/src/project-data-provider/platform-scripture-extender-pdpef.model.ts @@ -28,7 +28,11 @@ class ScriptureExtenderProjectDataProviderEngineFactory // eslint-disable-next-line class-methods-use-this async createProjectDataProviderEngine( projectId: string, - ): Promise> { + ): Promise< + IProjectDataProviderEngine< + ['platformScripture.USJ_Book', 'platformScripture.USJ_Chapter', 'platformScripture.USJ_Verse'] + > + > { // We're creating a ScriptureExtenderOverlayPDPs. Seems Object.fromEntries doesn't support // mapped types very well // eslint-disable-next-line no-type-assertion/no-type-assertion diff --git a/extensions/src/platform-scripture/src/types/platform-scripture.d.ts b/extensions/src/platform-scripture/src/types/platform-scripture.d.ts index 231080017e..27ce4cb889 100644 --- a/extensions/src/platform-scripture/src/types/platform-scripture.d.ts +++ b/extensions/src/platform-scripture/src/types/platform-scripture.d.ts @@ -10,32 +10,74 @@ declare module 'platform-scripture' { import { UnsubscriberAsync } from 'platform-bible-utils'; import type { Usj } from '@biblionexus-foundation/scripture-utilities'; - /** Provides Scripture data in USFM format by book, chapter, or verse */ - export type USFMBookChapterVerseProjectInterfaceDataTypes = { + // #region Project Interface Data Types + + /** Provides Scripture data in USFM format by book */ + export type USFMBookProjectInterfaceDataTypes = { /** Gets/sets the "raw" USFM data for the specified book */ BookUSFM: DataProviderDataType; + }; + + /** Provides Scripture data in USFM format by chapter */ + export type USFMChapterProjectInterfaceDataTypes = { /** Gets/sets the "raw" USFM data for the specified chapter */ ChapterUSFM: DataProviderDataType; - /** Gets/sets the "raw" USFM data for the specified verse */ + }; + + /** Provides Scripture data in USFM format by verse */ + export type USFMVerseProjectInterfaceDataTypes = { + /** Gets the "raw" USFM data for the specified verse */ VerseUSFM: DataProviderDataType; }; + /** Provides Scripture data in USX format by book */ + export type USXBookProjectInterfaceDataTypes = { + /** Gets/sets the data in USX form for the specified book */ + BookUSX: DataProviderDataType; + }; + /** Provides Scripture data in USX format by chapter */ export type USXChapterProjectInterfaceDataTypes = { /** Gets/sets the data in USX form for the specified chapter */ ChapterUSX: DataProviderDataType; }; + /** Provides Scripture data in USX format by verse */ + export type USXVerseProjectInterfaceDataTypes = { + /** Gets the "raw" data in USX form for the specified verse */ + VerseUSX: DataProviderDataType; + }; + + /** Provides Scripture data in USJ format by book */ + export type USJBookProjectInterfaceDataTypes = { + /** + * Gets/sets the data in USJ form for the specified book + * + * WARNING: USJ is in very early stages of proposal, so it will likely change over time. + */ + BookUSJ: DataProviderDataType; + }; + /** Provides Scripture data in USJ format by chapter */ export type USJChapterProjectInterfaceDataTypes = { /** - * Gets the tokenized USJ data for the specified chapter + * Gets/sets the data in USJ form for the specified chapter * * WARNING: USJ is in very early stages of proposal, so it will likely change over time. */ ChapterUSJ: DataProviderDataType; }; + /** Provides Scripture data in USJ format by chapter */ + export type USJVerseProjectInterfaceDataTypes = { + /** + * Gets the data in USJ form for the specified verse + * + * WARNING: USJ is in very early stages of proposal, so it will likely change over time. + */ + VerseUSJ: DataProviderDataType; + }; + /** * Provides project data for Scripture projects. * @@ -43,24 +85,7 @@ declare module 'platform-scripture' { * them. Following are a number of others that may be implemented at some point. This is not yet a * complete list of the data types available from Scripture projects. */ - type UnfinishedScriptureProjectDataTypes = { - /** - * Gets the tokenized USJ data for the specified book - * - * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change - * over time. Additionally, USJ is in very early stages of proposal, so it will likely also - * change over time. - */ - BookUSJ: DataProviderDataType; - /** - * Gets the tokenized USJ data for the specified verse - * - * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change - * over time. Additionally, USJ is in very early stages of proposal, so it will likely also - * change over time. - */ - VerseUSJ: DataProviderDataType; - }; + type UnfinishedScriptureProjectDataTypes = {}; /** * Provides project data for Scripture projects. @@ -69,136 +94,65 @@ declare module 'platform-scripture' { * them. Following are a number of others that may be implemented at some point. This is not yet a * complete list of the data types available from Scripture projects. */ - type UnfinishedProjectDataProviderExpanded = - IProjectDataProvider & { - /** - * Gets the tokenized USJ data for the specified book - * - * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change - * over time. Additionally, USJ is in very early stages of proposal, so it will likely also - * change over time. - */ - getBookUSJ(verseRef: VerseRef): Promise; - /** - * Sets the tokenized USJ data for the specified book - * - * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change - * over time. Additionally, USJ is in very early stages of proposal, so it will likely also - * change over time. - */ - setBookUSJ( - verseRef: VerseRef, - usj: Usj, - ): Promise>; - /** - * Subscribe to run a callback function when the tokenized USJ data is changed - * - * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change - * over time. Additionally, USJ is in very early stages of proposal, so it will likely also - * change over time. - * - * @param verseRef Tells the provider what changes to listen for - * @param callback Function to run with the updated USJ for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber function (run to unsubscribe from listening for updates) - */ - subscribeBookUSJ( - verseRef: VerseRef, - callback: (usj: Usj | undefined) => void, - options?: DataProviderSubscriberOptions, - ): Promise; + type UnfinishedProjectDataProviderExpanded = { + /** + * Gets an extension's serialized project data (so the extension can provide and manipulate its + * project data) + * + * @example `{ extensionName: 'biblicalTerms', dataQualifier: 'renderings' }` + * + * @param dataScope Contains the name of the extension requesting the data and which data it is + * requesting + * @returns Promise that resolves to the requested extension project data + */ + getExtensionData(dataScope: ExtensionDataScope): Promise; + /** + * Sets an extension's serialized project data (so the extension can provide and manipulate its + * project data) + * + * @example `{ extensionName: 'biblicalTerms', dataQualifier: 'renderings' }` + * + * @param dataScope Contains the name of the extension requesting the data and which data it is + * requesting + * @param extensionData The new project data for this extension + * @returns Promise that resolves indicating which data types received updates + */ + setExtensionData( + dataScope: ExtensionDataScope, + extensionData: string | undefined, + ): Promise>; + /** + * Subscribe to run a callback function when an extension's serialized project data is changed + * + * @example `{ extensionName: 'biblicalTerms', dataQualifier: 'renderings' }` + * + * @param dataScope Contains the name of the extension requesting the data and which data it is + * requesting + * @param callback Function to run with the updated extension data for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) + */ + subscribeExtensionData( + dataScope: ExtensionDataScope, + callback: (extensionData: string | undefined) => void, + options?: DataProviderSubscriberOptions, + ): Promise; + }; - /** - * Gets the tokenized USJ data for the specified verse - * - * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change - * over time. Additionally, USJ is in very early stages of proposal, so it will likely also - * change over time. - */ - getVerseUSJ(verseRef: VerseRef): Promise; - /** - * Sets the tokenized USJ data for the specified verse - * - * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change - * over time. Additionally, USJ is in very early stages of proposal, so it will likely also - * change over time. - */ - setVerseUSJ( - verseRef: VerseRef, - usj: Usj, - ): Promise>; - /** - * Subscribe to run a callback function when the tokenized USJ data is changed - * - * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change - * over time. Additionally, USJ is in very early stages of proposal, so it will likely also - * change over time. - * - * @param verseRef Tells the provider what changes to listen for - * @param callback Function to run with the updated USJ for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber function (run to unsubscribe from listening for updates) - */ - subscribeVerseUSJ( - verseRef: VerseRef, - callback: (usj: Usj | undefined) => void, - options?: DataProviderSubscriberOptions, - ): Promise; + // #endregion - /** - * Gets an extension's serialized project data (so the extension can provide and manipulate - * its project data) - * - * @example `{ extensionName: 'biblicalTerms', dataQualifier: 'renderings' }` - * - * @param dataScope Contains the name of the extension requesting the data and which data it - * is requesting - * @returns Promise that resolves to the requested extension project data - */ - getExtensionData(dataScope: ExtensionDataScope): Promise; - /** - * Sets an extension's serialized project data (so the extension can provide and manipulate - * its project data) - * - * @example `{ extensionName: 'biblicalTerms', dataQualifier: 'renderings' }` - * - * @param dataScope Contains the name of the extension requesting the data and which data it - * is requesting - * @param extensionData The new project data for this extension - * @returns Promise that resolves indicating which data types received updates - */ - setExtensionData( - dataScope: ExtensionDataScope, - extensionData: string | undefined, - ): Promise>; - /** - * Subscribe to run a callback function when an extension's serialized project data is changed - * - * @example `{ extensionName: 'biblicalTerms', dataQualifier: 'renderings' }` - * - * @param dataScope Contains the name of the extension requesting the data and which data it - * is requesting - * @param callback Function to run with the updated extension data for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber function (run to unsubscribe from listening for updates) - */ - subscribeExtensionData( - dataScope: ExtensionDataScope, - callback: (extensionData: string | undefined) => void, - options?: DataProviderSubscriberOptions, - ): Promise; - }; + // #region Project Data Provider Types - /** Provides Scripture data in USFM format by book, chapter, or verse */ - export type IUSFMBookChapterVerseProjectDataProvider = - IProjectDataProvider & { + /** Provides Scripture data in USFM format by book */ + export type IUSFMBookProjectDataProvider = + IProjectDataProvider & { /** Gets the "raw" USFM data for the specified book */ getBookUSFM(verseRef: VerseRef): Promise; /** Sets the "raw" USFM data for the specified book */ setBookUSFM( verseRef: VerseRef, usfm: string, - ): Promise>; + ): Promise>; /** * Subscribe to run a callback function when the "raw" USFM data is changed * @@ -212,14 +166,18 @@ declare module 'platform-scripture' { callback: (usfm: string | undefined) => void, options?: DataProviderSubscriberOptions, ): Promise; + }; + /** Provides Scripture data in USFM format by chapter */ + export type IUSFMChapterProjectDataProvider = + IProjectDataProvider & { /** Gets the "raw" USFM data for the specified chapter */ getChapterUSFM(verseRef: VerseRef): Promise; /** Sets the "raw" USFM data for the specified chapter */ setChapterUSFM( verseRef: VerseRef, usfm: string, - ): Promise>; + ): Promise>; /** * Subscribe to run a callback function when the "raw" USFM data is changed * @@ -233,14 +191,18 @@ declare module 'platform-scripture' { callback: (usfm: string | undefined) => void, options?: DataProviderSubscriberOptions, ): Promise; + }; + /** Provides Scripture data in USFM format by verse */ + export type IUSFMVerseProjectDataProvider = + IProjectDataProvider & { /** Gets the "raw" USFM data for the specified verse */ getVerseUSFM(verseRef: VerseRef): Promise; /** Sets the "raw" USFM data for the specified verse */ setVerseUSFM( verseRef: VerseRef, usfm: string, - ): Promise>; + ): Promise>; /** * Subscribe to run a callback function when the "raw" USFM data is changed * @@ -256,6 +218,31 @@ declare module 'platform-scripture' { ): Promise; }; + /** Provides Scripture data in USX format by book */ + export type IUSXBookProjectDataProvider = + IProjectDataProvider & { + /** Gets the "raw" USX data for the specified book */ + getBookUSX(verseRef: VerseRef): Promise; + /** Sets the "raw" USX data for the specified book */ + setBookUSX( + verseRef: VerseRef, + usx: string, + ): Promise>; + /** + * Subscribe to run a callback function when the "raw" USX data is changed + * + * @param verseRef Tells the provider what changes to listen for + * @param callback Function to run with the updated USX for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) + */ + subscribeBookUSX( + verseRef: VerseRef, + callback: (usx: string | undefined) => void, + options?: DataProviderSubscriberOptions, + ): Promise; + }; + /** Provides Scripture data in USX format by chapter */ export type IUSXChapterProjectDataProvider = IProjectDataProvider & { @@ -281,6 +268,72 @@ declare module 'platform-scripture' { ): Promise; }; + /** Provides Scripture data in USX format by verse */ + export type IUSXVerseProjectDataProvider = + IProjectDataProvider & { + /** Gets the "raw" USX data for the specified verse */ + getVerseUSX(verseRef: VerseRef): Promise; + /** Sets the "raw" USX data for the specified verse */ + setVerseUSX( + verseRef: VerseRef, + usx: string, + ): Promise>; + /** + * Subscribe to run a callback function when the "raw" USX data is changed + * + * @param verseRef Tells the provider what changes to listen for + * @param callback Function to run with the updated USX for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) + */ + subscribeVerseUSX( + verseRef: VerseRef, + callback: (usx: string | undefined) => void, + options?: DataProviderSubscriberOptions, + ): Promise; + }; + + /** Provides Scripture data in USJ format by book */ + export type IUSJBookProjectDataProvider = + IProjectDataProvider & { + /** + * Gets the tokenized USJ data for the specified book + * + * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change + * over time. Additionally, USJ is in very early stages of proposal, so it will likely also + * change over time. + */ + getBookUSJ(verseRef: VerseRef): Promise; + /** + * Sets the tokenized USJ data for the specified book + * + * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change + * over time. Additionally, USJ is in very early stages of proposal, so it will likely also + * change over time. + */ + setBookUSJ( + verseRef: VerseRef, + usj: Usj, + ): Promise>; + /** + * Subscribe to run a callback function when the tokenized USJ data is changed + * + * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change + * over time. Additionally, USJ is in very early stages of proposal, so it will likely also + * change over time. + * + * @param verseRef Tells the provider what changes to listen for + * @param callback Function to run with the updated USJ for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) + */ + subscribeBookUSJ( + verseRef: VerseRef, + callback: (usj: Usj | undefined) => void, + options?: DataProviderSubscriberOptions, + ): Promise; + }; + /** Provides Scripture data in USJ format by chapter */ export type IUSJChapterProjectDataProvider = IProjectDataProvider & { @@ -316,20 +369,73 @@ declare module 'platform-scripture' { ): Promise; }; - // #region USJ types + /** Provides Scripture data in USJ format by verse */ + export type IUSJVerseProjectDataProvider = + IProjectDataProvider & { + /** + * Gets the tokenized USJ data for the specified verse + * + * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change + * over time. Additionally, USJ is in very early stages of proposal, so it will likely also + * change over time. + */ + getVerseUSJ(verseRef: VerseRef): Promise; + /** + * Sets the tokenized USJ data for the specified verse + * + * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change + * over time. Additionally, USJ is in very early stages of proposal, so it will likely also + * change over time. + */ + setVerseUSJ( + verseRef: VerseRef, + usj: Usj, + ): Promise>; + /** + * Subscribe to run a callback function when the tokenized USJ data is changed + * + * WARNING: USJ is one of many possible tokenized formats that we may use, so this may change + * over time. Additionally, USJ is in very early stages of proposal, so it will likely also + * change over time. + * + * @param verseRef Tells the provider what changes to listen for + * @param callback Function to run with the updated USJ for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) + */ + subscribeVerseUSJ( + verseRef: VerseRef, + callback: (usj: Usj | undefined) => void, + options?: DataProviderSubscriberOptions, + ): Promise; + }; + + // #endregion } declare module 'papi-shared-types' { import type { - IUSFMBookChapterVerseProjectDataProvider, + IUSFMBookProjectDataProvider, + IUSFMChapterProjectDataProvider, + IUSFMVerseProjectDataProvider, + IUSXBookProjectDataProvider, IUSXChapterProjectDataProvider, + IUSXVerseProjectDataProvider, + IUSJBookProjectDataProvider, IUSJChapterProjectDataProvider, + IUSJVerseProjectDataProvider, } from 'platform-scripture'; export interface ProjectDataProviderInterfaces { - 'platformScripture.USFM_BookChapterVerse': IUSFMBookChapterVerseProjectDataProvider; + 'platformScripture.USFM_Book': IUSFMBookProjectDataProvider; + 'platformScripture.USFM_Chapter': IUSFMChapterProjectDataProvider; + 'platformScripture.USFM_Verse': IUSFMVerseProjectDataProvider; + 'platformScripture.USX_Book': IUSXBookProjectDataProvider; 'platformScripture.USX_Chapter': IUSXChapterProjectDataProvider; + 'platformScripture.USX_Verse': IUSXVerseProjectDataProvider; + 'platformScripture.USJ_Book': IUSJBookProjectDataProvider; 'platformScripture.USJ_Chapter': IUSJChapterProjectDataProvider; + 'platformScripture.USJ_Verse': IUSJVerseProjectDataProvider; } export interface CommandHandlers { diff --git a/extensions/src/quick-verse/src/main.ts b/extensions/src/quick-verse/src/main.ts index 58bdba8a09..daa5fa4359 100644 --- a/extensions/src/quick-verse/src/main.ts +++ b/extensions/src/quick-verse/src/main.ts @@ -68,7 +68,7 @@ class QuickVerseDataProviderEngine latestVerseRef = 'JHN 11:35'; usfmDataProviderPromise = papi.projectDataProviders.get( - 'platformScripture.USFM_BookChapterVerse', + 'platformScripture.USFM_Verse', '32664dc3288a28df2e2bb75ded887fc8f17a15fb', ); diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 347ce4016f..aaa7816da7 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -4152,7 +4152,7 @@ declare module 'shared/services/project-data-provider.service' { * @example * * ```typescript - * const pdp = await get('platformScripture.USFM_BookChapterVerse', 'ProjectID12345'); + * const pdp = await get('platformScripture.USFM_Verse', 'ProjectID12345'); * pdp.getVerse(new VerseRef('JHN', '1', '1')); * ``` * @@ -6096,8 +6096,7 @@ declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { /** * React hook to use data from a Project Data Provider * - * @example `useProjectData('platformScripture.USFM_BookChapterVerse', 'project - * id').VerseUSFM(...);` + * @example `useProjectData('platformScripture.USFM_Verse', 'project id').VerseUSFM(...);` */ type UseProjectDataHook = { ( @@ -6158,13 +6157,12 @@ declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { * Provider with `useProjectData('', '').` and use like any * other React hook. * - * _@example_ Subscribing to Verse USFM info at JHN 11:35 on a - * `platformScripture.USFM_BookChapterVerse` project with projectId - * `32664dc3288a28df2e2bb75ded887fc8f17a15fb`: + * _@example_ Subscribing to Verse USFM info at JHN 11:35 on a `platformScripture.USFM_Verse` + * project with projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`: * * ```typescript * const [verse, setVerse, verseIsLoading] = useProjectData( - * 'platformScripture.USFM_BookChapterVerse', + * 'platformScripture.USFM_Verse', * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', * ).VerseUSFM( * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), diff --git a/src/extension-host/services/extension.service.ts b/src/extension-host/services/extension.service.ts index c0d9cbac1c..ad569946e5 100644 --- a/src/extension-host/services/extension.service.ts +++ b/src/extension-host/services/extension.service.ts @@ -35,7 +35,7 @@ import menuDataService from '@shared/services/menu-data.service'; import { localizedStringsDocumentCombiner } from '@extension-host/services/localization.service-host'; import { settingsDocumentCombiner } from '@extension-host/services/settings.service-host'; import { PLATFORM_NAMESPACE } from '@shared/data/platform.data'; -import { projectSettingsDocumentCombiner } from './project-settings.service-host'; +import { projectSettingsDocumentCombiner } from '@extension-host/services/project-settings.service-host'; /** * The way to use `require` directly - provided by webpack because they overwrite normal `require`. diff --git a/src/renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component.tsx b/src/renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component.tsx index dc21df6707..b9bb0d6646 100644 --- a/src/renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component.tsx +++ b/src/renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component.tsx @@ -170,10 +170,7 @@ export default function RunBasicChecksTab({ currentProjectId }: RunBasicChecksTa ); }; - const project = useProjectDataProvider( - 'platformScripture.USFM_BookChapterVerse', - currentProjectId, - ); + const project = useProjectDataProvider('platformScripture.USFM_Verse', currentProjectId); const [projectString] = usePromise( useMemo(() => { diff --git a/src/renderer/hooks/papi-hooks/use-project-data.hook.ts b/src/renderer/hooks/papi-hooks/use-project-data.hook.ts index c5ed872407..f32648db2e 100644 --- a/src/renderer/hooks/papi-hooks/use-project-data.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-project-data.hook.ts @@ -14,8 +14,7 @@ import { /** * React hook to use data from a Project Data Provider * - * @example `useProjectData('platformScripture.USFM_BookChapterVerse', 'project - * id').VerseUSFM(...);` + * @example `useProjectData('platformScripture.USFM_Verse', 'project id').VerseUSFM(...);` */ type UseProjectDataHook = { ( @@ -75,13 +74,12 @@ type UseProjectDataHook = { * Provider with `useProjectData('', '').` and use like any * other React hook. * - * _@example_ Subscribing to Verse USFM info at JHN 11:35 on a - * `platformScripture.USFM_BookChapterVerse` project with projectId - * `32664dc3288a28df2e2bb75ded887fc8f17a15fb`: + * _@example_ Subscribing to Verse USFM info at JHN 11:35 on a `platformScripture.USFM_Verse` + * project with projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`: * * ```typescript * const [verse, setVerse, verseIsLoading] = useProjectData( - * 'platformScripture.USFM_BookChapterVerse', + * 'platformScripture.USFM_Verse', * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', * ).VerseUSFM( * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), diff --git a/src/shared/services/project-data-provider.service.ts b/src/shared/services/project-data-provider.service.ts index 7664c07da1..f5a13a58dc 100644 --- a/src/shared/services/project-data-provider.service.ts +++ b/src/shared/services/project-data-provider.service.ts @@ -148,7 +148,7 @@ export async function registerProjectDataProviderEngineFactory< * @example * * ```typescript - * const pdp = await get('platformScripture.USFM_BookChapterVerse', 'ProjectID12345'); + * const pdp = await get('platformScripture.USFM_Verse', 'ProjectID12345'); * pdp.getVerse(new VerseRef('JHN', '1', '1')); * ``` * diff --git a/src/shared/services/project-lookup.service.test.ts b/src/shared/services/project-lookup.service.test.ts index ce9687e77d..8c12772d2e 100644 --- a/src/shared/services/project-lookup.service.test.ts +++ b/src/shared/services/project-lookup.service.test.ts @@ -57,9 +57,7 @@ describe('Getting project metadata with Layering PDPs', () => { [getPDPFactoryNetworkObjectNameFromId('layer-2')]: ['platformScripture.USX_Chapter'], [getPDPFactoryNetworkObjectNameFromId('layer-3')]: ['platformScripture.USX_Chapter'], [getPDPFactoryNetworkObjectNameFromId('meta-layer-3')]: ['platform.notesOnly'], - [getPDPFactoryNetworkObjectNameFromId('non-layer')]: [ - 'platformScripture.USFM_BookChapterVerse', - ], + [getPDPFactoryNetworkObjectNameFromId('non-layer')]: ['platformScripture.USFM_Book'], }; function getIncludedPdpfIds(pdpfIds: string[]) { return pdpfIds.filter( @@ -355,11 +353,11 @@ describe('Metadata generation:', () => { const expectedTestProjectInterfaces: ProjectInterfaces[] = [ 'platform.placeholder', 'platform.notesOnly', - 'platformScripture.USFM_BookChapterVerse', + 'platformScripture.USFM_Book', 'helloWorld', ]; const expectedTest2ProjectInterfaces: ProjectInterfaces[] = [ - 'platformScripture.USFM_BookChapterVerse', + 'platformScripture.USFM_Book', 'platform.notesOnly', ]; const test2ProjectId = 'test-2-project'; @@ -403,7 +401,7 @@ describe('Metadata generation:', () => { }, { id: test2ProjectId, - projectInterfaces: ['platformScripture.USFM_BookChapterVerse', 'platform.notesOnly'], + projectInterfaces: ['platformScripture.USFM_Book', 'platform.notesOnly'], }, ]; }, @@ -416,7 +414,7 @@ describe('Metadata generation:', () => { projectInterfaces: [ 'platform.placeholder', 'platform.notesOnly', - 'platformScripture.USFM_BookChapterVerse', + 'platformScripture.USFM_Book', ], }, ]; @@ -485,7 +483,7 @@ describe('Metadata generation:', () => { expect(pdpfInfoValuesSorted[3].projectInterfaces).toEqual([ 'platform.placeholder', 'platform.notesOnly', - 'platformScripture.USFM_BookChapterVerse', + 'platformScripture.USFM_Book', ]); }); }); @@ -644,7 +642,7 @@ describe('Metadata generation:', () => { projectInterfaces: [ 'platform.placeholder', 'platform.notesOnly', - 'platformScripture.USFM_BookChapterVerse', + 'platformScripture.USFM_Book', ], }, 'test-0': { projectInterfaces: ['platform.placeholder'] }, @@ -704,7 +702,7 @@ describe('Metadata generation:', () => { // `projectInterface`s should be just the `projectInterface`s from this pdpf const expectedTest2ProjectInterfacesOnePDPF = [ - 'platformScripture.USFM_BookChapterVerse', + 'platformScripture.USFM_Book', 'platform.notesOnly', ]; expect(test2ProjectMetadata.projectInterfaces.length).toEqual( @@ -717,7 +715,7 @@ describe('Metadata generation:', () => { // Each entry in pdpfInfo should have only the `projectInterface`s provided by this pdpf expect(test2ProjectMetadata.pdpFactoryInfo).toEqual({ 'test-1': { - projectInterfaces: ['platformScripture.USFM_BookChapterVerse', 'platform.notesOnly'], + projectInterfaces: ['platformScripture.USFM_Book', 'platform.notesOnly'], }, }); }); @@ -793,12 +791,12 @@ describe('filterProjectsMetadata', () => { }, { id: 'asdfg', - projectInterfaces: ['platformScripture.USFM_BookChapterVerse', 'platform.notesOnly'], + projectInterfaces: ['platformScripture.USFM_Book', 'platform.notesOnly'], pdpFactoryInfo: { test2: { - projectInterfaces: ['platformScripture.USFM_BookChapterVerse', 'platform.notesOnly'], + projectInterfaces: ['platformScripture.USFM_Book', 'platform.notesOnly'], }, - test4: { projectInterfaces: ['platformScripture.USFM_BookChapterVerse'] }, + test4: { projectInterfaces: ['platformScripture.USFM_Book'] }, }, }, { @@ -842,10 +840,7 @@ describe('filterProjectsMetadata', () => { options = { excludeProjectIds: ['asdf', 'asdfg'], - includeProjectInterfaces: [ - '^platformScripture\\.USFM_BookChapterVerse$', - '^platform\\.placeholder$', - ], + includeProjectInterfaces: ['^platformScripture\\.USFM_Book$', '^platform\\.placeholder$'], }; filteredMetadata = projectLookupService.filterProjectsMetadata(projectsMetadata, options); @@ -879,10 +874,7 @@ describe('filterProjectsMetadata', () => { options = { includeProjectIds: ['asdf', 'asdfg', 'asdfgh'], excludeProjectIds: 'asdfg', - includeProjectInterfaces: [ - '^platformScripture\\.USFM_BookChapterVerse$', - '^platform\\.placeholder$', - ], + includeProjectInterfaces: ['^platformScripture\\.USFM_Book$', '^platform\\.placeholder$'], }; filteredMetadata = projectLookupService.filterProjectsMetadata(projectsMetadata, options); @@ -914,7 +906,7 @@ describe('filterProjectsMetadata', () => { // Multiple OR'ed RegExps that match two project interfaces options = { - excludeProjectInterfaces: ['^helloWorld$', '^platformScripture\\.USFM_BookChapterVerse$'], + excludeProjectInterfaces: ['^helloWorld$', '^platformScripture\\.USFM_Book$'], }; filteredMetadata = projectLookupService.filterProjectsMetadata(projectsMetadata, options); @@ -955,10 +947,7 @@ describe('filterProjectsMetadata', () => { options = { excludeProjectInterfaces: 'USFM', - includeProjectInterfaces: [ - '^platformScripture\\.USFM_BookChapterVerse$', - '^platform\\.placeholder$', - ], + includeProjectInterfaces: ['^platformScripture\\.USFM_Book$', '^platform\\.placeholder$'], }; filteredMetadata = projectLookupService.filterProjectsMetadata(projectsMetadata, options); @@ -990,7 +979,7 @@ describe('filterProjectsMetadata', () => { // Multiple RegExps that match two project interfaces options = { - includeProjectInterfaces: ['^helloWorld$', '^platformScripture\\.USFM_BookChapterVerse$'], + includeProjectInterfaces: ['^helloWorld$', '^platformScripture\\.USFM_Book$'], }; filteredMetadata = projectLookupService.filterProjectsMetadata(projectsMetadata, options); @@ -1031,10 +1020,7 @@ describe('filterProjectsMetadata', () => { options = { excludeProjectInterfaces: 'USFM', - includeProjectInterfaces: [ - '^platformScripture\\.USFM_BookChapterVerse$', - '^platform\\.placeholder$', - ], + includeProjectInterfaces: ['^platformScripture\\.USFM_Book$', '^platform\\.placeholder$'], }; filteredMetadata = projectLookupService.filterProjectsMetadata(projectsMetadata, options);