添加项目文件。

This commit is contained in:
Zel
2025-01-22 23:31:03 +08:00
parent 1b8ba6771f
commit 2ae76476fb
894 changed files with 774558 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
namespace TightWiki.Engine.Implementation
{
public class AggregatedSearchToken
{
public string Token { get; set; } = string.Empty;
public double Weight { get; set; }
public string DoubleMetaphone { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,22 @@
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles wiki comments. These are generally removed from the result.
/// </summary>
public class CommentHandler : ICommentHandler
{
/// <summary>
/// Handles a wiki comment.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="text">The comment text</param>
public HandlerResult Handle(ITightEngineState state, string text)
{
return new HandlerResult() { Instructions = [HandlerResultInstruction.TruncateTrailingLine] };
}
}
}

View File

@@ -0,0 +1,31 @@
using TightWiki.Engine.Library.Interfaces;
using TightWiki.Models;
using TightWiki.Repository;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles wiki completion events.
/// </summary>
public class CompletionHandler : ICompletionHandler
{
/// <summary>
/// Handles wiki completion events. Is called when the wiki processing competes for a given page.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
public void Complete(ITightEngineState state)
{
if (GlobalConfiguration.RecordCompilationMetrics)
{
StatisticsRepository.InsertCompilationStatistics(state.Page.Id,
state.ProcessingTime.TotalMilliseconds,
state.MatchCount,
state.ErrorCount,
state.OutgoingLinks.Count,
state.Tags.Count,
state.HtmlResult.Length,
state.Page.Body.Length);
}
}
}
}

View File

@@ -0,0 +1,44 @@
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using TightWiki.Models;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles wiki emojis.
/// </summary>
public class EmojiHandler : IEmojiHandler
{
/// <summary>
/// Handles an emoji instruction.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="key">The lookup key for the given emoji.</param>
/// <param name="scale">The desired 1-100 scale factor for the emoji.</param>
public HandlerResult Handle(ITightEngineState state, string key, int scale)
{
var emoji = GlobalConfiguration.Emojis.FirstOrDefault(o => o.Shortcut == key);
if (GlobalConfiguration.Emojis.Exists(o => o.Shortcut == key))
{
if (scale != 100 && scale > 0 && scale <= 500)
{
var emojiImage = $"<img src=\"{GlobalConfiguration.BasePath}/file/Emoji/{key.Trim('%')}?Scale={scale}\" alt=\"{emoji?.Name}\" />";
return new HandlerResult(emojiImage);
}
else
{
var emojiImage = $"<img src=\"{GlobalConfiguration.BasePath}/file/Emoji/{key.Trim('%')}\" alt=\"{emoji?.Name}\" />";
return new HandlerResult(emojiImage);
}
}
else
{
return new HandlerResult(key) { Instructions = [HandlerResultInstruction.DisallowNestedProcessing] };
}
}
}
}

View File

@@ -0,0 +1,27 @@
using TightWiki.Engine.Library.Interfaces;
using TightWiki.Repository;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles exceptions thrown by the wiki engine.
/// </summary>
public class ExceptionHandler : IExceptionHandler
{
/// <summary>
/// Called when an exception is thrown by the wiki engine.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="ex">Optional exception, in the case that this was an actual exception.</param>
/// <param name="customText">Text that accompanies the exception.</param>
public void Log(ITightEngineState state, Exception? ex, string customText)
{
if (ex != null)
{
ExceptionRepository.InsertException(ex, customText);
}
ExceptionRepository.InsertException(customText);
}
}
}

View File

@@ -0,0 +1,38 @@
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles links the wiki to another site.
/// </summary>
public class ExternalLinkHandler : IExternalLinkHandler
{
/// <summary>
/// Handles an internal wiki link.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="link">The address of the external site being linked to.</param>
/// <param name="text">The text which should be show in the absence of an image.</param>
/// <param name="image">The image that should be shown.</param>
/// <param name="imageScale">The 0-100 image scale factor for the given image.</param>
public HandlerResult Handle(ITightEngineState state, string link, string? text, string? image)
{
if (string.IsNullOrEmpty(image))
{
return new HandlerResult($"<a href=\"{link}\">{text}</a>")
{
Instructions = [HandlerResultInstruction.DisallowNestedProcessing]
};
}
else
{
return new HandlerResult($"<a href=\"{link}\"><img src=\"{image}\" border =\"0\"></a>")
{
Instructions = [HandlerResultInstruction.DisallowNestedProcessing]
};
}
}
}
}

View File

@@ -0,0 +1,33 @@
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles wiki headings. These are automatically added to the table of contents.
/// </summary>
public class HeadingHandler : IHeadingHandler
{
/// <summary>
/// Handles wiki headings. These are automatically added to the table of contents.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="depth">The size of the header, also used for table of table of contents indentation.</param>
/// <param name="link">The self link reference.</param>
/// <param name="text">The text for the self link.</param>
public HandlerResult Handle(ITightEngineState state, int depth, string link, string text)
{
if (depth >= 2 && depth <= 6)
{
int fontSize = 8 - depth;
if (fontSize < 5) fontSize = 5;
string html = "<font size=\"" + fontSize + "\"><a name=\"" + link + "\"><span class=\"WikiH" + (depth - 1).ToString() + "\">" + text + "</span></a></font>\r\n";
return new HandlerResult(html);
}
return new HandlerResult() { Instructions = [HandlerResultInstruction.Skip] };
}
}
}

