diff --git a/.gitignore b/.gitignore index d5e87be21a..f76c5c2282 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ launchSettings.json # Test development user appdata files dev-appdata/ +c-sharp/**/*.BAK diff --git a/c-sharp/NetworkObjects/UsfmDataProvider.cs b/c-sharp/NetworkObjects/UsfmDataProvider.cs index 9b2f2cdaf9..4adfe82be4 100644 --- a/c-sharp/NetworkObjects/UsfmDataProvider.cs +++ b/c-sharp/NetworkObjects/UsfmDataProvider.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Xml; +using System.Xml.XPath; using Paranext.DataProvider.JsonUtils; using Paranext.DataProvider.MessageHandlers; using Paranext.DataProvider.MessageTransports; @@ -42,8 +43,9 @@ protected override ResponseToRequest HandleRequest(string functionName, JsonArra { "getBookNames" => GetBookNames(), "getChapter" => GetChapter(args[0]!.ToJsonString()), - "getChapterUsx" => GetChapterUsx(args[0]!.ToJsonString()), - "getBookUsx" => GetBookUsx(args[0]!.ToJsonString()), + "getChapterUsx" => GetUsx(args[0]!.ToJsonString()), + "setChapterUsx" => SetUsx(args[0]!.ToJsonString(), args[1]!.ToString()), + "getBookUsx" => GetUsx(args[0]!.ToJsonString()), "getVerse" => GetVerse(args[0]!.ToJsonString()), _ => ResponseToRequest.Failed($"Unexpected function: {functionName}") }; @@ -67,17 +69,17 @@ private ResponseToRequest GetChapter(string args) : ResponseToRequest.Failed(errorMsg); } - private ResponseToRequest GetChapterUsx(string args) + private ResponseToRequest GetUsx(string args) { return VerseRefConverter.TryCreateVerseRef(args, out var verseRef, out string errorMsg) - ? ResponseToRequest.Succeeded(GetUsxForChapter(verseRef)) + ? ResponseToRequest.Succeeded(GetChapterOrBookUsx(verseRef)) : ResponseToRequest.Failed(errorMsg); } - private ResponseToRequest GetBookUsx(string args) + private ResponseToRequest SetUsx(string argVref, string argNewUsx) { - return VerseRefConverter.TryCreateVerseRef(args, out var verseRef, out string errorMsg) - ? ResponseToRequest.Succeeded(GetUsxForBook(verseRef)) + return VerseRefConverter.TryCreateVerseRef(argVref, out var verseRef, out string errorMsg) + ? SetChapterOrBookUsx(verseRef, argNewUsx) : ResponseToRequest.Failed(errorMsg); } @@ -88,18 +90,39 @@ private ResponseToRequest GetVerse(string args) : ResponseToRequest.Failed(errorMsg); } - private string GetUsxForChapter(VerseRef vref) + private string GetChapterOrBookUsx(VerseRef vref) { XmlDocument usx = ConvertUsfmToUsx(GetUsfm(vref.BookNum, vref.ChapterNum), vref.BookNum); string contents = usx.OuterXml ?? string.Empty; return contents; } - private string GetUsxForBook(VerseRef vref) + public ResponseToRequest SetChapterOrBookUsx(VerseRef vref, string newUsx) { - XmlDocument usx = ConvertUsfmToUsx(GetUsfm(vref.BookNum), vref.BookNum); - string contents = usx.OuterXml ?? string.Empty; - return contents; + try + { + XmlDocument doc = new() { PreserveWhitespace = true }; + doc.LoadXml(newUsx); + if (doc.FirstChild?.Name != "usx") + return ResponseToRequest.Failed("Invalid USX"); + + UsxFragmenter.FindFragments( + _scrText!.ScrStylesheet(vref.BookNum), + doc.CreateNavigator(), + XPathExpression.Compile("*[false()]"), + out string usfm + ); + + usfm = UsfmToken.NormalizeUsfm(_scrText, vref.BookNum, usfm); + _scrText.PutText(vref.BookNum, vref.ChapterNum, false, usfm, null); + SendDataUpdateEvent("*"); + } + catch (Exception e) + { + return ResponseToRequest.Failed(e.Message); + } + + return ResponseToRequest.Succeeded(); } /// diff --git a/c-sharp/ParatextUtils/ParatextGlobals.cs b/c-sharp/ParatextUtils/ParatextGlobals.cs index 1a93744631..240cba4342 100644 --- a/c-sharp/ParatextUtils/ParatextGlobals.cs +++ b/c-sharp/ParatextUtils/ParatextGlobals.cs @@ -34,6 +34,7 @@ public static void Initialize(string dataFolderPath) ICUDllLocator.Initialize(false, false); // Now tell Paratext.Data to use the specified folder + dataFolderPath = Path.GetFullPath(dataFolderPath); // Make sure path is rooted ParatextData.Initialize(dataFolderPath, false); s_initialized = true; } diff --git a/c-sharp/Projects/ParatextProjectDataProvider.cs b/c-sharp/Projects/ParatextProjectDataProvider.cs index a50335e4e2..ac3dc94f8a 100644 --- a/c-sharp/Projects/ParatextProjectDataProvider.cs +++ b/c-sharp/Projects/ParatextProjectDataProvider.cs @@ -20,6 +20,9 @@ ProjectDetails projectDetails Getters.Add("getChapterUSFM", GetChapterUSFM); Setters.Add("setChapterUSFM", SetChapterUSFM); Getters.Add("getVerseUSFM", GetVerseUSFM); + + Getters.Add("getChapterUSX", GetChapterUSX); + Setters.Add("setChapterUSX", SetChapterUSX); } protected override Task StartDataProvider() @@ -61,6 +64,7 @@ private ResponseToRequest Set(string dataType, string dataQualifier, string data return _paratextPsi.SetProjectData(scope, data); } + #region USFM handling methods private ResponseToRequest GetBookUSFM(string jsonString) { return Get(ParatextProjectStorageInterpreter.BookUSFM, jsonString); @@ -80,4 +84,17 @@ private ResponseToRequest SetChapterUSFM(string dataQualifier, string data) { return Set(ParatextProjectStorageInterpreter.ChapterUSFM, dataQualifier, data); } + #endregion + + #region USX handling methods + private ResponseToRequest GetChapterUSX(string jsonString) + { + return Get(ParatextProjectStorageInterpreter.ChapterUSX, jsonString); + } + + private ResponseToRequest SetChapterUSX(string dataQualifier, string data) + { + return Set(ParatextProjectStorageInterpreter.ChapterUSX, dataQualifier, data); + } + #endregion } diff --git a/c-sharp/Projects/ParatextProjectStorageInterpreter.cs b/c-sharp/Projects/ParatextProjectStorageInterpreter.cs index 07a7204e06..3af48cf0ae 100644 --- a/c-sharp/Projects/ParatextProjectStorageInterpreter.cs +++ b/c-sharp/Projects/ParatextProjectStorageInterpreter.cs @@ -1,9 +1,13 @@ using System.Text; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; using Newtonsoft.Json; using Paranext.DataProvider.JsonUtils; using Paranext.DataProvider.MessageHandlers; using Paranext.DataProvider.MessageTransports; using Paratext.Data; +using SIL.Scripture; namespace Paranext.DataProvider.Projects; @@ -12,6 +16,7 @@ internal class ParatextProjectStorageInterpreter : ProjectStorageInterpreter public const string BookUSFM = "BookUSFM"; public const string ChapterUSFM = "ChapterUSFM"; public const string VerseUSFM = "VerseUSFM"; + public const string ChapterUSX = "ChapterUSX"; public ParatextProjectStorageInterpreter(PapiClient papiClient) : base(ProjectStorageType.ParatextFolders, new[] { ProjectType.Paratext }, papiClient) { } @@ -88,16 +93,20 @@ public override ResponseToRequest GetProjectData(ProjectDataScope scope) { BookUSFM => string.IsNullOrEmpty(error) - ? ResponseToRequest.Succeeded(scrText.GetText(verseRef, false, false)) + ? ResponseToRequest.Succeeded(scrText.GetText(verseRef, false, true)) : ResponseToRequest.Failed(error), ChapterUSFM => string.IsNullOrEmpty(error) - ? ResponseToRequest.Succeeded(scrText.GetText(verseRef, true, false)) + ? ResponseToRequest.Succeeded(scrText.GetText(verseRef, true, true)) : ResponseToRequest.Failed(error), VerseUSFM => string.IsNullOrEmpty(error) ? ResponseToRequest.Succeeded(scrText.Parser.GetVerseUsfmText(verseRef)) : ResponseToRequest.Failed(error), + ChapterUSX + => string.IsNullOrEmpty(error) + ? ResponseToRequest.Succeeded(GetChapterUsx(scrText, verseRef)) + : ResponseToRequest.Failed(error), _ => ResponseToRequest.Failed($"Unknown data type: {scope.DataType}") }; } @@ -128,13 +137,22 @@ public override ResponseToRequest SetProjectData(ProjectDataScope scope, string verseRef.BookNum, verseRef.ChapterNum, false, - data, + data.ToString(), writeLock ); } ); // The value of returned string is case sensitive and cannot change unless data provider subscriptions change return ResponseToRequest.Succeeded(ChapterUSFM); + case ChapterUSX: + if (!string.IsNullOrEmpty(error)) + return ResponseToRequest.Failed(error); + ResponseToRequest? response = null; + RunWithinLock( + WriteScope.ProjectText(scrText, verseRef.BookNum, verseRef.ChapterNum), + writeLock => response = SetChapterUsx(scrText, verseRef, data, writeLock) + ); + return response ?? ResponseToRequest.Failed("Unknown error occurred"); default: return ResponseToRequest.Failed($"Unknown data type: {scope.DataType}"); } @@ -204,6 +222,66 @@ public override ResponseToRequest SetExtensionData(ProjectDataScope scope, strin ); } + private string GetChapterUsx(ScrText scrText, VerseRef vref) + { + XmlDocument usx = ConvertUsfmToUsx( + scrText, + scrText.GetText(vref, true, true), + vref.BookNum + ); + return usx.OuterXml ?? string.Empty; + } + + private ResponseToRequest SetChapterUsx( + ScrText scrText, + VerseRef vref, + string newUsx, + WriteLock writeLock + ) + { + try + { + XDocument doc; + using (TextReader reader = new StringReader(newUsx)) + doc = XDocument.Load(reader, LoadOptions.PreserveWhitespace); + + if (doc.Root?.Name != "usx") + return ResponseToRequest.Failed("Invalid USX"); + + UsxFragmenter.FindFragments( + scrText.ScrStylesheet(vref.BookNum), + doc.CreateNavigator(), + XPathExpression.Compile("*[false()]"), + out string usfm + ); + + usfm = UsfmToken.NormalizeUsfm(scrText, vref.BookNum, usfm); + scrText.PutText(vref.BookNum, vref.ChapterNum, true, usfm, writeLock); + } + catch (Exception e) + { + return ResponseToRequest.Failed(e.ToString()); + } + + return ResponseToRequest.Succeeded(ChapterUSX); + } + + private XmlDocument ConvertUsfmToUsx(ScrText scrText, string usfm, int bookNum) + { + ScrStylesheet scrStylesheet = scrText.ScrStylesheet(bookNum); + // Tokenize usfm + List tokens = UsfmToken.Tokenize(scrStylesheet, usfm ?? string.Empty, true); + + XmlDocument doc = new XmlDocument(); + using (XmlWriter xmlw = doc.CreateNavigator()!.AppendChild()) + { + // Convert to XML + UsfmToUsx.ConvertToXmlWriter(scrStylesheet, tokens, xmlw, false); + xmlw.Flush(); + } + return doc; + } + private static void RunWithinLock(WriteScope writeScope, Action action) { var myLock = diff --git a/c-sharp/Projects/ProjectStorageInterpreter.cs b/c-sharp/Projects/ProjectStorageInterpreter.cs index 8c7d109e43..85d811fb22 100644 --- a/c-sharp/Projects/ProjectStorageInterpreter.cs +++ b/c-sharp/Projects/ProjectStorageInterpreter.cs @@ -55,13 +55,13 @@ out string errorMessage case "getProjectData": return GetProjectData(dataScope); case "setProjectData": - var setProjectReturn = SetProjectData(dataScope, args[1]!.ToJsonString()); + var setProjectReturn = SetProjectData(dataScope, args[1]!.ToString()); SendDataUpdateEvent(setProjectReturn.Contents); return setProjectReturn; case "getExtensionData": return GetExtensionData(dataScope); case "setExtensionData": - var setExtensionReturn = SetExtensionData(dataScope, args[1]!.ToJsonString()); + var setExtensionReturn = SetExtensionData(dataScope, args[1]!.ToString()); SendDataUpdateEvent(setExtensionReturn.Contents); return setExtensionReturn; default: @@ -106,7 +106,7 @@ private ResponseToRequest GetSupportedTypes() /// /// Set data in a project identified by . /// - public abstract ResponseToRequest SetProjectData(ProjectDataScope scope, string jsonData); + public abstract ResponseToRequest SetProjectData(ProjectDataScope scope, string data); /// /// Get an extension's data in a project identified by . @@ -116,5 +116,5 @@ private ResponseToRequest GetSupportedTypes() /// /// Set an extension's data in a project identified by . /// - public abstract ResponseToRequest SetExtensionData(ProjectDataScope scope, string jsonData); + public abstract ResponseToRequest SetExtensionData(ProjectDataScope scope, string data); } diff --git a/extensions/src/resource-viewer/resource-viewer.web-view.tsx b/extensions/src/resource-viewer/resource-viewer.web-view.tsx index 38a2b93d1e..897cdd7cba 100644 --- a/extensions/src/resource-viewer/resource-viewer.web-view.tsx +++ b/extensions/src/resource-viewer/resource-viewer.web-view.tsx @@ -123,6 +123,7 @@ const usxEditorCharMap = Object.fromEntries( interface ScriptureTextPanelUsxProps { usx: string; + onChanged?: (newUsx: string) => void; } const defaultScrRef: ScriptureReference = { @@ -135,7 +136,7 @@ const defaultScrRef: ScriptureReference = { * Scripture text panel that displays a read only version of a usx editor that displays the current * chapter */ -function ScriptureTextPanelUsxEditor({ usx }: ScriptureTextPanelUsxProps) { +function ScriptureTextPanelUsxEditor({ usx, onChanged }: ScriptureTextPanelUsxProps) { return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
@@ -143,8 +144,9 @@ function ScriptureTextPanelUsxEditor({ usx }: ScriptureTextPanelUsxProps) { usx={usx} paraMap={usxEditorParaMap} charMap={usxEditorCharMap} - onUsxChanged={() => { - /* Read only */ + onUsxChanged={(newUsx) => { + // TODO: Check if the project is editable + if (onChanged) onChanged(newUsx); }} />
@@ -155,11 +157,19 @@ globalThis.webViewComponent = function ResourceViewer(): JSX.Element { logger.info('Preparing to display the Resource Viewer'); const [scrRef] = useSetting('platform.verseRef', defaultScrRef); - const [usx, , isLoading] = useData.ChapterUsx( + const [usx, setUsx, isLoading] = useData.ChapterUsx( 'usfm', useMemo(() => new VerseRef(scrRef.bookNum, scrRef.chapterNum, scrRef.verseNum), [scrRef]), 'Loading Scripture...', ); - return
{isLoading ? 'Loading' : '} />}
; + return ( +
+ {isLoading ? ( + 'Loading' + ) : ( + '} onChanged={setUsx} /> + )} +
+ ); }; diff --git a/extensions/src/usfm-data-provider/index.d.ts b/extensions/src/usfm-data-provider/index.d.ts index 101e62bb40..b180230076 100644 --- a/extensions/src/usfm-data-provider/index.d.ts +++ b/extensions/src/usfm-data-provider/index.d.ts @@ -15,7 +15,7 @@ declare module 'usfm-data-provider' { export type UsfmProviderDataTypes = { BookNames: DataProviderDataType; Chapter: DataProviderDataType; - ChapterUsx: DataProviderDataType; + ChapterUsx: DataProviderDataType; BookUsx: DataProviderDataType; Verse: DataProviderDataType; }; @@ -30,12 +30,14 @@ declare module 'papi-shared-types' { * This is not yet a complete list of the data types available from Paratext projects. */ export type ParatextStandardProjectDataTypes = MandatoryProjectDataType & { - /** Gets the "raw" USFM data for the specified book */ + /** Gets/sets the "raw" USFM data for the specified book */ BookUSFM: DataProviderDataType; - /** Gets the "raw" USFM data for the specified chapter */ + /** Gets/sets the "raw" USFM data for the specified chapter */ ChapterUSFM: DataProviderDataType; - /** Gets the "raw" USFM data for the specified verse */ + /** Gets/sets the "raw" USFM data for the specified verse */ VerseUSFM: DataProviderDataType; + /** Gets/sets the data in USX form for the specified chapter */ + ChapterUSX: DataProviderDataType; /** * Gets the tokenized USJ data for the specified book * @@ -189,6 +191,28 @@ declare module 'papi-shared-types' { options?: DataProviderSubscriberOptions, ): Unsubscriber; + /** Gets the Scripture text in USX format for the specified chapter */ + getChapterUSX(verseRef: VerseRef): Promise; + /** Sets the Scripture text in USX format for the specified chapter */ + setChapterUSX( + verseRef: VerseRef, + usx: string, + ): Promise>; + /** + * Subscribe to run a callback function when the 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) + */ + subscribeChapterUSX( + verseRef: VerseRef, + callback: (usx: string | undefined) => void, + options?: DataProviderSubscriberOptions, + ): Unsubscriber; + /** * Gets the tokenized USJ data for the specified book * diff --git a/src/main/main.ts b/src/main/main.ts index 110d0d893b..3a29da94fa 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -23,6 +23,9 @@ import extensionAssetProtocolService from '@main/services/extension-asset-protoc import { wait } from '@shared/utils/util'; import { CommandNames } from 'papi-shared-types'; import { SerializedRequestType } from '@shared/utils/papi-util'; +// Used with the commented out code at the bottom of this file to test the ParatextProjectDataProvider +// import { getProjectDataProvider } from '@shared/services/project-data-provider.service'; +// import { VerseRef } from '@sillsdev/scripture'; const PROCESS_CLOSE_TIME_OUT = 2000; @@ -330,8 +333,11 @@ async function main() { const paratextPdp = await getProjectDataProvider<'ParatextStandard'>( '32664dc3288a28df2e2bb75ded887fc8f17a15fb', ); - const verse = await paratextPdp.getVerseUSFM(new VerseRef('JHN', '1', '1')); + const verse = await paratextPdp.getChapterUSX(new VerseRef('JHN', '1', '1')); logger.info(`Got PDP data: ${verse}`); + + if (verse !== undefined) await paratextPdp.setChapterUSX(new VerseRef('JHN', '1', '1'), verse); + paratextPdp.setExtensionData( { extensionName: 'foo', dataQualifier: 'fooData' }, 'This is the data from extension foo',