From 05a7e44de47d2346ea19cddea6cba5be531a79e2 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 13 Aug 2024 08:57:58 +0200 Subject: [PATCH] Add a grammar and spell checker assistant (#72) --- app/MindWork AI Studio/Components/App.razor | 1 + .../Components/AssistantBase.razor | 40 ++++++++- .../Components/AssistantBase.razor.cs | 32 ++++++- .../Components/AssistantBaseCore.cs | 2 +- .../Components/Pages/About.razor | 1 + .../Components/Pages/Assistants.razor | 1 + .../AssistantGrammarSpelling.razor | 11 +++ .../AssistantGrammarSpelling.razor.cs | 84 +++++++++++++++++++ app/MindWork AI Studio/Tools/ButtonData.cs | 3 + .../Tools/CommonLanguageExtensions.cs | 8 ++ .../Tools/JsRuntimeExtensions.cs | 11 +++ app/MindWork AI Studio/wwwroot/app.js | 19 +++++ .../wwwroot/changelog/v0.8.8.md | 2 + app/MindWork AI Studio/wwwroot/diff.js | 2 + 14 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 app/MindWork AI Studio/Components/Pages/GrammarSpelling/AssistantGrammarSpelling.razor create mode 100644 app/MindWork AI Studio/Components/Pages/GrammarSpelling/AssistantGrammarSpelling.razor.cs create mode 100644 app/MindWork AI Studio/Tools/ButtonData.cs create mode 100644 app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs create mode 100644 app/MindWork AI Studio/wwwroot/diff.js diff --git a/app/MindWork AI Studio/Components/App.razor b/app/MindWork AI Studio/Components/App.razor index 152203c..b173c9a 100644 --- a/app/MindWork AI Studio/Components/App.razor +++ b/app/MindWork AI Studio/Components/App.razor @@ -13,6 +13,7 @@ + diff --git a/app/MindWork AI Studio/Components/AssistantBase.razor b/app/MindWork AI Studio/Components/AssistantBase.razor index 0a8c0c2..61bf2ce 100644 --- a/app/MindWork AI Studio/Components/AssistantBase.razor +++ b/app/MindWork AI Studio/Components/AssistantBase.razor @@ -16,10 +16,44 @@ } - - @if (this.resultingContentBlock is not null) + + @if (this.isProcessing) { - + + } +
+ @if (this.ShowResult && this.resultingContentBlock is not null) + { + + } +
+ +
+
+ + @if (this.FooterButtons.Count > 0) + { + + @foreach (var buttonData in this.FooterButtons) + { + switch (buttonData) + { + case var _ when !string.IsNullOrWhiteSpace(buttonData.Tooltip): + + + @buttonData.Text + + + break; + + default: + + @buttonData.Text + + break; + } + } + } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AssistantBase.razor.cs b/app/MindWork AI Studio/Components/AssistantBase.razor.cs index 5ef9e70..5e3154f 100644 --- a/app/MindWork AI Studio/Components/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Components/AssistantBase.razor.cs @@ -18,6 +18,15 @@ public abstract partial class AssistantBase : ComponentBase [Inject] protected ThreadSafeRandom RNG { get; init; } = null!; + [Inject] + protected ISnackbar Snackbar { get; init; } = null!; + + [Inject] + protected Rust Rust { get; init; } = null!; + + internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult"; + internal const string ASSISTANT_RESULT_DIV_ID = "assistantResult"; + protected abstract string Title { get; } protected abstract string Description { get; } @@ -26,6 +35,10 @@ public abstract partial class AssistantBase : ComponentBase private protected virtual RenderFragment? Body => null; + protected virtual bool ShowResult => true; + + protected virtual IReadOnlyList FooterButtons => []; + protected static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); protected AIStudio.Settings.Provider providerSettings; @@ -35,6 +48,7 @@ public abstract partial class AssistantBase : ComponentBase private ChatThread? chatThread; private ContentBlock? resultingContentBlock; private string[] inputIssues = []; + private bool isProcessing; #region Overrides of ComponentBase @@ -96,7 +110,7 @@ public abstract partial class AssistantBase : ComponentBase return time; } - protected async Task AddAIResponseAsync(DateTimeOffset time) + protected async Task AddAIResponseAsync(DateTimeOffset time) { var aiText = new ContentText { @@ -114,10 +128,26 @@ public abstract partial class AssistantBase : ComponentBase }; this.chatThread?.Blocks.Add(this.resultingContentBlock); + this.isProcessing = true; + this.StateHasChanged(); // Use the selected provider to get the AI response. // By awaiting this line, we wait for the entire // content to be streamed. await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); + + this.isProcessing = false; + this.StateHasChanged(); + + // Return the AI response: + return aiText.Text; + } + + private static string? GetButtonIcon(string icon) + { + if(string.IsNullOrWhiteSpace(icon)) + return null; + + return icon; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AssistantBaseCore.cs b/app/MindWork AI Studio/Components/AssistantBaseCore.cs index 5508b14..d6fe7f7 100644 --- a/app/MindWork AI Studio/Components/AssistantBaseCore.cs +++ b/app/MindWork AI Studio/Components/AssistantBaseCore.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Components.Rendering; namespace AIStudio.Components; // -// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed +// See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary // public abstract class AssistantBaseCore : AssistantBase diff --git a/app/MindWork AI Studio/Components/Pages/About.razor b/app/MindWork AI Studio/Components/Pages/About.razor index 0f29700..02f6ab7 100644 --- a/app/MindWork AI Studio/Components/Pages/About.razor +++ b/app/MindWork AI Studio/Components/Pages/About.razor @@ -49,6 +49,7 @@ + diff --git a/app/MindWork AI Studio/Components/Pages/Assistants.razor b/app/MindWork AI Studio/Components/Pages/Assistants.razor index 97ad484..8565631 100644 --- a/app/MindWork AI Studio/Components/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Components/Pages/Assistants.razor @@ -12,6 +12,7 @@ + diff --git a/app/MindWork AI Studio/Components/Pages/GrammarSpelling/AssistantGrammarSpelling.razor b/app/MindWork AI Studio/Components/Pages/GrammarSpelling/AssistantGrammarSpelling.razor new file mode 100644 index 0000000..647cb94 --- /dev/null +++ b/app/MindWork AI Studio/Components/Pages/GrammarSpelling/AssistantGrammarSpelling.razor @@ -0,0 +1,11 @@ +@using AIStudio.Tools +@page "/assistant/grammar-spelling" +@inherits AssistantBaseCore + + + + + + + Proofread + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Pages/GrammarSpelling/AssistantGrammarSpelling.razor.cs b/app/MindWork AI Studio/Components/Pages/GrammarSpelling/AssistantGrammarSpelling.razor.cs new file mode 100644 index 0000000..1332630 --- /dev/null +++ b/app/MindWork AI Studio/Components/Pages/GrammarSpelling/AssistantGrammarSpelling.razor.cs @@ -0,0 +1,84 @@ +using AIStudio.Tools; + +namespace AIStudio.Components.Pages.GrammarSpelling; + +public partial class AssistantGrammarSpelling : AssistantBaseCore +{ + protected override string Title => "Grammar and Spelling Checker"; + + protected override string Description => + """ + Check the grammar and spelling of a text. + """; + + protected override string SystemPrompt => + $""" + You are an expert in languages and their rules. For example, you know how US and UK English or German in + Germany and German in Austria differ. You receive text as input. You check the spelling and grammar of + this text according to the rules of {this.SystemPromptLanguage()}. You never add information. You + never ask the user for additional information. You do not attempt to improve the wording of the text. + Your response includes only the corrected text. Do not explain your changes. If no changes are needed, + you return the text unchanged. + """; + + protected override bool ShowResult => false; + + protected override IReadOnlyList FooterButtons => new[] + { + new ButtonData("Copy corrected text", Icons.Material.Filled.ContentCopy, Color.Default, string.Empty, this.CopyToClipboard), + }; + + private string inputText = string.Empty; + private CommonLanguages selectedTargetLanguage; + private string customTargetLanguage = string.Empty; + private string correctedText = string.Empty; + + private string? ValidateText(string text) + { + if(string.IsNullOrWhiteSpace(text)) + return "Please provide a text as input. You might copy the desired text from a document or a website."; + + return null; + } + + private string? ValidateCustomLanguage(string language) + { + if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) + return "Please provide a custom language."; + + return null; + } + + private string SystemPromptLanguage() + { + var lang = this.selectedTargetLanguage switch + { + CommonLanguages.AS_IS => "the source language", + CommonLanguages.OTHER => this.customTargetLanguage, + + _ => $"{this.selectedTargetLanguage.Name()}", + }; + + if (string.IsNullOrWhiteSpace(lang)) + return "the source language"; + + return lang; + } + + private async Task ProofreadText() + { + if (!this.inputIsValid) + return; + + this.CreateChatThread(); + var time = this.AddUserRequest(this.inputText); + + this.correctedText = await this.AddAIResponseAsync(time); + await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.correctedText); + } + + private async Task CopyToClipboard() + { + await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, this.correctedText); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ButtonData.cs b/app/MindWork AI Studio/Tools/ButtonData.cs new file mode 100644 index 0000000..7f05ddf --- /dev/null +++ b/app/MindWork AI Studio/Tools/ButtonData.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct ButtonData(string Text, string Icon, Color Color, string Tooltip, Func AsyncAction); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs b/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs index 57618b6..96dfafe 100644 --- a/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs +++ b/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs @@ -42,4 +42,12 @@ public static class CommonLanguageExtensions return language.Name(); } + + public static string NameSelectingOptional(this CommonLanguages language) + { + if(language is CommonLanguages.AS_IS) + return "Do not specify the language"; + + return language.Name(); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs b/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs new file mode 100644 index 0000000..d813234 --- /dev/null +++ b/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs @@ -0,0 +1,11 @@ +using AIStudio.Components; + +namespace AIStudio.Tools; + +public static class JsRuntimeExtensions +{ + public static async Task GenerateAndShowDiff(this IJSRuntime jsRuntime, string text1, string text2) + { + await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantBase.ASSISTANT_RESULT_DIV_ID, AssistantBase.AFTER_RESULT_DIV_ID); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/app.js b/app/MindWork AI Studio/wwwroot/app.js index e69de29..6629610 100644 --- a/app/MindWork AI Studio/wwwroot/app.js +++ b/app/MindWork AI Studio/wwwroot/app.js @@ -0,0 +1,19 @@ +window.generateDiff = function (text1, text2, divDiff, divLegend) { + let wikEdDiff = new WikEdDiff(); + let targetDiv = document.getElementById(divDiff) + targetDiv.innerHTML = wikEdDiff.diff(text1, text2); + targetDiv.classList.add('mud-typography-body1'); + + let legend = document.getElementById(divLegend); + legend.innerHTML = ` +
+

Legend

+
    +
  • Original block position
  • +
  • Inserted text
  • +
  • Deleted text
  • +
  • Moved block
  • +
+
+ `; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.8.8.md b/app/MindWork AI Studio/wwwroot/changelog/v0.8.8.md index 9ab877b..3ce6a96 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.8.8.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.8.8.md @@ -1,2 +1,4 @@ # v0.8.8, build 170 +- Added a grammar and spell checker assistant +- Improved all assistants by showing a progress bar while processing - Upgraded MudBlazor to v7.6.0 \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/diff.js b/app/MindWork AI Studio/wwwroot/diff.js new file mode 100644 index 0000000..32f68a9 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/diff.js @@ -0,0 +1,2 @@ +// @license released into the public domain +"use strict";var wikEdDiffConfig,WED,WikEdDiff=function(){this.config={fullDiff:!1,showBlockMoves:!0,charDiff:!0,repeatedDiff:!0,recursiveDiff:!0,recursionMax:10,unlinkBlocks:!0,unlinkMax:5,blockMinLength:3,coloredBlocks:!1,noUnicodeSymbols:!1,stripTrailingNewline:!0,debug:!1,timer:!1,unitTesting:!1,regExpLetters:"a-zA-Z0-9"+"00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC".replace(/(\w{4})/g,"\\u$1"),regExpNewLines:"\\u0085\\u2028",regExpNewLinesAll:"\\n\\r\\u0085\\u2028",regExpBlanks:" \\t\\x0b\\u2000-\\u200b\\u202f\\u205f\\u3000",regExpFullStops:"\\u0589\\u06D4\\u0701\\u0702\\u0964\\u0DF4\\u1362\\u166E\\u1803\\u1809\\u2CF9\\u2CFE\\u2E3C\\u3002\\uA4FF\\uA60E\\uA6F3\\uFE52\\uFF0E\\uFF61",regExpNewParagraph:"\\f\\u2029",regExpExclamationMarks:"\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01",regExpQuestionMarks:"\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F",clipHeadingLeft:1500,clipParagraphLeftMax:1500,clipParagraphLeftMin:500,clipLineLeftMax:1e3,clipLineLeftMin:500,clipBlankLeftMax:1e3,clipBlankLeftMin:500,clipCharsLeft:500,clipHeadingRight:1500,clipParagraphRightMax:1500,clipParagraphRightMin:500,clipLineRightMax:1e3,clipLineRightMin:500,clipBlankRightMax:1e3,clipBlankRightMin:500,clipCharsRight:500,clipLinesRightMax:10,clipLinesLeftMax:10,clipSkipLines:5,clipSkipChars:1e3,cssMarkLeft:"◀",cssMarkRight:"▶",stylesheet:'.wikEdDiffInsert {font-weight: bold; background-color: #bbddff; color: #222; border-radius: 0.25em; padding: 0.2em 1px; } .wikEdDiffInsertBlank { background-color: #66bbff; } .wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } .wikEdDiffDelete {font-weight: bold; background-color: #ffe49c; color: #222; border-radius: 0.25em; padding: 0.2em 1px; } .wikEdDiffDeleteBlank { background-color: #ffd064; } .wikEdDiffFragment:hover .wikEdDiffDeleteBlank { background-color: #ffe49c; } .wikEdDiffBlock {font-weight: bold; background-color: #e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; } .wikEdDiffBlock { color: #000; } .wikEdDiffBlock0 { background-color: #ffff80; } .wikEdDiffBlock1 { background-color: #d0ff80; } .wikEdDiffBlock2 { background-color: #ffd8f0; } .wikEdDiffBlock3 { background-color: #c0ffff; } .wikEdDiffBlock4 { background-color: #fff888; } .wikEdDiffBlock5 { background-color: #bbccff; } .wikEdDiffBlock6 { background-color: #e8c8ff; } .wikEdDiffBlock7 { background-color: #ffbbbb; } .wikEdDiffBlock8 { background-color: #a0e8a0; } .wikEdDiffBlockHighlight {background-color: #777; color: #fff; border: solid #777; border-width: 1px 0; } .wikEdDiffMarkLeft, .wikEdDiffMarkRight {font-weight: bold; background-color: #ffe49c; color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; } .wikEdDiffMarkLeft:before { content: "{cssMarkLeft}"; } .wikEdDiffMarkRight:before { content: "{cssMarkRight}"; } .wikEdDiffMarkLeft.wikEdDiffNoUnicode:before { content: "<"; } .wikEdDiffMarkRight.wikEdDiffNoUnicode:before { content: ">"; } .wikEdDiffMark { background-color: #e8e8e8; color: #666; } .wikEdDiffMark0 { background-color: #ffff60; } .wikEdDiffMark1 { background-color: #c8f880; } .wikEdDiffMark2 { background-color: #ffd0f0; } .wikEdDiffMark3 { background-color: #a0ffff; } .wikEdDiffMark4 { background-color: #fff860; } .wikEdDiffMark5 { background-color: #b0c0ff; } .wikEdDiffMark6 { background-color: #e0c0ff; } .wikEdDiffMark7 { background-color: #ffa8a8; } .wikEdDiffMark8 { background-color: #98e898; } .wikEdDiffMarkHighlight { background-color: #777; color: #fff; } .wikEdDiffContainer { } .wikEdDiffFragment {white-space: pre-wrap; background-color: var(--background-color-base, #fff); border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; } .wikEdDiffNoChange { background: #f0f0f0; border: 1px #bbb solid; border-radius: 0.5em; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; text-align: center; } .wikEdDiffSeparator { margin-bottom: 1em; } .wikEdDiffOmittedChars { } .wikEdDiffNewline:before { content: "¶"; color: transparent; } .wikEdDiffBlock:hover .wikEdDiffNewline:before { color: #aaa; } .wikEdDiffBlockHighlight .wikEdDiffNewline:before { color: transparent; } .wikEdDiffBlockHighlight:hover .wikEdDiffNewline:before { color: #ccc; } .wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffNewline:before, .wikEdDiffInsert:hover .wikEdDiffNewline:before{ color: #999; } .wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffNewline:before, .wikEdDiffDelete:hover .wikEdDiffNewline:before{ color: #aaa; } .wikEdDiffTab { position: relative; } .wikEdDiffTabSymbol { position: absolute; top: -0.2em; } .wikEdDiffTabSymbol:before { content: "→"; font-size: smaller; color: #ccc; } .wikEdDiffBlock .wikEdDiffTabSymbol:before { color: #aaa; } .wikEdDiffBlockHighlight .wikEdDiffTabSymbol:before { color: #aaa; } .wikEdDiffInsert .wikEdDiffTabSymbol:before { color: #aaa; } .wikEdDiffDelete .wikEdDiffTabSymbol:before { color: #bbb; } .wikEdDiffSpace { position: relative; } .wikEdDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; } .wikEdDiffSpaceSymbol:before { content: "·"; color: transparent; } .wikEdDiffBlock:hover .wikEdDiffSpaceSymbol:before { color: #999; } .wikEdDiffBlockHighlight .wikEdDiffSpaceSymbol:before { color: transparent; } .wikEdDiffBlockHighlight:hover .wikEdDiffSpaceSymbol:before { color: #ddd; } .wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffSpaceSymbol:before,.wikEdDiffInsert:hover .wikEdDiffSpaceSymbol:before { color: #888; } .wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffSpaceSymbol:before,.wikEdDiffDelete:hover .wikEdDiffSpaceSymbol:before { color: #999; } .wikEdDiffError .wikEdDiffFragment,.wikEdDiffError .wikEdDiffNoChange{ background: #faa; }'},this.config.regExp={split:{paragraph:new RegExp("(\\r\\n|\\n|\\r){2,}|["+this.config.regExpNewParagraph+"]","g"),line:new RegExp("\\r\\n|\\n|\\r|["+this.config.regExpNewLinesAll+"]","g"),sentence:new RegExp("[^"+this.config.regExpBlanks+"].*?[.!?:;"+this.config.regExpFullStops+this.config.regExpExclamationMarks+this.config.regExpQuestionMarks+"]+(?=["+this.config.regExpBlanks+"]|$)","g"),chunk:new RegExp('\\[\\[[^\\[\\]\\n]+\\]\\]|\\{\\{[^\\{\\}\\n]+\\}\\}|\\[[^\\[\\]\\n]+\\]|<\\/?[^<>\\[\\]\\{\\}\\n]+>|\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||\\{\\{[^\\{\\}\\|\\n]+\\||\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+',"g"),word:new RegExp("(\\w+|[_"+this.config.regExpLetters+"])+(['’][_"+this.config.regExpLetters+"]*)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|'''|''|==+|\\{\\||\\|\\}|\\|-|.","g"),character:/./g},blankOnlyToken:new RegExp("[^"+this.config.regExpBlanks+this.config.regExpNewLinesAll+this.config.regExpNewParagraph+"]"),slideStop:new RegExp("["+this.config.regExpNewLinesAll+this.config.regExpNewParagraph+"]$"),slideBorder:new RegExp("["+this.config.regExpBlanks+"]$"),countWords:new RegExp("(\\w+|[_"+this.config.regExpLetters+"])+(['’][_"+this.config.regExpLetters+"]*)*","g"),countChunks:new RegExp('\\[\\[[^\\[\\]\\n]+\\]\\]|\\{\\{[^\\{\\}\\n]+\\}\\}|\\[[^\\[\\]\\n]+\\]|<\\/?[^<>\\[\\]\\{\\}\\n]+>|\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||\\{\\{[^\\{\\}\\|\\n]+\\||\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+',"g"),blankBlock:/^([^\t\S]+|[^\t])$/,clipLine:new RegExp("["+this.config.regExpNewLinesAll+this.config.regExpNewParagraph+"]+","g"),clipHeading:new RegExp("( ^|\\n)(==+.+?==+|\\{\\||\\|\\}).*?(?=\\n|$)","g"),clipParagraph:new RegExp("( (\\r\\n|\\n|\\r){2,}|["+this.config.regExpNewParagraph+"])+","g"),clipBlank:new RegExp("["+this.config.regExpBlanks+"]+","g"),clipTrimNewLinesLeft:new RegExp("["+this.config.regExpNewLinesAll+this.config.regExpNewParagraph+"]+$","g"),clipTrimNewLinesRight:new RegExp("^["+this.config.regExpNewLinesAll+this.config.regExpNewParagraph+"]+","g"),clipTrimBlanksLeft:new RegExp("["+this.config.regExpBlanks+this.config.regExpNewLinesAll+this.config.regExpNewParagraph+"]+$","g"),clipTrimBlanksRight:new RegExp("^["+this.config.regExpBlanks+this.config.regExpNewLinesAll+this.config.regExpNewParagraph+"]+","g")},this.config.msg={"wiked-diff-empty":"(No difference)","wiked-diff-same":"=","wiked-diff-ins":"+","wiked-diff-del":"-","wiked-diff-block-left":"◀","wiked-diff-block-right":"▶","wiked-diff-block-left-nounicode":"<","wiked-diff-block-right-nounicode":">","wiked-diff-error":"Error: diff not consistent with versions!"},this.config.htmlCode={noChangeStart:'
',noChangeEnd:"
",containerStart:'
',containerEnd:"
",fragmentStart:'
',fragmentEnd:"
",separator:'
',insertStart:'',insertStartBlank:'',insertEnd:"",deleteStart:'',deleteStartBlank:'',deleteEnd:"",blockStart:'',blockColoredStart:'',blockEnd:"",markLeft:'',markLeftColored:'',markRight:'',markRightColored:'',newline:'\n',tab:'\t',space:' ',omittedChars:'',errorStart:'
',errorEnd:"
"},this.config.blockHandler=function(t,i,e){void 0===t&&void 0!==window.event&&(t=window.event);var n=i.id.replace(/\D/g,""),o=document.getElementById("wikEdDiffBlock"+n),l=document.getElementById("wikEdDiffMark"+n);if(null!==o&&null!==l){if("mouseover"===e&&(i.onmouseover=null,i.onmouseout=function(t){window.wikEdDiffBlockHandler(t,i,"mouseout")},i.onclick=function(t){window.wikEdDiffBlockHandler(t,i,"click")},o.className+=" wikEdDiffBlockHighlight",l.className+=" wikEdDiffMarkHighlight"),("mouseout"===e||"click"===e)&&(i.onmouseout=null,i.onmouseover=function(t){window.wikEdDiffBlockHandler(t,i,"mouseover")},"click"!==e)){o.className=o.className.replace(/ wikEdDiffBlockHighlight/g,""),l.className=l.className.replace(/ wikEdDiffMarkHighlight/g,"");var s=document.getElementById("wikEdDiffContainer");if(null!==s)for(var r=s.getElementsByTagName("span"),h=r.length,f=0;f0)break;d=!1===l?this.newText.tokens[d].next:this.newText.tokens[d].prev}for(var u=o;null!==u;){if(null===this.oldText.tokens[u].link){k=this.oldText.tokens[u].token;if(!1===Object.prototype.hasOwnProperty.call(r.hashTable,k))r.hashTable[k]=r.token.length,r.token.push({newCount:0,oldCount:1,newToken:null,oldToken:u});else{g=r.hashTable[k];r.token[g].oldCount++,r.token[g].oldToken=u}}else if(s>0)break;u=!1===l?this.oldText.tokens[u].next:this.oldText.tokens[u].prev}var p=r.token.length;for(d=0;d=this.config.blockMinLength)b=!0;else for(d=0;d0&&this.maxWords>=this.config.blockMinLength){!0===this.config.timer&&this.time("total unlinking");for(var i=!0;!0===i&&ts?s=t[h].oldNumber:t[h].oldNumbero){for(var f=o;f<=l;f++)t[f].section=i.length;i.push({blockStart:o,blockEnd:l}),n=l}}!0===this.config.timer&&this.timeEnd("getSections")},this.getGroups=function(){!0===this.config.timer&&this.time("getGroups");var t=this.blocks,i=this.groups;i.splice(0);for(var e=t.length,n=0;nh&&(h=t[a].words),!0===t[a].unique&&(f=!0),r+=t[a].words,c+=t[a].chars,l=a;if(l>=o){var d=!1;null===t[o].section&&(d=!0);for(a=o;a<=l;a++)t[a].group=i.length,t[a].fixed=d;i.push({oldNumber:t[o].oldNumber,blockStart:o,blockEnd:l,unique:f,maxWords:h,words:r,chars:c,fixed:d,movedFrom:null,color:null}),n=l,h>this.maxWords&&(this.maxWords=h)}}!0===this.config.timer&&this.timeEnd("getGroups")},this.setFixed=function(){!0===this.config.timer&&this.time("setFixed");for(var t=this.blocks,i=this.groups,e=this.sections,n=e.length,o=0;oc&&(a=k.path,c=k.chars)}var g=a.length;for(d=0;do&&(o=h.chars,s=h)}return s.path.unshift(t),s.chars+=n[t].chars,void 0===e[t]&&(e[t]={path:s.path.slice(),chars:s.chars}),s},this.unlinkBlocks=function(){for(var t=this.blocks,i=this.groups,e=!1,n=i.length,o=0;o1||!0===t[r].unique)break;this.unlinkSingleBlock(t[r]),e=!0,l=r}for(r=s;r>l;r--)if("="===t[r].type){if(t[r].words>1||1===t[r].words&&!0===t[r].unique)break;this.unlinkSingleBlock(t[r]),e=!0}}}return e},this.unlinkSingleBlock=function(t){for(var i=t.oldStart,e=0;e0&&(r=t[s=e[o-1].newBlock]);var h=null,f=null;o=0;a--)if("="===e[a].type&&!0===e[a].fixed){c=e[a];break}null===c?l.newNumber=-1:(l.newNumber=c.newNumber,l.section=c.section,l.group=c.group,l.fixed=c.fixed)}}this.sortBlocks(),!0===this.config.timer&&this.timeEnd("positionDelBlocks")},this.getInsBlocks=function(){!0===this.config.timer&&this.time("getInsBlocks");for(var t=this.blocks,i=this.newText.first;null!==i;){for(;null!==i&&null!==this.newText.tokens[i].link;)i=this.newText.tokens[i].next;if(null!==i){for(var e=i,n=0,o="";null!==i&&null===this.newText.tokens[i].link;)n++,o+=this.newText.tokens[i].token,i=this.newText.tokens[i].next;t.push({oldBlock:null,newBlock:null,oldNumber:null,newNumber:this.newText.tokens[e].number,oldStart:null,count:n,unique:!1,words:null,chars:o.length,type:"+",section:null,group:null,fixed:null,moved:null,text:o})}}this.sortBlocks(),!0===this.config.timer&&this.timeEnd("getInsBlocks")},this.sortBlocks=function(){var t=this.blocks,i=this.groups;t.sort((function(t,i){var e=t.newNumber-i.newNumber;return 0===e&&(e=t.oldNumber-i.oldNumber),e}));for(var e=null,n=t.length,o=0;o0&&(a=o[d-1]);var d,k=null;(d=r[f.blockEnd])=0;m--)if("="===o[m].type&&!0===o[m].fixed){p=o[m];break}null===p?(g=-1,u=i.length,i.push({oldNumber:0,blockStart:t.length,blockEnd:t.length,unique:!1,maxWords:null,words:null,chars:0,fixed:null,movedFrom:null,color:null})):(g=p.newNumber,u=p.group),t.push({oldBlock:null,newBlock:null,oldNumber:c,newNumber:g,oldStart:null,count:null,unique:null,words:null,chars:0,type:"|",section:null,group:u,fixed:!0,moved:e,text:""}),f.color=n,f.movedFrom=u,n++}}this.sortBlocks(),!0===this.config.timer&&this.timeEnd("insertMarks")},this.getDiffFragments=function(){var t=this.blocks,i=this.groups,e=this.fragments,n=i.slice();n.sort((function(t,i){return t.blockStart-i.blockStart}));for(var o=n.length,l=0;l",e.push({text:"",type:c,color:h});for(var f=s;f<=r;f++){var c;if("="===(c=t[f].type)||"-"===c||"+"===c)e.push({text:t[f].text,type:c,color:h});else if("|"===c){for(var a,d=i[t[f].moved],k="",g=d.blockStart;g<=d.blockEnd;g++)"="!==t[g].type&&"-"!==t[g].type||(k+=t[g].text);a=d.blockStart",e.push({text:k,type:a,color:d.color})}}null!==h&&e.push({text:"",type:" )",color:h})}for(var u=e.length,p=1;pthis.config.clipHeadingLeft||k[b]>w);b++){p=k[b],x="heading";break}if(null===p){var D=g.length;for(b=0;bthis.config.clipParagraphLeftMax||g[b]>w);b++)if(g[b]>this.config.clipParagraphLeftMin){p=g[b],x="paragraph";break}}if(null===p){var B=c.length;for(b=0;bthis.config.clipLineLeftMax||c[b]>w);b++)if(c[b]>this.config.clipLineLeftMin){p=c[b],x="line";break}}null===p&&(this.config.regExp.clipBlank.lastIndex=this.config.clipBlankLeftMin,null!==(f=this.config.regExp.clipBlank.exec(r))&&f.index=this.config.clipLinesRightMax&&(A=c[c.length-this.config.clipLinesRightMax]),null===u)for(b=d.length-1;b>=0&&!(d[b]=0&&!(g[b]=0&&!(c[b]h-this.config.clipBlankRightMin){null!==v&&(u=v,m="blank");break}v=f.index}}null===u&&h-this.config.clipCharsRight>A&&(u=h-this.config.clipCharsRight,m="chars"),null===u&&(u=A,m="fixed")}if(null!==p&&null!==u){if(p>u)continue;if(u-pu||T>this.config.clipSkipLines);b++)c[b]>p&&T++;if(T"===s){var c;if("old"!==t)c=!0===this.config.noUnicodeSymbols?this.config.msg["wiked-diff-block-right-nounicode"]:this.config.msg["wiked-diff-block-right"],h=!0===this.config.coloredBlocks?this.config.htmlCode.blockColoredStart:this.config.htmlCode.blockStart,h=this.htmlCustomize(h,r,c)}else" )"===s&&"old"!==t&&(h=this.config.htmlCode.blockEnd);"="===s?(l=this.htmlEscape(l),null!==r?"old"!==t&&(h=this.markupBlanks(l,!0)):h=this.markupBlanks(l)):"-"===s?"new"!==t&&("old"===t&&null!==r||(l=this.htmlEscape(l),l=this.markupBlanks(l,!0),h=!0===f?this.config.htmlCode.deleteStartBlank:this.config.htmlCode.deleteStart,h+=l+this.config.htmlCode.deleteEnd)):"+"===s?"old"!==t&&(l=this.htmlEscape(l),l=this.markupBlanks(l,!0),h=!0===f?this.config.htmlCode.insertStartBlank:this.config.htmlCode.insertStart,h+=l+this.config.htmlCode.insertEnd):"<"!==s&&">"!==s||"new"!==t&&(!1===this.config.showBlockMoves||"old"===t?(l=this.htmlEscape(l),l=this.markupBlanks(l,!0),h="old"===t?!0===this.config.coloredBlocks?this.htmlCustomize(this.config.htmlCode.blockColoredStart,r)+l+this.config.htmlCode.blockEnd:this.htmlCustomize(this.config.htmlCode.blockStart,r)+l+this.config.htmlCode.blockEnd:!0===f?this.config.htmlCode.deleteStartBlank+l+this.config.htmlCode.deleteEnd:this.config.htmlCode.deleteStart+l+this.config.htmlCode.deleteEnd):h="<"===s?!0===this.config.coloredBlocks?this.htmlCustomize(this.config.htmlCode.markLeftColored,r,l):this.htmlCustomize(this.config.htmlCode.markLeft,r,l):!0===this.config.coloredBlocks?this.htmlCustomize(this.config.htmlCode.markRightColored,r,l):this.htmlCustomize(this.config.htmlCode.markRight,r,l)),e.push(h)}this.html=e.join("")}else this.html=""},this.htmlCustomize=function(t,i,e){if(t=t.replace(/\{number\}/g,i),t=!0===this.config.noUnicodeSymbols?t.replace(/\{nounicode\}/g," wikEdDiffNoUnicode"):t.replace(/\{nounicode\}/g,""),void 0!==e){var n=" [...] ";e.length>512&&(e=e.substr(0,377)+n+e.substr(e.length-128)),e=(e=(e=this.htmlEscape(e)).replace(/\t/g,"  ")).replace(/ /g,"  "),t=t.replace(/\{title\}/,e)}return t},this.htmlEscape=function(t){return t=(t=(t=(t=t.replace(/&/g,"&")).replace(//g,">")).replace(/"/g,""")},this.markupBlanks=function(t,i){return!0===i&&(t=(t=t.replace(/ /g,this.config.htmlCode.space)).replace(/\n/g,this.config.htmlCode.newline)),t=t.replace(/\t/g,this.config.htmlCode.tab)},this.wordCount=function(t){return(t.match(this.config.regExp.countWords)||[]).length},this.unitTests=function(){var t,i;this.getDiffHtml("new"),(t=this.html.replace(/<[^>]*>/g,""))!==(i=this.htmlEscape(this.newText.text))?(console.log("Error: wikEdDiff unit test failure: diff not consistent with new text version!"),this.error=!0,console.log("new text:\n",i),console.log("new diff:\n",t)):console.log("OK: wikEdDiff unit test passed: diff consistent with new text."),this.getDiffHtml("old"),(t=this.html.replace(/<[^>]*>/g,""))!==(i=this.htmlEscape(this.oldText.text))?(console.log("Error: wikEdDiff unit test failure: diff not consistent with old text version!"),this.error=!0,console.log("old text:\n",i),console.log("old diff:\n",t)):console.log("OK: wikEdDiff unit test passed: diff consistent with old text.")},this.debugBlocks=function(t,i){void 0===i&&(i=this.blocks);for(var e="\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq\twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n",n=i.length,o=0;oi&&(t=t.substr(0,i-1-e)+"…"+t.substr(t.length-e)),'"'+t+'"'},this.time=function(t){this.timer[t]=(new Date).getTime()},this.timeEnd=function(t,i){var e=0;if(void 0!==this.timer[t]){var n=this.timer[t];e=(new Date).getTime()-n,this.timer[t]=void 0,!0!==i&&console.log(t+": "+e.toFixed(2)+" ms")}return e},this.timeRecursionEnd=function(t){if(this.recursionTimer.length>1){for(var i=this.recursionTimer.length-1,e=0;ec&&f.push(s.substring(c,r.index)),f.push(r[0]),c=a.lastIndex;c0&&void 0!==i&&(null!==e&&(this.tokens[e].next=n),null!==n&&(this.tokens[n].prev=e)),h>0&&(void 0===i?(this.first=0,this.last=e):(i===this.first&&(this.first=l),i===this.last&&(this.last=e)))},this.splitRefine=function(t){for(var i=this.first;null!==i;)null===this.tokens[i].link&&this.splitText(t,i),i=this.tokens[i].next},this.enumerateTokens=function(){for(var t=0,i=this.first;null!==i;)this.tokens[i].number=t,t++,i=this.tokens[i].next},this.debugText=function(t){var e=this.tokens,n="first: "+this.first+"\tlast: "+this.last+"\n";n+='\ni \tlink \t(prev \tnext) \tuniq \t#num \t"token"\n';for(var o=this.first;null!==o;)n+=o+" \t"+e[o].link+" \t("+e[o].prev+" \t"+e[o].next+") \t"+e[o].unique+" \t#"+e[o].number+" \t"+i.debugShortenText(e[o].token)+"\n",o=e[o].next;console.log(t+":\n"+n)},this.init()}; \ No newline at end of file