View File

@@ -0,0 +1,130 @@
using DuoVia.FuzzyStrings;
using NTDLS.Helpers;
using TightWiki.Caching;
using TightWiki.Engine.Library.Interfaces;
using TightWiki.Library.Interfaces;
using TightWiki.Models.DataModels;
using TightWiki.Repository;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
public class Helpers
{
/// <summary>
/// Inserts a new page if Page.Id == 0, other wise updates the page. All metadata is written to the database.
/// </summary>
/// <param name="sessionState"></param>
/// <param name="query"></param>
/// <param name="page"></param>
/// <returns></returns>
public static int UpsertPage(ITightEngine wikifier, Page page, ISessionState? sessionState = null)
{
bool isNewlyCreated = page.Id == 0;
page.Id = PageRepository.SavePage(page);
RefreshPageMetadata(wikifier, page, sessionState);
if (isNewlyCreated)
{
//This will update the PageId of references that have been saved to the navigation link.
PageRepository.UpdateSinglePageReference(page.Navigation, page.Id);
}
return page.Id;
}
/// <summary>
/// Rebuilds the page and writes all aspects to the database.
/// </summary>
/// <param name="sessionState"></param>
/// <param name="query"></param>
/// <param name="page"></param>
public static void RefreshPageMetadata(ITightEngine wikifier, Page page, ISessionState? sessionState = null)
{
//We omit function calls from the tokenization process because they are too dynamic for static searching.
var state = wikifier.Transform(sessionState, page, null,
[WikiMatchType.StandardFunction]);
PageRepository.UpdatePageTags(page.Id, state.Tags);
PageRepository.UpdatePageProcessingInstructions(page.Id, state.ProcessingInstructions);
var pageTokens = ParsePageTokens(state).Select(o =>
new PageToken
{
PageId = page.Id,
Token = o.Token,
DoubleMetaphone = o.DoubleMetaphone,
Weight = o.Weight
}).ToList();
PageRepository.SavePageSearchTokens(pageTokens);
PageRepository.UpdatePageReferences(page.Id, state.OutgoingLinks);
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.Page, [page.Id]));
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.Page, [page.Navigation]));
}
public static List<AggregatedSearchToken> ParsePageTokens(ITightEngineState state)
{
var parsedTokens = new List<WeightedSearchToken>();
parsedTokens.AddRange(ComputeParsedPageTokens(state.HtmlResult, 1));
parsedTokens.AddRange(ComputeParsedPageTokens(state.Page.Description, 1.2));
parsedTokens.AddRange(ComputeParsedPageTokens(string.Join(" ", state.Tags), 1.4));
parsedTokens.AddRange(ComputeParsedPageTokens(state.Page.Name, 1.6));
var aggregatedTokens = parsedTokens.GroupBy(o => o.Token).Select(o => new AggregatedSearchToken
{
Token = o.Key,
DoubleMetaphone = o.Key.ToDoubleMetaphone(),
Weight = o.Sum(g => g.Weight)
}).ToList();
return aggregatedTokens;
}
internal static List<WeightedSearchToken> ComputeParsedPageTokens(string content, double weightMultiplier)
{
var searchConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Search");
var exclusionWords = searchConfig?.Value<string>("Word Exclusions")?
.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries).Distinct() ?? new List<string>();
var strippedContent = Html.StripHtml(content);
var tokens = strippedContent.Split([' ', '\n', '\t', '-', '_']).ToList();
if (searchConfig?.Value<bool>("Split Camel Case") == true)
{
var allSplitTokens = new List<string>();
foreach (var token in tokens)
{
var splitTokens = Text.SplitCamelCase(token);
if (splitTokens.Count > 1)
{
splitTokens.ForEach(t => allSplitTokens.Add(t));
}
}
tokens.AddRange(allSplitTokens);
}
tokens = tokens.ConvertAll(d => d.ToLowerInvariant());
tokens.RemoveAll(o => exclusionWords.Contains(o));
var searchTokens = (from w in tokens
group w by w into g
select new WeightedSearchToken
{
Token = g.Key,
Weight = g.Count() * weightMultiplier
}).ToList();
return searchTokens.Where(o => string.IsNullOrWhiteSpace(o.Token) == false).ToList();
}
}
}

View File

