Skip to content

Commit

Permalink
#460 Make the Resource Viewer editable as a proof-of-concept Scriptur…
Browse files Browse the repository at this point in the history
…e editor (#556)
  • Loading branch information
FoolRunning authored Oct 26, 2023
2 parents 5407068 + ead7db3 commit 23f4498
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ launchSettings.json

# Test development user appdata files
dev-appdata/
c-sharp/**/*.BAK
47 changes: 35 additions & 12 deletions c-sharp/NetworkObjects/UsfmDataProvider.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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}")
};
Expand All @@ -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);
}

Expand All @@ -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();
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions c-sharp/ParatextUtils/ParatextGlobals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
17 changes: 17 additions & 0 deletions c-sharp/Projects/ParatextProjectDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand All @@ -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
}
84 changes: 81 additions & 3 deletions c-sharp/Projects/ParatextProjectStorageInterpreter.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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) { }
Expand Down Expand Up @@ -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}")
};
}
Expand Down Expand Up @@ -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}");
}
Expand Down Expand Up @@ -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<UsfmToken> 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<WriteLock> action)
{
var myLock =
Expand Down
8 changes: 4 additions & 4 deletions c-sharp/Projects/ProjectStorageInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -106,7 +106,7 @@ private ResponseToRequest GetSupportedTypes()
/// <summary>
/// Set data in a project identified by <param name="scope"></param>.
/// </summary>
public abstract ResponseToRequest SetProjectData(ProjectDataScope scope, string jsonData);
public abstract ResponseToRequest SetProjectData(ProjectDataScope scope, string data);

/// <summary>
/// Get an extension's data in a project identified by <param name="scope"></param>.
Expand All @@ -116,5 +116,5 @@ private ResponseToRequest GetSupportedTypes()
/// <summary>
/// Set an extension's data in a project identified by <param name="scope"></param>.
/// </summary>
public abstract ResponseToRequest SetExtensionData(ProjectDataScope scope, string jsonData);
public abstract ResponseToRequest SetExtensionData(ProjectDataScope scope, string data);
}
20 changes: 15 additions & 5 deletions extensions/src/resource-viewer/resource-viewer.web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const usxEditorCharMap = Object.fromEntries(

interface ScriptureTextPanelUsxProps {
usx: string;
onChanged?: (newUsx: string) => void;
}

const defaultScrRef: ScriptureReference = {
Expand All @@ -135,16 +136,17 @@ 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
<div className="text-panel">
<UsxEditor
usx={usx}
paraMap={usxEditorParaMap}
charMap={usxEditorCharMap}
onUsxChanged={() => {
/* Read only */
onUsxChanged={(newUsx) => {
// TODO: Check if the project is editable
if (onChanged) onChanged(newUsx);
}}
/>
</div>
Expand All @@ -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<UsfmProviderDataTypes, 'ChapterUsx'>(
const [usx, setUsx, isLoading] = useData.ChapterUsx<UsfmProviderDataTypes, 'ChapterUsx'>(
'usfm',
useMemo(() => new VerseRef(scrRef.bookNum, scrRef.chapterNum, scrRef.verseNum), [scrRef]),
'Loading Scripture...',
);

return <div>{isLoading ? 'Loading' : <ScriptureTextPanelUsxEditor usx={usx ?? '<usx/>'} />}</div>;
return (
<div>
{isLoading ? (
'Loading'
) : (
<ScriptureTextPanelUsxEditor usx={usx ?? '<usx/>'} onChanged={setUsx} />
)}
</div>
);
};
Loading

0 comments on commit 23f4498

Please sign in to comment.