@@ -0,0 +1,153 @@
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using TightWiki.Library;
using TightWiki.Models;
using TightWiki.Repository;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles links from one wiki page to another.
/// </summary>
public class InternalLinkHandler : IInternalLinkHandler
{
/// <summary>
/// Handles an internal wiki link.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="pageNavigation">The navigation for the linked page.</param>
/// <param name="pageName">The name of the page being linked to.</param>
/// <param name="linkText">The text which should be show in the absence of an image.</param>
/// <param name="image">The image that should be shown.</param>
/// <param name="imageScale">The 0-100 image scale factor for the given image.</param>
public HandlerResult Handle(ITightEngineState state, NamespaceNavigation pageNavigation,
string pageName, string linkText, string? image, int imageScale)
{
var page = PageRepository.GetPageRevisionByNavigation(pageNavigation);
if (page == null)
{
if (state.Session?.CanCreate == true)
{
if (image != null)
{
string href;
if (image.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)
|| image.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
{
//The image is external.
href = $"<a href=\"{GlobalConfiguration.BasePath}/Page/Create?Name={pageName}\"><img src=\"{GlobalConfiguration.BasePath}{image}?Scale={imageScale}\" /></a>";
}
else if (image.Contains('/'))
{
//The image is located on another page.
href = $"<a href=\"{GlobalConfiguration.BasePath}/Page/Create?Name={pageName}\"><img src=\"{GlobalConfiguration.BasePath}/Page/Image/{image}?Scale={imageScale}\" /></a>";
}
else
{
//The image is located on this page, but this page does not exist.
href = $"<a href=\"{GlobalConfiguration.BasePath}/Page/Create?Name={pageName}\">{linkText}</a>";
}
return new HandlerResult(href)
{
Instructions = [HandlerResultInstruction.DisallowNestedProcessing]
};
}
else if (linkText != null)
{
var href = $"<a href=\"{GlobalConfiguration.BasePath}/Page/Create?Name={pageName}\">{linkText}</a>"
+ "<font color=\"#cc0000\" size=\"2\">?</font>";
return new HandlerResult(href)
{
Instructions = [HandlerResultInstruction.DisallowNestedProcessing]
};
}
else
{
throw new Exception("No link or image was specified.");
}
}
else
{
//The page does not exist and the user does not have permission to create it.
if (image != null)
{
string mockHref;
if (image.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)
|| image.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
{
//The image is external.
mockHref = $"<img src=\"{GlobalConfiguration.BasePath}{image}?Scale={imageScale}\" />";
}
else if (image.Contains('/'))
{
//The image is located on another page.
mockHref = $"<img src=\"{GlobalConfiguration.BasePath}/Page/Image/{image}?Scale={imageScale}\" />";
}
else
{
//The image is located on this page, but this page does not exist.
mockHref = $"linkText";
}
return new HandlerResult(mockHref)
{
Instructions = [HandlerResultInstruction.DisallowNestedProcessing]
};
}
else if (linkText != null)
{
return new HandlerResult(linkText)
{
Instructions = [HandlerResultInstruction.DisallowNestedProcessing]
};
}
else
{
throw new Exception("No link or image was specified.");
}
}
}
else
{
string href;
if (image != null)
{
if (image.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)
|| image.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
{
//The image is external.
href = $"<a href=\"{GlobalConfiguration.BasePath}/{page.Navigation}\"><img src=\"{GlobalConfiguration.BasePath}{image}\" /></a>";
}
else if (image.Contains('/'))
{
//The image is located on another page.
href = $"<a href=\"{GlobalConfiguration.BasePath}/{page.Navigation}\"><img src=\"{GlobalConfiguration.BasePath}/Page/Image/{image}?Scale={imageScale}\" /></a>";
}
else
{
//The image is located on this page.
href = $"<a href=\"{GlobalConfiguration.BasePath}/{page.Navigation}\"><img src=\"{GlobalConfiguration.BasePath}/Page/Image/{state.Page.Navigation}/{image}?Scale={imageScale}\" /></a>";
}
}
else
{
//Just a plain ol' internal page link.
href = $"<a href=\"{GlobalConfiguration.BasePath}/{page.Navigation}\">{linkText}</a>";
}
return new HandlerResult(href)
{
Instructions = [HandlerResultInstruction.DisallowNestedProcessing]
};
}
}
}
}

View File

@@ -0,0 +1,34 @@
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles basic markup/style instructions like bole, italic, underline, etc.
/// </summary>
public class MarkupHandler : IMarkupHandler
{
/// <summary>
/// Handles basic markup instructions like bole, italic, underline, etc.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="sequence">The sequence of symbols that were found to denotate this markup instruction,</param>
/// <param name="scopeBody">The body of text to apply the style to.</param>
public HandlerResult Handle(ITightEngineState state, char sequence, string scopeBody)
{
switch (sequence)
{
case '~': return new HandlerResult($"<strike>{scopeBody}</strike>");
case '*': return new HandlerResult($"<strong>{scopeBody}</strong>");
case '_': return new HandlerResult($"<u>{scopeBody}</u>");
case '/': return new HandlerResult($"<i>{scopeBody}</i>");
case '!': return new HandlerResult($"<mark>{scopeBody}</mark>");
default:
break;
}
return new HandlerResult() { Instructions = [HandlerResultInstruction.Skip] };
}
}
}

View File

@@ -0,0 +1,174 @@
using System.Text;
using TightWiki.Engine.Function;
using TightWiki.Engine.Implementation.Utility;
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using TightWiki.Models;
using static TightWiki.Engine.Function.FunctionPrototypeCollection;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles post-processing function calls.
/// </summary>
public class PostProcessingFunctionHandler : IPostProcessingFunctionHandler
{
private static FunctionPrototypeCollection? _collection;
public FunctionPrototypeCollection Prototypes
{
get
{
if (_collection == null)
{
_collection = new FunctionPrototypeCollection(WikiFunctionType.Standard);
#region Prototypes.
_collection.Add("##Tags: <string>{styleName(Flat,List)}='List'");
_collection.Add("##TagCloud: <string>[pageTag] | <integer>{Top}='1000'");
_collection.Add("##SearchCloud: <string>[searchPhrase] | <integer>{Top}='1000'");
_collection.Add("##TOC:<bool>{alphabetized}='false'");
#endregion
}
return _collection;
}
}
/// <summary>
/// Called to handle function calls when proper prototypes are matched.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="function">The parsed function call and all its parameters and their values.</param>
/// <param name="scopeBody">This is not a scope function, this should always be null</param>
public HandlerResult Handle(ITightEngineState state, FunctionCall function, string? scopeBody = null)
{
switch (function.Name.ToLower())
{
//------------------------------------------------------------------------------------------------------------------------------
//Displays a tag link list.
case "tags": //##tags
{
string styleName = function.Parameters.Get<string>("styleName").ToLower();
var html = new StringBuilder();
if (styleName == "list")
{
html.Append("<ul>");
foreach (var tag in state.Tags)
{
html.Append($"<li><a href=\"{GlobalConfiguration.BasePath}/Tag/Browse/{tag}\">{tag}</a>");
}
html.Append("</ul>");
}
else if (styleName == "flat")
{
foreach (var tag in state.Tags)
{
if (html.Length > 0) html.Append(" | ");
html.Append($"<a href=\"{GlobalConfiguration.BasePath}/Tag/Browse/{tag}\">{tag}</a>");
}
}
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "tagcloud":
{
var top = function.Parameters.Get<int>("Top");
string seedTag = function.Parameters.Get<string>("pageTag");
string html = TagCloud.Build(seedTag, top);
return new HandlerResult(html);
}
//------------------------------------------------------------------------------------------------------------------------------
case "searchcloud":
{
var top = function.Parameters.Get<int>("Top");
var tokens = function.Parameters.Get<string>("searchPhrase").Split(" ", StringSplitOptions.RemoveEmptyEntries).ToList();
string html = SearchCloud.Build(tokens, top);
return new HandlerResult(html);
}
//------------------------------------------------------------------------------------------------------------------------------
//Displays a table of contents for the page based on the header tags.
case "toc":
{
bool alphabetized = function.Parameters.Get<bool>("alphabetized");
var html = new StringBuilder();
var tags = (from t in state.TableOfContents
orderby t.StartingPosition
select t).ToList();
var unordered = new List<TableOfContentsTag>();
var ordered = new List<TableOfContentsTag>();
if (alphabetized)
{
int level = tags.FirstOrDefault()?.Level ?? 0;
foreach (var tag in tags)
{
if (level != tag.Level)
{
ordered.AddRange(unordered.OrderBy(o => o.Text));
unordered.Clear();
level = tag.Level;
}
unordered.Add(tag);
}
ordered.AddRange(unordered.OrderBy(o => o.Text));
unordered.Clear();
tags = ordered.ToList();
}
int currentLevel = 0;
foreach (var tag in tags)
{
if (tag.Level > currentLevel)
{
while (currentLevel < tag.Level)
{
html.Append("<ul>");
currentLevel++;
}
}
else if (tag.Level < currentLevel)
{
while (currentLevel > tag.Level)
{
html.Append("</ul>");
currentLevel--;
}
}
html.Append("<li><a href=\"#" + tag.HrefTag + "\">" + tag.Text + "</a></li>");
}
while (currentLevel > 0)
{
html.Append("</ul>");
currentLevel--;
}
return new HandlerResult(html.ToString());
}
}
return new HandlerResult() { Instructions = [HandlerResultInstruction.Skip] };
}
}
}

View File

@@ -0,0 +1,207 @@
using TightWiki.Engine.Function;
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using static TightWiki.Engine.Function.FunctionPrototypeCollection;
using static TightWiki.Engine.Library.Constants;
using static TightWiki.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handles processing-instruction function calls, these functions affect the way the page is processed, but are not directly replaced with text.
/// </summary>
public class ProcessingInstructionFunctionHandler : IProcessingInstructionFunctionHandler
{
private static FunctionPrototypeCollection? _collection;
public FunctionPrototypeCollection Prototypes
{
get
{
if (_collection == null)
{
_collection = new FunctionPrototypeCollection(WikiFunctionType.Instruction);
#region Prototypes.
//Processing instructions:
_collection.Add("@@Deprecate:");
_collection.Add("@@Protect:<bool>{isSilent}='false'");
_collection.Add("@@Tags: <string:infinite>[pageTags]");
_collection.Add("@@Template:");
_collection.Add("@@Review:");
_collection.Add("@@NoCache:");
_collection.Add("@@Include:");
_collection.Add("@@Draft:");
_collection.Add("@@HideFooterComments:");
_collection.Add("@@Title:<string>[pageTitle]");
_collection.Add("@@HideFooterLastModified:");
#endregion
}
return _collection;
}
}
/// <summary>
/// Called to handle function calls when proper prototypes are matched.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="function">The parsed function call and all its parameters and their values.</param>
/// <param name="scopeBody">This is not a scope function, this should always be null</param>
public HandlerResult Handle(ITightEngineState state, FunctionCall function, string? scopeBody = null)
{
switch (function.Name.ToLower())
{
//We check wikifierSession.Factory.CurrentNestLevel here because we don't want to include the processing instructions on any parent pages that are injecting this one.
//------------------------------------------------------------------------------------------------------------------------------
//Associates tags with a page. These are saved with the page and can also be displayed.
case "tags": //##tag(pipe|separated|list|of|tags)
{
var tags = function.Parameters.GetList<string>("pageTags");
state.Tags.AddRange(tags);
state.Tags = state.Tags.Distinct().ToList();
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "title":
{
state.PageTitle = function.Parameters.Get<string>("pageTitle");
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "hidefooterlastmodified":
{
state.ProcessingInstructions.Add(WikiInstruction.HideFooterLastModified);
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "hidefootercomments":
{
state.ProcessingInstructions.Add(WikiInstruction.HideFooterComments);
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "nocache":
{
state.ProcessingInstructions.Add(WikiInstruction.NoCache);
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "deprecate":
{
if (state.NestDepth == 0)
{
state.ProcessingInstructions.Add(WikiInstruction.Deprecate);
state.Headers.Add("<div class=\"alert alert-danger\">This page has been deprecated and will eventually be deleted.</div>");
}
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "protect":
{
if (state.NestDepth == 0)
{
bool isSilent = function.Parameters.Get<bool>("isSilent");
state.ProcessingInstructions.Add(WikiInstruction.Protect);
if (isSilent == false)
{
state.Headers.Add("<div class=\"alert alert-info\">This page has been protected and can not be changed by non-moderators.</div>");
}
}
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "template":
{
if (state.NestDepth == 0)
{
state.ProcessingInstructions.Add(WikiInstruction.Template);
state.Headers.Add("<div class=\"alert alert-secondary\">This page is a template and will not appear in indexes or glossaries.</div>");
}
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "review":
{
if (state.NestDepth == 0)
{
state.ProcessingInstructions.Add(WikiInstruction.Review);
state.Headers.Add("<div class=\"alert alert-warning\">This page has been flagged for review, its content may be inaccurate.</div>");
}
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "include":
{
if (state.NestDepth == 0)
{
state.ProcessingInstructions.Add(WikiInstruction.Include);
state.Headers.Add("<div class=\"alert alert-secondary\">This page is an include and will not appear in indexes or glossaries.</div>");
}
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "draft":
{
if (state.NestDepth == 0)
{
state.ProcessingInstructions.Add(WikiInstruction.Draft);
state.Headers.Add("<div class=\"alert alert-warning\">This page is a draft and may contain incorrect information and/or experimental styling.</div>");
}
return new HandlerResult(string.Empty)
{
Instructions = [HandlerResultInstruction.TruncateTrailingLine]
};
}
}
return new HandlerResult() { Instructions = [HandlerResultInstruction.Skip] };
}
}
}

View File

@@ -0,0 +1,373 @@
using NTDLS.Helpers;
using System.Text;
using TightWiki.Engine.Function;
using TightWiki.Engine.Implementation.Utility;
using TightWiki.Engine.Library;
using TightWiki.Engine.Library.Interfaces;
using static TightWiki.Engine.Function.FunctionPrototypeCollection;
using static TightWiki.Engine.Library.Constants;
namespace TightWiki.Engine.Implementation
{
/// <summary>
/// Handled scope function calls.
/// </summary>
public class ScopeFunctionHandler : IScopeFunctionHandler
{
private static FunctionPrototypeCollection? _collection;
public FunctionPrototypeCollection Prototypes
{
get
{
if (_collection == null)
{
_collection = new FunctionPrototypeCollection(WikiFunctionType.Scoped);
#region Prototypes.
_collection.Add("$$Code: <string>{language(auto,wiki,cpp,lua,graphql,swift,r,yaml,kotlin,scss,shell,vbnet,json,objectivec,perl,diff,wasm,php,xml,bash,csharp,css,go,ini,javascript,less,makefile,markdown,plaintext,python,python-repl,ruby,rust,sql,typescript)}='auto'");
_collection.Add("$$Bullets: <string>{type(unordered,ordered)}='unordered'");
_collection.Add("$$Order: <string>{direction(ascending,descending)}='ascending'");
_collection.Add("$$Jumbotron:");
_collection.Add("$$Callout: <string>{styleName(default,primary,secondary,success,info,warning,danger)}='default' | <string>{titleText}=''");
_collection.Add("$$Background: <string>{styleName(default,primary,secondary,light,dark,success,info,warning,danger,muted)}='default'");
_collection.Add("$$Foreground: <string>{styleName(default,primary,secondary,light,dark,success,info,warning,danger,muted)}='default'");
_collection.Add("$$Alert: <string>{styleName(default,primary,secondary,light,dark,success,info,warning,danger)}='default' | <string>{titleText}=''");
_collection.Add("$$Card: <string>{styleName(default,primary,secondary,light,dark,success,info,warning,danger)}='default' | <string>{titleText}=''");
_collection.Add("$$Collapse: <string>{linkText}='Show'");
_collection.Add("$$Table: <boolean>{hasBorder}='true' | <boolean>{isFirstRowHeader}='true'");
_collection.Add("$$StripedTable: <boolean>{hasBorder}='true' | <boolean>{isFirstRowHeader}='true'");
_collection.Add("$$DefineSnippet: <string>[name]");
#endregion
}
return _collection;
}
}
/// <summary>
/// Called to handle function calls when proper prototypes are matched.
/// </summary>
/// <param name="state">Reference to the wiki state object</param>
/// <param name="function">The parsed function call and all its parameters and their values.</param>
/// <param name="scopeBody">The the text that the function is designed to affect.</param>
public HandlerResult Handle(ITightEngineState state, FunctionCall function, string? scopeBody = null)
{
scopeBody.EnsureNotNull($"The function '{function.Name}' scope body can not be null");
switch (function.Name.ToLower())
{
//------------------------------------------------------------------------------------------------------------------------------
case "code":
{
var html = new StringBuilder();
string language = function.Parameters.Get<string>("language");
if (string.IsNullOrEmpty(language) || language?.ToLower() == "auto")
{
html.Append($"<pre>");
html.Append($"<code>{scopeBody.Replace("\r\n", "\n").Replace("\n", SoftBreak)}</code></pre>");
}
else
{
html.Append($"<pre class=\"language-{language}\">");
html.Append($"<code>{scopeBody.Replace("\r\n", "\n").Replace("\n", SoftBreak)}</code></pre>");
}
return new HandlerResult(html.ToString())
{
Instructions = [HandlerResultInstruction.DisallowNestedProcessing]
};
}
//------------------------------------------------------------------------------------------------------------------------------
case "stripedtable":
case "table":
{
var html = new StringBuilder();
var hasBorder = function.Parameters.Get<bool>("hasBorder");
var isFirstRowHeader = function.Parameters.Get<bool>("isFirstRowHeader");
html.Append($"<table class=\"table");
if (function.Name.Equals("stripedtable", StringComparison.InvariantCultureIgnoreCase))
{
html.Append(" table-striped");
}
if (hasBorder)
{
html.Append(" table-bordered");
}
html.Append($"\">");
var lines = scopeBody.Split(['\n'], StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim()).Where(o => o.Length > 0);
int rowNumber = 0;
foreach (var lineText in lines)
{
var columns = lineText.Split("||");
if (rowNumber == 0 && isFirstRowHeader)
{
html.Append($"<thead>");
}
else if (rowNumber == 1 && isFirstRowHeader || rowNumber == 0 && isFirstRowHeader == false)
{
html.Append($"<tbody>");
}
html.Append($"<tr>");
foreach (var columnText in columns)
{
if (rowNumber == 0 && isFirstRowHeader)
{
html.Append($"<td><strong>{columnText}</strong></td>");
}
else
{
html.Append($"<td>{columnText}</td>");
}
}
if (rowNumber == 0 && isFirstRowHeader)
{
html.Append($"</thead>");
}
html.Append($"</tr>");
rowNumber++;
}
html.Append($"</tbody>");
html.Append($"</table>");
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "bullets":
{
var html = new StringBuilder();
string type = function.Parameters.Get<string>("type");
if (type == "unordered")
{
var lines = scopeBody.Split(['\n'], StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim()).Where(o => o.Length > 0);
int currentLevel = 0;
foreach (var line in lines)
{
int newIndent = 0;
for (; newIndent < line.Length && line[newIndent] == '>'; newIndent++)
{
//Count how many '>' are at the start of the line.
}
newIndent++;
if (newIndent < currentLevel)
{
for (; currentLevel != newIndent; currentLevel--)
{
html.Append($"</ul>");
}
}
else if (newIndent > currentLevel)
{
for (; currentLevel != newIndent; currentLevel++)
{
html.Append($"<ul>");
}
}
html.Append($"<li>{line.Trim(['>'])}</li>");
}
for (; currentLevel > 0; currentLevel--)
{
html.Append($"</ul>");
}
}
else if (type == "ordered")
{
var lines = scopeBody.Split(['\n'], StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim()).Where(o => o.Length > 0);
int currentLevel = 0;
foreach (var line in lines)
{
int newIndent = 0;
for (; newIndent < line.Length && line[newIndent] == '>'; newIndent++)
{
//Count how many '>' are at the start of the line.
}
newIndent++;
if (newIndent < currentLevel)
{
for (; currentLevel != newIndent; currentLevel--)
{
html.Append($"</ol>");
}
}
else if (newIndent > currentLevel)
{
for (; currentLevel != newIndent; currentLevel++)
{
html.Append($"<ol>");
}
}
html.Append($"<li>{line.Trim(['>'])}</li>");
}
for (; currentLevel > 0; currentLevel--)
{
html.Append($"</ol>");
}
}
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "definesnippet":
{
var html = new StringBuilder();
string name = function.Parameters.Get<string>("name");
if (!state.Snippets.TryAdd(name, scopeBody))
{
state.Snippets[name] = scopeBody;
}
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "alert":
{
var html = new StringBuilder();
string titleText = function.Parameters.Get<string>("titleText");
string style = function.Parameters.Get<string>("styleName").ToLower();
style = style == "default" ? "" : $"alert-{style}";
if (!string.IsNullOrEmpty(titleText)) scopeBody = $"<h1>{titleText}</h1>{scopeBody}";
html.Append($"<div class=\"alert {style}\">{scopeBody}</div>");
return new HandlerResult(html.ToString());
}
case "order":
{
var html = new StringBuilder();
string direction = function.Parameters.Get<string>("direction");
var lines = scopeBody.Split("\n").Select(o => o.Trim()).ToList();
if (direction == "ascending")
{
html.Append(string.Join("\r\n", lines.OrderBy(o => o)));
}
else
{
html.Append(string.Join("\r\n", lines.OrderByDescending(o => o)));
}
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "jumbotron":
{
var html = new StringBuilder();
string titleText = function.Parameters.Get("titleText", "");
html.Append($"<div class=\"mt-4 p-5 bg-secondary text-white rounded\">");
if (!string.IsNullOrEmpty(titleText)) html.Append($"<h1>{titleText}</h1>");
html.Append($"<p>{scopeBody}</p>");
html.Append($"</div>");
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "foreground":
{
var html = new StringBuilder();
var style = BGFGStyle.GetForegroundStyle(function.Parameters.Get("styleName", "default")).Swap();
html.Append($"<p class=\"{style.ForegroundStyle} {style.BackgroundStyle}\">{scopeBody}</p>");
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "background":
{
var html = new StringBuilder();
var style = BGFGStyle.GetBackgroundStyle(function.Parameters.Get("styleName", "default"));
html.Append($"<div class=\"p-3 mb-2 {style.ForegroundStyle} {style.BackgroundStyle}\">{scopeBody}</div>");
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "collapse":
{
var html = new StringBuilder();
string linkText = function.Parameters.Get<string>("linktext");
string uid = "A" + Guid.NewGuid().ToString().Replace("-", "");
html.Append($"<a data-bs-toggle=\"collapse\" href=\"#{uid}\" role=\"button\" aria-expanded=\"false\" aria-controls=\"{uid}\">{linkText}</a>");
html.Append($"<div class=\"collapse\" id=\"{uid}\">");
html.Append($"<div class=\"card card-body\"><p class=\"card-text\">{scopeBody}</p></div></div>");
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "callout":
{
var html = new StringBuilder();
string titleText = function.Parameters.Get<string>("titleText");
string style = function.Parameters.Get<string>("styleName").ToLower();
style = style == "default" ? "" : style;
html.Append($"<div class=\"bd-callout bd-callout-{style}\">");
if (string.IsNullOrWhiteSpace(titleText) == false) html.Append($"<h4>{titleText}</h4>");
html.Append($"{scopeBody}");
html.Append($"</div>");
return new HandlerResult(html.ToString());
}
//------------------------------------------------------------------------------------------------------------------------------
case "card":
{
var html = new StringBuilder();
string titleText = function.Parameters.Get<string>("titleText");
var style = BGFGStyle.GetBackgroundStyle(function.Parameters.Get("styleName", "default"));
html.Append($"<div class=\"card {style.ForegroundStyle} {style.BackgroundStyle} mb-3\">");
if (string.IsNullOrEmpty(titleText) == false)
{
html.Append($"<div class=\"card-header\">{titleText}</div>");
}
html.Append("<div class=\"card-body\">");
html.Append($"<p class=\"card-text\">{scopeBody}</p>");
html.Append("</div>");
html.Append("</div>");
return new HandlerResult(html.ToString());
}
}
return new HandlerResult() { Instructions = [HandlerResultInstruction.Skip] };
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>2.20.1</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DebugSymbols>False</DebugSymbols>
<DebugType>None</DebugType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TightWiki.Engine.Library\TightWiki.Engine.Library.csproj" />
<ProjectReference Include="..\TightWiki.Library\TightWiki.Library.csproj" />
<ProjectReference Include="..\TightWiki.Models\TightWiki.Models.csproj" />
<ProjectReference Include="..\TightWiki.Repository\TightWiki.Repository.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,70 @@
namespace TightWiki.Engine.Implementation.Utility
{
public class BGFGStyle
{
public string ForegroundStyle { get; set; } = String.Empty;
public string BackgroundStyle { get; set; } = String.Empty;
public BGFGStyle(string foregroundStyle, string backgroundStyle)
{
ForegroundStyle = foregroundStyle;
BackgroundStyle = backgroundStyle;
}
public BGFGStyle Swap()
{
return new BGFGStyle(BackgroundStyle, ForegroundStyle);
}
public BGFGStyle()
{
}
public static readonly Dictionary<string, BGFGStyle> ForegroundStyles = new(StringComparer.OrdinalIgnoreCase)
{
{ "primary", new BGFGStyle("text-primary", "") },
{ "secondary", new BGFGStyle("text-secondary", "") },
{ "success", new BGFGStyle("text-success", "") },
{ "danger", new BGFGStyle("text-danger", "") },
{ "warning", new BGFGStyle("text-warning", "") },
{ "info", new BGFGStyle("text-info", "") },
{ "light", new BGFGStyle("text-light", "") },
{ "dark", new BGFGStyle("text-dark", "") },
{ "muted", new BGFGStyle("text-muted", "") },
{ "white", new BGFGStyle("text-white", "bg-dark") }
};
public static readonly Dictionary<string, BGFGStyle> BackgroundStyles = new(StringComparer.OrdinalIgnoreCase)
{
{ "muted", new BGFGStyle("text-muted", "") },
{ "primary", new BGFGStyle("text-white", "bg-primary") },
{ "secondary", new BGFGStyle("text-white", "bg-secondary") },
{ "info", new BGFGStyle("text-white", "bg-info") },
{ "success", new BGFGStyle("text-white", "bg-success") },
{ "warning", new BGFGStyle("bg-warning", "") },
{ "danger", new BGFGStyle("text-white", "bg-danger") },
{ "light", new BGFGStyle("text-black", "bg-light") },
{ "dark", new BGFGStyle("text-white", "bg-dark") }
};
public static BGFGStyle GetBackgroundStyle(string style)
{
if (BackgroundStyles.TryGetValue(style, out var html))
{
return html;
}
return new BGFGStyle();
}
public static BGFGStyle GetForegroundStyle(string style)
{
if (ForegroundStyles.TryGetValue(style, out var html))
{
return html;
}
return new BGFGStyle();
}
}
}

View File

@@ -0,0 +1,50 @@
using System.Text;
namespace TightWiki.Engine.Implementation.Utility
{
public static class Differentiator
{
/// <summary>
/// This leaves a lot to be desired.
/// </summary>
/// <param name="thisRev"></param>
/// <param name="prevRev"></param>
/// <returns></returns>
public static string GetComparisonSummary(string thisRev, string prevRev)
{
var summary = new StringBuilder();
var thisRevLines = thisRev.Split('\n');
var prevRevLines = prevRev.Split('\n');
int thisRevLineCount = thisRevLines.Length;
int prevRevLinesCount = prevRevLines.Length;
int linesAdded = prevRevLines.Except(thisRevLines).Count();
int linesDeleted = thisRevLines.Except(prevRevLines).Count();
if (thisRevLineCount != prevRevLinesCount)
{
summary.Append($"{Math.Abs(thisRevLineCount - prevRevLinesCount):N0} lines changed.");
}
if (linesAdded > 0)
{
if (summary.Length > 0) summary.Append(' ');
summary.Append($"{linesAdded:N0} lines added.");
}
if (linesDeleted > 0)
{
if (summary.Length > 0) summary.Append(' ');
summary.Append($"{linesDeleted:N0} lines deleted.");
}
if (summary.Length == 0)
{
summary.Append($"No changes detected.");
}
return summary.ToString();
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Text;
using TightWiki.Models;
using TightWiki.Models.DataModels;
using TightWiki.Repository;
namespace TightWiki.Engine.Implementation.Utility
{
public class SearchCloud
{
public static string Build(List<string> searchTokens, int? maxCount = null)
{
var pages = PageRepository.PageSearch(searchTokens).OrderByDescending(o => o.Score).ToList();
if (maxCount > 0)
{
pages = pages.Take((int)maxCount).ToList();
}
int pageCount = pages.Count;
int fontSize = 7;
int sizeStep = (pageCount > fontSize ? pageCount : (fontSize * 2)) / fontSize;
int pageIndex = 0;
var pageList = new List<TagCloudItem>();
foreach (var page in pages)
{
pageList.Add(new TagCloudItem(page.Name, pageIndex, "<font size=\"" + fontSize + $"\"><a href=\"{GlobalConfiguration.BasePath}/" + page.Navigation + "\">" + page.Name + "</a></font>"));
if ((pageIndex % sizeStep) == 0)
{
fontSize--;
}
pageIndex++;
}
var cloudHtml = new StringBuilder();
pageList.Sort(TagCloudItem.CompareItem);
cloudHtml.Append("<table align=\"center\" border=\"0\" width=\"100%\"><tr><td><p align=\"justify\">");
foreach (TagCloudItem tag in pageList)
{
cloudHtml.Append(tag.HTML + "&nbsp; ");
}
cloudHtml.Append("</p></td></tr></table>");
return cloudHtml.ToString();
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Text;
using TightWiki.Library;
using TightWiki.Models;
using TightWiki.Models.DataModels;
using TightWiki.Repository;
namespace TightWiki.Engine.Implementation.Utility
{
public static class TagCloud
{
public static string Build(string seedTag, int? maxCount)
{
var tags = PageRepository.GetAssociatedTags(seedTag).OrderByDescending(o => o.PageCount).ToList();
if (maxCount > 0)
{
tags = tags.Take((int)maxCount).ToList();
}
int tagCount = tags.Count;
int fontSize = 7;
int sizeStep = (tagCount > fontSize ? tagCount : (fontSize * 2)) / fontSize;
int tagIndex = 0;
var tagList = new List<TagCloudItem>();
foreach (var tag in tags)
{
tagList.Add(new TagCloudItem(tag.Tag, tagIndex, "<font size=\"" + fontSize + $"\"><a href=\"{GlobalConfiguration.BasePath}/Tag/Browse/" + NamespaceNavigation.CleanAndValidate(tag.Tag) + "\">" + tag.Tag + "</a></font>"));
if ((tagIndex % sizeStep) == 0)
{
fontSize--;
}
tagIndex++;
}
var cloudHtml = new StringBuilder();
tagList.Sort(TagCloudItem.CompareItem);
cloudHtml.Append("<table align=\"center\" border=\"0\" width=\"100%\"><tr><td><p align=\"justify\">");
foreach (TagCloudItem tag in tagList)
{
cloudHtml.Append(tag.HTML + "&nbsp; ");
}
cloudHtml.Append("</p></td></tr></table>");
return cloudHtml.ToString();
}
}
}

View File

@@ -0,0 +1,8 @@
namespace TightWiki.Engine.Implementation
{
public class WeightedSearchToken
{
public string Token { get; set; } = string.Empty;
public double Weight { get; set; }
}
}