添加项目文件。
100
.gitignore
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
|
||||||
|
TightWiki/bin/
|
||||||
|
|
||||||
|
TightWiki/obj/
|
||||||
|
|
||||||
|
*.user
|
||||||
|
|
||||||
|
TightWiki.Library/obj/
|
||||||
|
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
TightWiki.Library/bin/
|
||||||
|
|
||||||
|
DatabaseMigration/bin/
|
||||||
|
|
||||||
|
DatabaseMigration/obj/
|
||||||
|
|
||||||
|
TightWiki.DataModel/bin/
|
||||||
|
|
||||||
|
TightWiki.DataModel/obj/
|
||||||
|
|
||||||
|
TightWiki.Shared/bin/
|
||||||
|
|
||||||
|
TightWiki.Shared/obj/
|
||||||
|
|
||||||
|
TightWiki.Repository/bin/
|
||||||
|
|
||||||
|
TightWiki.Repository/obj/
|
||||||
|
|
||||||
|
Mssql2Sqlite/bin/
|
||||||
|
|
||||||
|
Mssql2Sqlite/obj/
|
||||||
|
|
||||||
|
TightWiki.Caching/bin/
|
||||||
|
|
||||||
|
TightWiki.Caching/obj/
|
||||||
|
|
||||||
|
TightWiki.Configuration/bin/
|
||||||
|
|
||||||
|
TightWiki.Configuration/obj/
|
||||||
|
|
||||||
|
TightWiki.Email/bin/
|
||||||
|
|
||||||
|
TightWiki.Email/obj/
|
||||||
|
|
||||||
|
TightWiki.Engine/bin/
|
||||||
|
|
||||||
|
TightWiki.Engine/obj/
|
||||||
|
|
||||||
|
TightWiki.Interfaces/bin/
|
||||||
|
|
||||||
|
TightWiki.Interfaces/obj/
|
||||||
|
|
||||||
|
TightWiki.Models/bin/
|
||||||
|
|
||||||
|
TightWiki.Models/obj/
|
||||||
|
|
||||||
|
TightWiki.Security/bin/
|
||||||
|
|
||||||
|
TightWiki.Security/obj/
|
||||||
|
|
||||||
|
DummyPageGenerator/bin/
|
||||||
|
|
||||||
|
DummyPageGenerator/obj/
|
||||||
|
|
||||||
|
TightWiki.Engine.Function/bin/
|
||||||
|
|
||||||
|
TightWiki.Engine.Function/obj/
|
||||||
|
|
||||||
|
MarkdigTester/
|
||||||
|
|
||||||
|
TightWiki.Engine.Types/obj/
|
||||||
|
|
||||||
|
TightWiki.Engine.Types/bin/
|
||||||
|
|
||||||
|
TightWiki.Engine.Implementation/bin/
|
||||||
|
|
||||||
|
TightWiki.Engine.Implementation/obj/
|
||||||
|
|
||||||
|
TightWiki.Engine.Factory/bin/
|
||||||
|
|
||||||
|
TightWiki.Engine.Factory/obj/
|
||||||
|
|
||||||
|
TightWiki.Engine.Function.Handlers/obj/
|
||||||
|
|
||||||
|
TightWiki.Engine.Handlers/bin/
|
||||||
|
|
||||||
|
TightWiki.Engine.Handlers/obj/
|
||||||
|
|
||||||
|
TightWiki.Engine.Library/bin/
|
||||||
|
|
||||||
|
TightWiki.Engine.Library/obj/
|
||||||
|
|
||||||
|
TightWiki.Wiki/bin/
|
||||||
|
|
||||||
|
TightWiki.Wiki/obj/
|
||||||
|
|
||||||
|
DumpConfiguration/bin/
|
||||||
|
|
||||||
|
DumpConfiguration/obj/
|
||||||
BIN
@Utility/AspnetIdentityScaffolding.zip
Normal file
141
@Utility/Custom Highlighter/tightwiki.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
Language: TightWiki
|
||||||
|
Description: Highlighter for TightWiki markup.
|
||||||
|
Authors: Josh Patterson <Josh@NTDLS.com>
|
||||||
|
Website: https://networkdls.com/
|
||||||
|
Category: common
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type LanguageFn */
|
||||||
|
export default function(hljs) {
|
||||||
|
const regex = hljs.regex;
|
||||||
|
/**
|
||||||
|
* Character Literal
|
||||||
|
* Either a single character ("a"C) or an escaped double quote (""""C).
|
||||||
|
*/
|
||||||
|
const CHARACTER = {
|
||||||
|
className: 'string',
|
||||||
|
begin: /"(""|[^/n])"C\b/
|
||||||
|
};
|
||||||
|
|
||||||
|
const STRING = {
|
||||||
|
className: 'string',
|
||||||
|
begin: /"/,
|
||||||
|
end: /"/,
|
||||||
|
illegal: /\n/,
|
||||||
|
contains: [
|
||||||
|
{
|
||||||
|
// double quote escape
|
||||||
|
begin: /""/ }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Date Literals consist of a date, a time, or both separated by whitespace, surrounded by # */
|
||||||
|
const MM_DD_YYYY = /\d{1,2}\/\d{1,2}\/\d{4}/;
|
||||||
|
const YYYY_MM_DD = /\d{4}-\d{1,2}-\d{1,2}/;
|
||||||
|
const TIME_12H = /(\d|1[012])(:\d+){0,2} *(AM|PM)/;
|
||||||
|
const TIME_24H = /\d{1,2}(:\d{1,2}){1,2}/;
|
||||||
|
const DATE = {
|
||||||
|
className: 'literal',
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
// #YYYY-MM-DD# (ISO-Date) or #M/D/YYYY# (US-Date)
|
||||||
|
begin: regex.concat(/# */, regex.either(YYYY_MM_DD, MM_DD_YYYY), / *#/) },
|
||||||
|
{
|
||||||
|
// #H:mm[:ss]# (24h Time)
|
||||||
|
begin: regex.concat(/# */, TIME_24H, / *#/) },
|
||||||
|
{
|
||||||
|
// #h[:mm[:ss]] A# (12h Time)
|
||||||
|
begin: regex.concat(/# */, TIME_12H, / *#/) },
|
||||||
|
{
|
||||||
|
// date plus time
|
||||||
|
begin: regex.concat(
|
||||||
|
/# */,
|
||||||
|
regex.either(YYYY_MM_DD, MM_DD_YYYY),
|
||||||
|
/ +/,
|
||||||
|
regex.either(TIME_12H, TIME_24H),
|
||||||
|
/ *#/
|
||||||
|
) }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const NUMBER = {
|
||||||
|
className: 'number',
|
||||||
|
relevance: 0,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
// Float
|
||||||
|
begin: /\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ },
|
||||||
|
{
|
||||||
|
// Integer (base 10)
|
||||||
|
begin: /\b\d[\d_]*((U?[SIL])|[%&])?/ },
|
||||||
|
{
|
||||||
|
// Integer (base 16)
|
||||||
|
begin: /&H[\dA-F_]+((U?[SIL])|[%&])?/ },
|
||||||
|
{
|
||||||
|
// Integer (base 8)
|
||||||
|
begin: /&O[0-7_]+((U?[SIL])|[%&])?/ },
|
||||||
|
{
|
||||||
|
// Integer (base 2)
|
||||||
|
begin: /&B[01_]+((U?[SIL])|[%&])?/ }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const LABEL = {
|
||||||
|
className: 'label',
|
||||||
|
begin: /^\w+:/
|
||||||
|
};
|
||||||
|
|
||||||
|
const DOC_COMMENT = hljs.COMMENT(/'''/, /$/, { contains: [
|
||||||
|
{
|
||||||
|
className: 'doctag',
|
||||||
|
begin: /<\/?/,
|
||||||
|
end: />/
|
||||||
|
}
|
||||||
|
] });
|
||||||
|
|
||||||
|
const COMMENT = hljs.COMMENT(null, /$/, { variants: [
|
||||||
|
{ begin: /'/ },
|
||||||
|
{
|
||||||
|
// TODO: Use multi-class for leading spaces
|
||||||
|
begin: /([\t ]|^)REM(?=\s)/ }
|
||||||
|
] });
|
||||||
|
|
||||||
|
const DIRECTIVES = {
|
||||||
|
className: 'meta',
|
||||||
|
// TODO: Use multi-class for indentation once available
|
||||||
|
begin: /[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/,
|
||||||
|
end: /$/,
|
||||||
|
keywords: { keyword:
|
||||||
|
'const disable else elseif enable end externalsource if region then' },
|
||||||
|
contains: [ COMMENT ]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'TightWiki',
|
||||||
|
aliases: [ 'wiki' ],
|
||||||
|
case_insensitive: true,
|
||||||
|
classNameAliases: { label: 'symbol' },
|
||||||
|
keywords: {
|
||||||
|
keyword:
|
||||||
|
'NamespaceGlossary NamespaceList Namespace Code Bullets Table StripedTable Jumbotron Callout Background Foreground Alert Card Collapse Tag SearchList TagList SearchCloud TagGlossary RecentlyModified TextGlossary TagCloud Image File Related Tags EditLink Include Inject BR NL HR NewLine History Attachments TOC Title Navigation Name Created LastModified AppVersion Deprecate Protect Template Review Include HideFooterComments NoCache Draft Order definesnippet snippet',
|
||||||
|
built_in:
|
||||||
|
'infinite',
|
||||||
|
type:
|
||||||
|
'boolean integer string decimal',
|
||||||
|
literal: 'true false null'
|
||||||
|
},
|
||||||
|
illegal:
|
||||||
|
'//|\\{|\\}|endif|gosub|variant|wend|^\\$ ' /* reserved deprecated keywords */,
|
||||||
|
contains: [
|
||||||
|
CHARACTER,
|
||||||
|
STRING,
|
||||||
|
DATE,
|
||||||
|
NUMBER,
|
||||||
|
LABEL,
|
||||||
|
DOC_COMMENT,
|
||||||
|
COMMENT,
|
||||||
|
DIRECTIVES
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
266
@Utility/tightwikidata.db.sql
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
CREATE TABLE IF NOT EXISTS "AspNetUserTokens" (
|
||||||
|
"UserId" TEXT NOT NULL,
|
||||||
|
"LoginProvider" TEXT NOT NULL,
|
||||||
|
"Name" TEXT NOT NULL,
|
||||||
|
"Value" TEXT,
|
||||||
|
CONSTRAINT "FK_AspNetUserTokens_AspNetUsers_UserId" FOREIGN KEY("UserId") REFERENCES "AspNetUsers"("Id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "PK_AspNetUserTokens" PRIMARY KEY("UserId","LoginProvider","Name")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "AspNetUsers" (
|
||||||
|
"Id" TEXT NOT NULL,
|
||||||
|
"AccessFailedCount" INTEGER NOT NULL,
|
||||||
|
"ConcurrencyStamp" TEXT,
|
||||||
|
"Email" TEXT,
|
||||||
|
"EmailConfirmed" INTEGER NOT NULL,
|
||||||
|
"LockoutEnabled" INTEGER NOT NULL,
|
||||||
|
"LockoutEnd" TEXT,
|
||||||
|
"NormalizedEmail" TEXT,
|
||||||
|
"NormalizedUserName" TEXT,
|
||||||
|
"PasswordHash" TEXT,
|
||||||
|
"PhoneNumber" TEXT,
|
||||||
|
"PhoneNumberConfirmed" INTEGER NOT NULL,
|
||||||
|
"SecurityStamp" TEXT,
|
||||||
|
"TwoFactorEnabled" INTEGER NOT NULL,
|
||||||
|
"UserName" TEXT,
|
||||||
|
CONSTRAINT "PK_AspNetUsers" PRIMARY KEY("Id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "AspNetUserRoles" (
|
||||||
|
"UserId" TEXT NOT NULL,
|
||||||
|
"RoleId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "FK_AspNetUserRoles_AspNetRoles_RoleId" FOREIGN KEY("RoleId") REFERENCES "AspNetRoles"("Id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "FK_AspNetUserRoles_AspNetUsers_UserId" FOREIGN KEY("UserId") REFERENCES "AspNetUsers"("Id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "PK_AspNetUserRoles" PRIMARY KEY("UserId","RoleId")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "AspNetUserLogins" (
|
||||||
|
"LoginProvider" TEXT NOT NULL,
|
||||||
|
"ProviderKey" TEXT NOT NULL,
|
||||||
|
"ProviderDisplayName" TEXT,
|
||||||
|
"UserId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "FK_AspNetUserLogins_AspNetUsers_UserId" FOREIGN KEY("UserId") REFERENCES "AspNetUsers"("Id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "PK_AspNetUserLogins" PRIMARY KEY("LoginProvider","ProviderKey")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "AspNetUserClaims" (
|
||||||
|
"Id" INTEGER NOT NULL,
|
||||||
|
"ClaimType" TEXT,
|
||||||
|
"ClaimValue" TEXT,
|
||||||
|
"UserId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "FK_AspNetUserClaims_AspNetUsers_UserId" FOREIGN KEY("UserId") REFERENCES "AspNetUsers"("Id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "PK_AspNetUserClaims" PRIMARY KEY("Id" AUTOINCREMENT)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "AspNetRoles" (
|
||||||
|
"Id" TEXT NOT NULL,
|
||||||
|
"ConcurrencyStamp" TEXT,
|
||||||
|
"Name" TEXT,
|
||||||
|
"NormalizedName" TEXT,
|
||||||
|
CONSTRAINT "PK_AspNetRoles" PRIMARY KEY("Id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "AspNetRoleClaims" (
|
||||||
|
"Id" INTEGER NOT NULL,
|
||||||
|
"ClaimType" TEXT,
|
||||||
|
"ClaimValue" TEXT,
|
||||||
|
"RoleId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "FK_AspNetRoleClaims_AspNetRoles_RoleId" FOREIGN KEY("RoleId") REFERENCES "AspNetRoles"("Id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "PK_AspNetRoleClaims" PRIMARY KEY("Id" AUTOINCREMENT)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "ConfigurationEntry" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"ConfigurationGroupId" int NOT NULL,
|
||||||
|
"Name" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Value" text,
|
||||||
|
"DataTypeId" int NOT NULL,
|
||||||
|
"Description" nvarchar(1000) COLLATE NOCASE,
|
||||||
|
"IsEncrypted" bit NOT NULL,
|
||||||
|
CONSTRAINT "PK_ConfigurationEntry" PRIMARY KEY("ConfigurationGroupId" ASC,"Name" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "ConfigurationGroup" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"Name" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Description" nvarchar(1000) COLLATE NOCASE,
|
||||||
|
CONSTRAINT "PK_ConfigurationGroup" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "CryptoCheck" (
|
||||||
|
"Content" blob
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "DataType" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"Name" nvarchar(50) COLLATE NOCASE,
|
||||||
|
CONSTRAINT "PK_DataType" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "Emoji" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"Name" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"ImageData" blob,
|
||||||
|
"MimeType" nvarchar(50) COLLATE NOCASE,
|
||||||
|
CONSTRAINT "IX_Emoji" UNIQUE("Name" ASC),
|
||||||
|
CONSTRAINT "PK_Emoji" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "EmojiCategory" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"EmojiId" int NOT NULL,
|
||||||
|
"Category" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
CONSTRAINT "IX_EmojiCategory" UNIQUE("EmojiId" ASC,"Category" ASC),
|
||||||
|
CONSTRAINT "PK_EmojiCategory" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "Exception" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"Text" text,
|
||||||
|
"ExceptionText" text,
|
||||||
|
"StackTrace" text,
|
||||||
|
"CreatedDate" datetime,
|
||||||
|
CONSTRAINT "PK_Exceptions" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "MenuItem" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"Name" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Link" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Ordinal" int NOT NULL,
|
||||||
|
CONSTRAINT "PK_MenuItem" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "Page" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"Name" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Namespace" nvarchar(128) COLLATE NOCASE,
|
||||||
|
"Navigation" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Description" text,
|
||||||
|
"Revision" int NOT NULL,
|
||||||
|
"CreatedByUserId" int NOT NULL,
|
||||||
|
"CreatedDate" datetime NOT NULL,
|
||||||
|
"ModifiedByUserId" int NOT NULL,
|
||||||
|
"ModifiedDate" datetime NOT NULL,
|
||||||
|
CONSTRAINT "PK_Page" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageComment" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"CreatedDate" datetime NOT NULL,
|
||||||
|
"UserId" int NOT NULL,
|
||||||
|
"Body" text NOT NULL,
|
||||||
|
CONSTRAINT "PK_PageComment" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageFileRevision" (
|
||||||
|
"PageFileId" int NOT NULL,
|
||||||
|
"ContentType" nvarchar(100) NOT NULL COLLATE NOCASE,
|
||||||
|
"Size" int NOT NULL,
|
||||||
|
"CreatedDate" datetime NOT NULL,
|
||||||
|
"Data" blob NOT NULL,
|
||||||
|
"Revision" int NOT NULL,
|
||||||
|
"DataHash" int NOT NULL,
|
||||||
|
CONSTRAINT "PK_PageFileRevision_1" PRIMARY KEY("PageFileId" ASC,"Revision" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageProcessingInstruction" (
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"Instruction" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
CONSTRAINT "PK_ProcessingInstruction" PRIMARY KEY("PageId" ASC,"Instruction" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageReference" (
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"ReferencesPageName" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"ReferencesPageNavigation" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"ReferencesPageId" int,
|
||||||
|
CONSTRAINT "PK_PageReference" PRIMARY KEY("PageId" ASC,"ReferencesPageNavigation" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageRevision" (
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"Name" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Namespace" nvarchar(128) COLLATE NOCASE,
|
||||||
|
"Description" text NOT NULL,
|
||||||
|
"Body" text NOT NULL,
|
||||||
|
"Revision" int NOT NULL,
|
||||||
|
"ModifiedByUserId" int NOT NULL,
|
||||||
|
"ModifiedDate" datetime NOT NULL,
|
||||||
|
"DataHash" int NOT NULL,
|
||||||
|
CONSTRAINT "PK_PageRevision" PRIMARY KEY("PageId" ASC,"Revision" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageRevisionAttachment" (
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"PageFileId" int NOT NULL,
|
||||||
|
"FileRevision" int NOT NULL,
|
||||||
|
"PageRevision" int NOT NULL,
|
||||||
|
CONSTRAINT "PK_PageRevisionAttachment" PRIMARY KEY("PageId" ASC,"PageFileId" ASC,"FileRevision" ASC,"PageRevision" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageStatistics" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"CreatedDate" datetime NOT NULL,
|
||||||
|
"WikifyTimeMs" decimal(8, 2),
|
||||||
|
"MatchCount" int,
|
||||||
|
"ErrorCount" int,
|
||||||
|
"OutgoingLinkCount" int,
|
||||||
|
"TagCount" int,
|
||||||
|
"ProcessedBodySize" int,
|
||||||
|
"BodySize" int,
|
||||||
|
CONSTRAINT "PK_PageStatistics" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageTag" (
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"Tag" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
CONSTRAINT "PK_PageTag" PRIMARY KEY("PageId" ASC,"Tag" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageToken" (
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"Token" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Weight" decimal(6, 2) NOT NULL,
|
||||||
|
"DoubleMetaphone" varchar(16) NOT NULL,
|
||||||
|
CONSTRAINT "PK_PageToken" PRIMARY KEY("PageId" ASC,"Token" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "Role" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"Name" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Description" nvarchar(1000) COLLATE NOCASE,
|
||||||
|
CONSTRAINT "PK_Role" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "User" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"EmailAddress" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"AccountName" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Navigation" nvarchar(128) COLLATE NOCASE,
|
||||||
|
"PasswordHash" nvarchar(128) COLLATE NOCASE,
|
||||||
|
"FirstName" nvarchar(128) COLLATE NOCASE,
|
||||||
|
"LastName" nvarchar(128) COLLATE NOCASE,
|
||||||
|
"Country" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"Language" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"TimeZone" nvarchar(128) NOT NULL COLLATE NOCASE,
|
||||||
|
"AboutMe" text,
|
||||||
|
"Avatar" blob,
|
||||||
|
"CreatedDate" datetime NOT NULL,
|
||||||
|
"ModifiedDate" datetime NOT NULL,
|
||||||
|
"LastLoginDate" datetime NOT NULL,
|
||||||
|
"VerificationCode" varchar(20),
|
||||||
|
"EmailVerified" bit NOT NULL,
|
||||||
|
"RoleId" int NOT NULL,
|
||||||
|
"Provider" varchar(20) NOT NULL,
|
||||||
|
"Deleted" bit NOT NULL,
|
||||||
|
CONSTRAINT "PK_User" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "PageFile" (
|
||||||
|
"Id" int IDENTITY(1, 1) NOT NULL,
|
||||||
|
"PageId" int NOT NULL,
|
||||||
|
"Name" nvarchar(250) NOT NULL COLLATE NOCASE,
|
||||||
|
"Navigation" nvarchar(250) NOT NULL COLLATE NOCASE,
|
||||||
|
"Revision" int NOT NULL,
|
||||||
|
"CreatedDate" datetime NOT NULL,
|
||||||
|
CONSTRAINT "PK_PageFile" PRIMARY KEY("Id" ASC)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "EmailIndex" ON "AspNetUsers" (
|
||||||
|
"NormalizedEmail"
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "UserNameIndex" ON "AspNetUsers" (
|
||||||
|
"NormalizedUserName"
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_AspNetUserRoles_RoleId" ON "AspNetUserRoles" (
|
||||||
|
"RoleId"
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_AspNetUserLogins_UserId" ON "AspNetUserLogins" (
|
||||||
|
"UserId"
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_AspNetUserClaims_UserId" ON "AspNetUserClaims" (
|
||||||
|
"UserId"
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "RoleNameIndex" ON "AspNetRoles" (
|
||||||
|
"NormalizedName"
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_AspNetRoleClaims_RoleId" ON "AspNetRoleClaims" (
|
||||||
|
"RoleId"
|
||||||
|
);
|
||||||
|
COMMIT;
|
||||||
BIN
Data/config.db
Normal file
26
Data/databases.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
## Config.db
|
||||||
|
The configuration for the site. Such as site name, email setup, customizations, etc.
|
||||||
|
|
||||||
|
## DeletedPageRevisions.db
|
||||||
|
When page revisions are deleted, they are moved to this database.
|
||||||
|
|
||||||
|
## DeletedPages.db
|
||||||
|
When entire pages are deleted, they are moved to this database along with all of their file attachments.
|
||||||
|
|
||||||
|
## Emoji.db
|
||||||
|
Contains all emoji image binaries and their tags.
|
||||||
|
|
||||||
|
## Exceptions.db
|
||||||
|
Contains site exceptions.
|
||||||
|
|
||||||
|
## Pages.db
|
||||||
|
Contains site pages, their file attachments, tags, processing instructions and search tokens.
|
||||||
|
|
||||||
|
## Statistics.db
|
||||||
|
When enabled, contains the wiki compilation statistics.
|
||||||
|
|
||||||
|
## Users.db
|
||||||
|
Contains user accounts and profiles, this is for local login and 3rd party authentication providers.
|
||||||
|
|
||||||
|
## Words.db
|
||||||
|
Contains a list of words than can be used for generating random strings (such as user profile names).
|
||||||
BIN
Data/deletedpagerevisions.db
Normal file
BIN
Data/deletedpages.db
Normal file
BIN
Data/emoji.db
Normal file
BIN
Data/exceptions.db
Normal file
BIN
Data/pages.db
Normal file
BIN
Data/statistics.db
Normal file
BIN
Data/users.db
Normal file
BIN
DummyPageGenerator/Data/words.db
Normal file
46
DummyPageGenerator/DummyPageGenerator.csproj
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<DebugSymbols>False</DebugSymbols>
|
||||||
|
<DebugType>None</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="appsettings.Development.json" />
|
||||||
|
<None Remove="appsettings.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="appsettings.Development.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
|
||||||
|
<PackageReference Include="NTDLS.DelegateThreadPooling" Version="1.5.12" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\TightWiki.Engine.Implementation\TightWiki.Engine.Implementation.csproj" />
|
||||||
|
<ProjectReference Include="..\TightWiki.Engine\TightWiki.Engine.csproj" />
|
||||||
|
<ProjectReference Include="..\TightWiki.Models\TightWiki.Models.csproj" />
|
||||||
|
<ProjectReference Include="..\TightWiki.Repository\TightWiki.Repository.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
399
DummyPageGenerator/PageGenerator.cs
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using NTDLS.Helpers;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using TightWiki.Engine.Library.Interfaces;
|
||||||
|
using TightWiki.Library;
|
||||||
|
using TightWiki.Models;
|
||||||
|
using TightWiki.Models.DataModels;
|
||||||
|
using TightWiki.Repository;
|
||||||
|
|
||||||
|
namespace DummyPageGenerator
|
||||||
|
{
|
||||||
|
internal class PageGenerator
|
||||||
|
{
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
private List<Page> _pagePool;
|
||||||
|
private readonly Random _random;
|
||||||
|
private readonly List<string> _namespaces;
|
||||||
|
private readonly List<string> _tags;
|
||||||
|
private readonly List<string> _fileNames;
|
||||||
|
private List<string> _recentPageNames = new();
|
||||||
|
private readonly UserManager<IdentityUser> _userManager;
|
||||||
|
private readonly List<AccountProfile> _users;
|
||||||
|
|
||||||
|
public List<AccountProfile> Users => _users;
|
||||||
|
public Random Random => _random;
|
||||||
|
|
||||||
|
public PageGenerator(UserManager<IdentityUser> userManager)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_random = new Random();
|
||||||
|
|
||||||
|
_namespaces = PageRepository.GetAllNamespaces();
|
||||||
|
_tags = WordsRepository.GetRandomWords(250);
|
||||||
|
_fileNames = WordsRepository.GetRandomWords(50);
|
||||||
|
_pagePool = PageRepository.GetAllPages();
|
||||||
|
|
||||||
|
if (_namespaces.Count < 250)
|
||||||
|
{
|
||||||
|
_namespaces.AddRange(WordsRepository.GetRandomWords(250));
|
||||||
|
}
|
||||||
|
|
||||||
|
_users = UsersRepository.GetAllUsers();
|
||||||
|
|
||||||
|
if (_users.Count < 1124)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 1124 - _users.Count; i++)
|
||||||
|
{
|
||||||
|
string emailAddress = WordsRepository.GetRandomWords(1).First() + "@" + WordsRepository.GetRandomWords(1).First() + ".com";
|
||||||
|
CreateUserAndProfile(emailAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
_users = UsersRepository.GetAllUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetRandomUnusedAccountName()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var randomAccountName = string.Join(" ", WordsRepository.GetRandomWords(2));
|
||||||
|
if (UsersRepository.DoesProfileAccountExist(Navigation.Clean(randomAccountName)) == false)
|
||||||
|
{
|
||||||
|
return randomAccountName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a user and the associated profile with claims and such.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="emailAddress"></param>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public void CreateUserAndProfile(string emailAddress)
|
||||||
|
{
|
||||||
|
var user = new IdentityUser()
|
||||||
|
{
|
||||||
|
UserName = emailAddress,
|
||||||
|
Email = emailAddress
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _userManager.CreateAsync(user, WordsRepository.GetRandomWords(1).First() + Guid.NewGuid().ToString()).Result;
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
throw new Exception(string.Join("\r\n", result.Errors.Select(o => o.Description)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = _userManager.GetUserIdAsync(user).Result;
|
||||||
|
var membershipConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Membership");
|
||||||
|
|
||||||
|
UsersRepository.CreateProfile(Guid.Parse(userId), GetRandomUnusedAccountName());
|
||||||
|
|
||||||
|
var claimsToAdd = new List<Claim>
|
||||||
|
{
|
||||||
|
new (ClaimTypes.Role, membershipConfig.Value<string>("Default Signup Role").EnsureNotNull()),
|
||||||
|
new ("timezone", membershipConfig.Value<string>("Default TimeZone").EnsureNotNull()),
|
||||||
|
new (ClaimTypes.Country, membershipConfig.Value<string>("Default Country").EnsureNotNull()),
|
||||||
|
new ("language", membershipConfig.Value<string>("Default Language").EnsureNotNull()),
|
||||||
|
};
|
||||||
|
|
||||||
|
SecurityRepository.UpsertUserClaims(_userManager, user, claimsToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a paragraph/sentence structure.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="words"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string GetParagraph(int words)
|
||||||
|
{
|
||||||
|
using var client = new HttpClient();
|
||||||
|
|
||||||
|
var response = client.GetAsync($"https://textsauce.com/api/Paragraph/English/{words}").Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
return response.Content.ReadAsStringAsync().Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a paragraph/sentence structure with links and wiki markup.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="wordCount"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string GenerateWikiParagraph(int wordCount)
|
||||||
|
{
|
||||||
|
var paragraph = GetParagraph(wordCount);
|
||||||
|
var tokens = paragraph.Split(' ');
|
||||||
|
|
||||||
|
int replacementCount = _random.Next(2, 10);
|
||||||
|
|
||||||
|
for (int r = 0; r < replacementCount; r++)
|
||||||
|
{
|
||||||
|
var token = tokens[_random.Next(0, tokens.Length)];
|
||||||
|
|
||||||
|
switch (_random.Next(0, 7))
|
||||||
|
{
|
||||||
|
case 2: //Dead link.
|
||||||
|
paragraph = paragraph.Replace(token, $"[[{token}]]");
|
||||||
|
break;
|
||||||
|
case 4: //Wiki markup.
|
||||||
|
paragraph = paragraph.Replace(token, AddWikiMarkup(token));
|
||||||
|
break;
|
||||||
|
case 6: //Good link.
|
||||||
|
var recentPage = GetRandomRecentPageName();
|
||||||
|
if (recentPage != null)
|
||||||
|
{
|
||||||
|
paragraph = paragraph.Replace(token, $"[[{recentPage}]]");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paragraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetRandomRecentPageName()
|
||||||
|
{
|
||||||
|
lock (_pagePool)
|
||||||
|
{
|
||||||
|
if (_recentPageNames.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_recentPageNames.Count > 200) //Shuffle and limit the recent page names.
|
||||||
|
{
|
||||||
|
_recentPageNames = ShuffleList(_recentPageNames).Take(100).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _recentPageNames[_random.Next(0, _recentPageNames.Count)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetRandomRecentPageNames(int count)
|
||||||
|
{
|
||||||
|
lock (_pagePool)
|
||||||
|
{
|
||||||
|
if (_recentPageNames.Count > 200) //Shuffle and limit the recent page names.
|
||||||
|
{
|
||||||
|
_recentPageNames = ShuffleList(_recentPageNames).Take(100).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageNames = new List<string>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
pageNames.Add(_recentPageNames[_random.Next(0, _recentPageNames.Count)]);
|
||||||
|
}
|
||||||
|
return pageNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRecentPageName(string pageName)
|
||||||
|
{
|
||||||
|
lock (_pagePool)
|
||||||
|
{
|
||||||
|
_recentPageNames.Add(pageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a random page on the wiki.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
public void GeneratePage(ITightEngine engine, Guid userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{userId} is creating a page.");
|
||||||
|
|
||||||
|
var ns = _namespaces[_random.Next(_namespaces.Count)];
|
||||||
|
|
||||||
|
var pageName = ns + " :: " + string.Join(" ", WordsRepository.GetRandomWords(3));
|
||||||
|
|
||||||
|
AddRecentPageName(pageName);
|
||||||
|
|
||||||
|
var body = new StringBuilder();
|
||||||
|
|
||||||
|
body.AppendLine($"##title ##Tag(" + string.Join(' ', ShuffleList(_tags).Take(_random.Next(1, 4))) + ")");
|
||||||
|
body.AppendLine($"##toc");
|
||||||
|
|
||||||
|
body.AppendLine($"==Overview");
|
||||||
|
body.AppendLine(GenerateWikiParagraph(_random.Next(50, 100)));
|
||||||
|
body.AppendLine("\r\n");
|
||||||
|
|
||||||
|
body.AppendLine($"==Revision Section");
|
||||||
|
body.AppendLine($"This is here for the workload generator to easily modify the page.");
|
||||||
|
body.AppendLine($"PLACEHOLDER_FOR_REVISION_TEXT_BEGIN\r\nPLACEHOLDER_FOR_REVISION_TEXT_END\r\n");
|
||||||
|
|
||||||
|
var textWithLinks = WordsRepository.GetRandomWords(_random.Next(5, 10));
|
||||||
|
textWithLinks.AddRange(GetRandomRecentPageNames(_random.Next(1, 2)).Select(o => $"[[{o}]]"));
|
||||||
|
|
||||||
|
if (_random.Next(100) >= 95)
|
||||||
|
{
|
||||||
|
//Add dead links (missing pages).
|
||||||
|
textWithLinks.AddRange(WordsRepository.GetRandomWords(_random.Next(1, 2)).Select(o => $"[[{o}]]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
body.AppendLine($"==See Also");
|
||||||
|
body.AppendLine(string.Join(' ', ShuffleList(textWithLinks)));
|
||||||
|
body.AppendLine("\r\n");
|
||||||
|
|
||||||
|
body.AppendLine($"==Related");
|
||||||
|
body.AppendLine($"##related");
|
||||||
|
body.AppendLine("\r\n");
|
||||||
|
|
||||||
|
var page = new Page()
|
||||||
|
{
|
||||||
|
Name = pageName,
|
||||||
|
Body = body.ToString(),
|
||||||
|
CreatedByUserId = userId,
|
||||||
|
ModifiedByUserId = userId,
|
||||||
|
CreatedDate = DateTime.UtcNow,
|
||||||
|
ModifiedDate = DateTime.UtcNow,
|
||||||
|
Description = string.Join(' ', WordsRepository.GetRandomWords(_random.Next(3, 5))),
|
||||||
|
};
|
||||||
|
int newPageId = TightWiki.Engine.Implementation.Helpers.UpsertPage(engine, page);
|
||||||
|
|
||||||
|
if (_random.Next(100) >= 70)
|
||||||
|
{
|
||||||
|
var fileName = _fileNames[_random.Next(_fileNames.Count)] + ".txt"; ;
|
||||||
|
var fileData = Encoding.UTF8.GetBytes(page.Body);
|
||||||
|
AttachFile(newPageId, userId, fileName, fileData);
|
||||||
|
}
|
||||||
|
|
||||||
|
InsertPagePool(page);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Page GetRandomPage()
|
||||||
|
{
|
||||||
|
lock (_pagePool)
|
||||||
|
{
|
||||||
|
return _pagePool[_random.Next(0, _pagePool.Count)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InsertPagePool(Page page)
|
||||||
|
{
|
||||||
|
lock (_pagePool)
|
||||||
|
{
|
||||||
|
_pagePool.Add(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifies a random page on the wiki.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
public void ModifyRandomPages(ITightEngine engine, Guid userId)
|
||||||
|
{
|
||||||
|
var pageToModify = GetRandomPage();
|
||||||
|
|
||||||
|
Console.WriteLine($"{userId} is modifying page: {pageToModify.Id}.");
|
||||||
|
|
||||||
|
|
||||||
|
AddRecentPageName(pageToModify.Name);
|
||||||
|
|
||||||
|
string beginTag = "PLACEHOLDER_FOR_REVISION_TEXT_BEGIN";
|
||||||
|
string endTag = "PLACEHOLDER_FOR_REVISION_TEXT_END";
|
||||||
|
|
||||||
|
int beginIndex = pageToModify.Body.IndexOf(beginTag);
|
||||||
|
int endIndex = pageToModify.Body.IndexOf(endTag);
|
||||||
|
|
||||||
|
if (beginIndex > 0 && endIndex > beginIndex)
|
||||||
|
{
|
||||||
|
string topText = pageToModify.Body.Substring(0, beginIndex + beginTag.Length);
|
||||||
|
string bottomText = pageToModify.Body.Substring(endIndex);
|
||||||
|
|
||||||
|
pageToModify.Body = topText.Trim()
|
||||||
|
+ "\r\n" + GenerateWikiParagraph(_random.Next(10, 20))
|
||||||
|
+ "\r\n" + bottomText.Trim();
|
||||||
|
pageToModify.ModifiedByUserId = userId;
|
||||||
|
pageToModify.ModifiedByUserId = userId;
|
||||||
|
TightWiki.Engine.Implementation.Helpers.UpsertPage(engine, pageToModify);
|
||||||
|
|
||||||
|
if (_random.Next(100) >= 90)
|
||||||
|
{
|
||||||
|
var fileName = _fileNames[_random.Next(_fileNames.Count)] + ".txt";
|
||||||
|
var fileData = Encoding.UTF8.GetBytes(pageToModify.Body);
|
||||||
|
AttachFile(pageToModify.Id, userId, fileName, fileData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches a file to a wiki page.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pageId"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="fileName"></param>
|
||||||
|
/// <param name="fileData"></param>
|
||||||
|
private void AttachFile(int pageId, Guid userId, string fileName, byte[] fileData)
|
||||||
|
{
|
||||||
|
if (fileData.Length > GlobalConfiguration.MaxAttachmentFileSize)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not save the attached file, too large");
|
||||||
|
}
|
||||||
|
|
||||||
|
PageFileRepository.UpsertPageFile(new PageFileAttachment()
|
||||||
|
{
|
||||||
|
Data = fileData,
|
||||||
|
CreatedDate = DateTime.UtcNow,
|
||||||
|
PageId = pageId,
|
||||||
|
Name = fileName,
|
||||||
|
FileNavigation = Navigation.Clean(fileName),
|
||||||
|
Size = fileData.Length,
|
||||||
|
ContentType = Utility.GetMimeType(fileName)
|
||||||
|
}, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a shuffled version of the input list.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="list"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private List<T> ShuffleList<T>(List<T> list)
|
||||||
|
{
|
||||||
|
var newList = new List<T>(list);
|
||||||
|
var rand = new Random();
|
||||||
|
int n = newList.Count;
|
||||||
|
while (n > 1)
|
||||||
|
{
|
||||||
|
n--;
|
||||||
|
int k = _random.Next(n + 1);
|
||||||
|
T value = newList[k];
|
||||||
|
newList[k] = newList[n];
|
||||||
|
newList[n] = value;
|
||||||
|
}
|
||||||
|
return newList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds some random wiki text to a word.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string AddWikiMarkup(string text)
|
||||||
|
{
|
||||||
|
switch (_random.Next(0, 5))
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
return $"//{text}//";
|
||||||
|
case 2:
|
||||||
|
return $"~~{text}~~";
|
||||||
|
case 3:
|
||||||
|
return $"__{text}__";
|
||||||
|
case 4:
|
||||||
|
return $"!!{text}!!";
|
||||||
|
default:
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
DummyPageGenerator/Program.cs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
using Autofac;
|
||||||
|
using Autofac.Extensions.DependencyInjection;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TightWiki.Engine;
|
||||||
|
using TightWiki.Engine.Implementation;
|
||||||
|
using TightWiki.Engine.Library.Interfaces;
|
||||||
|
using TightWiki.Library;
|
||||||
|
using TightWiki.Repository;
|
||||||
|
|
||||||
|
namespace DummyPageGenerator
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
public class NoOpCompletionHandler : ICompletionHandler
|
||||||
|
{
|
||||||
|
public void Complete(ITightEngineState state)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
SqlMapper.AddTypeHandler(new GuidTypeHandler());
|
||||||
|
|
||||||
|
var host = Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureAppConfiguration((context, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(AppContext.BaseDirectory)
|
||||||
|
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||||
|
})
|
||||||
|
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
|
||||||
|
.ConfigureContainer<ContainerBuilder>(containerBuilder =>
|
||||||
|
{
|
||||||
|
containerBuilder.RegisterType<StandardFunctionHandler>().As<IStandardFunctionHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<ScopeFunctionHandler>().As<IScopeFunctionHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<ProcessingInstructionFunctionHandler>().As<IProcessingInstructionFunctionHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<PostProcessingFunctionHandler>().As<IPostProcessingFunctionHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<MarkupHandler>().As<IMarkupHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<HeadingHandler>().As<IHeadingHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<CommentHandler>().As<ICommentHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<EmojiHandler>().As<IEmojiHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<ExternalLinkHandler>().As<IExternalLinkHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<InternalLinkHandler>().As<IInternalLinkHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<ExceptionHandler>().As<IExceptionHandler>().SingleInstance();
|
||||||
|
containerBuilder.RegisterType<NoOpCompletionHandler>().As<ICompletionHandler>().SingleInstance();
|
||||||
|
|
||||||
|
containerBuilder.RegisterType<TightEngine>();
|
||||||
|
}).Build();
|
||||||
|
|
||||||
|
|
||||||
|
var configuration = host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddLogging(configure => configure.AddConsole());
|
||||||
|
|
||||||
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
options.UseSqlite(configuration.GetConnectionString("UsersConnection")));
|
||||||
|
|
||||||
|
ManagedDataStorage.Pages.SetConnectionString(configuration.GetConnectionString("PagesConnection"));
|
||||||
|
ManagedDataStorage.DeletedPages.SetConnectionString(configuration.GetConnectionString("DeletedPagesConnection"));
|
||||||
|
ManagedDataStorage.DeletedPageRevisions.SetConnectionString(configuration.GetConnectionString("DeletedPageRevisionsConnection"));
|
||||||
|
ManagedDataStorage.Statistics.SetConnectionString(configuration.GetConnectionString("StatisticsConnection"));
|
||||||
|
ManagedDataStorage.Emoji.SetConnectionString(configuration.GetConnectionString("EmojiConnection"));
|
||||||
|
ManagedDataStorage.Exceptions.SetConnectionString(configuration.GetConnectionString("ExceptionsConnection"));
|
||||||
|
WordsRepository.Words.SetConnectionString(configuration.GetConnectionString("WordsConnection"));
|
||||||
|
ManagedDataStorage.Users.SetConnectionString(configuration.GetConnectionString("UsersConnection"));
|
||||||
|
ManagedDataStorage.Config.SetConnectionString(configuration.GetConnectionString("ConfigConnection"));
|
||||||
|
|
||||||
|
// Register Identity services
|
||||||
|
services.AddIdentity<IdentityUser, IdentityRole>()
|
||||||
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||||
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// Resolve the services
|
||||||
|
var signInManager = serviceProvider.GetRequiredService<SignInManager<IdentityUser>>();
|
||||||
|
var userManager = serviceProvider.GetRequiredService<UserManager<IdentityUser>>();
|
||||||
|
var userStore = serviceProvider.GetRequiredService<IUserStore<IdentityUser>>();
|
||||||
|
|
||||||
|
ConfigurationRepository.ReloadEverything();
|
||||||
|
|
||||||
|
var pg = new PageGenerator(userManager);
|
||||||
|
|
||||||
|
var pool = new NTDLS.DelegateThreadPooling.DelegateThreadPool(4, 0);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var workload = pool.CreateChildPool();
|
||||||
|
|
||||||
|
foreach (var user in pg.Users)
|
||||||
|
{
|
||||||
|
workload.Enqueue(() =>
|
||||||
|
{
|
||||||
|
using var scope = host.Services.CreateScope();
|
||||||
|
var engine = scope.ServiceProvider.GetRequiredService<TightEngine>();
|
||||||
|
|
||||||
|
//Create a new page:
|
||||||
|
pg.GeneratePage(engine, user.UserId);
|
||||||
|
|
||||||
|
//Modify existing pages:
|
||||||
|
int modifications = pg.Random.Next(0, 10);
|
||||||
|
for (int i = 0; i < modifications; i++)
|
||||||
|
{
|
||||||
|
pg.ModifyRandomPages(engine, user.UserId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
workload.WaitForCompletion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
DummyPageGenerator/WordsRepository.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using NTDLS.SqliteDapperWrapper;
|
||||||
|
|
||||||
|
namespace DummyPageGenerator
|
||||||
|
{
|
||||||
|
public static class WordsRepository
|
||||||
|
{
|
||||||
|
public static ManagedDataStorageFactory Words { get; private set; } = new();
|
||||||
|
|
||||||
|
public static int GetWordsCount()
|
||||||
|
=> Words.ExecuteScalar<int>("GetWordsCount.sql");
|
||||||
|
|
||||||
|
public static List<string> GetRandomWords(int count)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
|
||||||
|
var random = new Random();
|
||||||
|
int countOfWords = GetWordsCount();
|
||||||
|
|
||||||
|
while (result.Count < count)
|
||||||
|
{
|
||||||
|
var param = new
|
||||||
|
{
|
||||||
|
Offset = random.Next(countOfWords),
|
||||||
|
};
|
||||||
|
|
||||||
|
var word = Words.QueryFirstOrDefault<string>("GetSingleWordAt.sql", param);
|
||||||
|
if (word != null)
|
||||||
|
{
|
||||||
|
result.Add(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
DummyPageGenerator/appsettings.Development.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"DetailedErrors": true,
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
DummyPageGenerator/appsettings.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"PagesConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\pages.db",
|
||||||
|
"DeletedPagesConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\deletedpages.db",
|
||||||
|
"DeletedPageRevisionsConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\deletedpagerevisions.db",
|
||||||
|
"StatisticsConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\statistics.db",
|
||||||
|
"EmojiConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\emoji.db",
|
||||||
|
"ExceptionsConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\exceptions.db",
|
||||||
|
"WordsConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\words.db",
|
||||||
|
"UsersConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\users.db",
|
||||||
|
"ConfigConnection": "Data Source=C:\\NTDLS\\TightWiki\\TightWiki\\Data\\config.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
DumpConfiguration/ConfigurationEntry.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace DumpConfiguration
|
||||||
|
{
|
||||||
|
public partial class ConfigurationEntry
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int ConfigurationGroupId { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public int DataTypeId { get; set; }
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public bool IsEncrypted { get; set; }
|
||||||
|
public bool IsRequired { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
DumpConfiguration/ConfigurationGroup.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace DumpConfiguration
|
||||||
|
{
|
||||||
|
public partial class ConfigurationGroup
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
DumpConfiguration/DumpConfiguration.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NTDLS.SqliteDapperWrapper" Version="1.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
41
DumpConfiguration/Program.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace DumpConfiguration
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length != 1)
|
||||||
|
{
|
||||||
|
Console.WriteLine("DumpConfiguration.exe <sqliteFile>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
using var dbConnection = new SqliteConnection($"Data Source={args[0]}");
|
||||||
|
dbConnection.Open();
|
||||||
|
|
||||||
|
var configurationGroups = dbConnection.Query<ConfigurationGroup>("SELECT * FROM ConfigurationGroup");
|
||||||
|
foreach (var cg in configurationGroups)
|
||||||
|
{
|
||||||
|
sb.AppendLine("INSERT INTO ConfigurationGroup(Id, Name, Description)");
|
||||||
|
sb.AppendLine($"SELECT {cg.Id}, '{cg.Name}', '{cg.Description}'");
|
||||||
|
sb.AppendLine($"ON CONFLICT(Name) DO UPDATE SET Description = '{cg.Description}';");
|
||||||
|
}
|
||||||
|
|
||||||
|
var ConfigurationEntries = dbConnection.Query<ConfigurationEntry>("SELECT * FROM ConfigurationEntry");
|
||||||
|
foreach (var ce in ConfigurationEntries)
|
||||||
|
{
|
||||||
|
sb.AppendLine("INSERT INTO ConfigurationEntry(Id, ConfigurationGroupId, Name, Value, DataTypeId, Description, IsEncrypted, IsRequired)");
|
||||||
|
sb.AppendLine($"SELECT {ce.Id}, {ce.ConfigurationGroupId}, '{ce.Name}', '{ce.Value.Replace("'", "''")}', {ce.DataTypeId}, '{ce.Description}', {(ce.IsEncrypted ? 1 : 0)}, {(ce.IsRequired ? 1 : 0)}");
|
||||||
|
sb.AppendLine($"ON CONFLICT(ConfigurationGroupId, Name) DO UPDATE SET Name = '{ce.Name}', DataTypeId = {ce.DataTypeId}, Description = '{ce.Description}', IsEncrypted = '{(ce.IsEncrypted ? 1 : 0)}', IsRequired = '{(ce.IsRequired ? 1 : 0)}';");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(sb.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
DumpConfiguration/Properties/launchSettings.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"DumpConfiguration": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "\"C:\\NTDLS\\TightWiki\\Data\\config.db\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Highlight.js.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Tools:
|
||||||
|
node.js : https://nodejs.org/en/download/
|
||||||
|
git : https://git-scm.com/download/win
|
||||||
|
|
||||||
|
* Clone: https://github.com/highlightjs/highlight.js.git
|
||||||
|
* change directory to pulled source directory
|
||||||
|
* Copy tightwiki.js to \src\languages
|
||||||
|
* run: npm install
|
||||||
|
* run: npm run build
|
||||||
|
* node tools/build.js
|
||||||
|
|
||||||
|
Use highlight.min.js from .\build
|
||||||
|
Dont forget to include the styles.
|
||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Josh Patterson
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
BIN
Media/Emoj-NotImplemented.zip
Normal file
BIN
Media/Emoji.zip
Normal file
BIN
Media/TightWiki Icon (multi).ico
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
Media/TightWiki Icon 128.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
Media/TightWiki Icon 16.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
Media/TightWiki Icon 32.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
Media/TightWiki Icon 64.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
Media/TightWiki Icon.pspimage
Normal file
BIN
Media/TightWiki Logo 64x64.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
Media/TightWiki Logo.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
Media/TightWiki Logo.pspimage
Normal file
21
Mssql2Sqlite/Mssql2Sqlite.csproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<DebugSymbols>False</DebugSymbols>
|
||||||
|
<DebugType>None</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.1" />
|
||||||
|
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
114
Mssql2Sqlite/Program.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace Mssql2Sqlite
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length != 4 && args.Length != 6)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Usage for SQL Server integrated security:");
|
||||||
|
Console.WriteLine("Mssql2Sqlite.exe <sqliteFile> <userGuid> <sqlServer> <sqlDatabase>");
|
||||||
|
|
||||||
|
Console.WriteLine("");
|
||||||
|
Console.WriteLine("Usage for SQL Server user security:");
|
||||||
|
Console.WriteLine("Mssql2Sqlite.exe <sqliteFile> <userGuid> <sqlServer> <sqlDatabase> <SQLServerUser> <SQLServerPassword>");
|
||||||
|
|
||||||
|
Console.WriteLine("sqliteFile: The SQLite file to import the data into, must already exist with proper schema. Existing data will be deleted.");
|
||||||
|
Console.WriteLine("userGuid: The UserId from the SQLite users.db that you want to associate with the import. Use: 963f0b81-f2ac-488b-9b21-521852641ec4 for admin@tightwiki.com");
|
||||||
|
|
||||||
|
Console.WriteLine("sqlServer: The SQL Server name to export the data from.");
|
||||||
|
Console.WriteLine("SQLServerUser: Optional, is the username to use for connecting to SQL Server.");
|
||||||
|
Console.WriteLine("SQLServerPassword: Optional, is the password to use for connecting to SQL Server.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string sqliteFile = args[0];
|
||||||
|
string userId = args[1];
|
||||||
|
|
||||||
|
var builder = new SqlConnectionStringBuilder
|
||||||
|
{
|
||||||
|
DataSource = args[2],
|
||||||
|
InitialCatalog = args[3],
|
||||||
|
Encrypt = SqlConnectionEncryptOption.Optional,
|
||||||
|
TrustServerCertificate = true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.Length == 4)
|
||||||
|
{
|
||||||
|
builder.IntegratedSecurity = true;
|
||||||
|
}
|
||||||
|
else if (args.Length == 6)
|
||||||
|
{
|
||||||
|
builder.IntegratedSecurity = false;
|
||||||
|
builder.UserID = args[3];
|
||||||
|
builder.Password = args[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
MigrateTable("Page", sqliteFile, builder.ToString(), userId);
|
||||||
|
MigrateTable("PageFile", sqliteFile, builder.ToString(), userId);
|
||||||
|
MigrateTable("PageRevision", sqliteFile, builder.ToString(), userId);
|
||||||
|
MigrateTable("PageFileRevision", sqliteFile, builder.ToString(), userId);
|
||||||
|
MigrateTable("PageRevisionAttachment", sqliteFile, builder.ToString(), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MigrateTable(string tableName, string sqliteFile, string sqlServerConnectionString, string userId)
|
||||||
|
{
|
||||||
|
using var sqlServerConnection = new SqlConnection(sqlServerConnectionString);
|
||||||
|
var data = sqlServerConnection.Query<dynamic>($"SELECT * FROM [{tableName}]").ToList();
|
||||||
|
var insertColumns = sqlServerConnection.Query<string>($"SELECT name from sys.columns where object_id = object_id('{tableName}')").ToList();
|
||||||
|
|
||||||
|
using (var sqliteConnection = new SqliteConnection($"Data Source={sqliteFile}"))
|
||||||
|
{
|
||||||
|
sqliteConnection.Open();
|
||||||
|
|
||||||
|
// Enable writing of identity columns in SQLite
|
||||||
|
sqliteConnection.Execute("PRAGMA foreign_keys=OFF;");
|
||||||
|
sqliteConnection.Execute("BEGIN TRANSACTION;");
|
||||||
|
sqliteConnection.Execute("PRAGMA defer_foreign_keys=ON;");
|
||||||
|
|
||||||
|
var selectColumns = new List<string>();
|
||||||
|
|
||||||
|
foreach (var column in insertColumns)
|
||||||
|
{
|
||||||
|
switch (column)
|
||||||
|
{
|
||||||
|
case "Namespace":
|
||||||
|
selectColumns.Add("Coalesce(@Namespace, '')");
|
||||||
|
break;
|
||||||
|
case "CreatedByUserId":
|
||||||
|
case "ModifiedByUserId":
|
||||||
|
selectColumns.Add($"'{userId}'");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
selectColumns.Add($"@{column}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableName == "PageFileRevision")
|
||||||
|
{
|
||||||
|
insertColumns.Add("CreatedByUserId");
|
||||||
|
selectColumns.Add("(SELECT P.ModifiedByUserId FROM PageFile as PF INNER JOIN Page as P ON P.Id = PF.PageId WHERE PF.Id = @PageFileId)");
|
||||||
|
}
|
||||||
|
|
||||||
|
sqliteConnection.Execute($"DELETE FROM {tableName}");
|
||||||
|
|
||||||
|
foreach (var item in data)
|
||||||
|
{
|
||||||
|
var insertQuery = $"INSERT INTO {tableName} ({string.Join(",", insertColumns)}) SELECT {string.Join(",", selectColumns)}";
|
||||||
|
sqliteConnection.Execute(insertQuery, (object?)item);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqliteConnection.Execute("COMMIT;");
|
||||||
|
sqliteConnection.Execute("PRAGMA defer_foreign_keys=OFF;");
|
||||||
|
sqliteConnection.Execute("PRAGMA foreign_keys=ON;");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Data import complete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Mssql2Sqlite/Properties/launchSettings.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"Mssql2Sqlite": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "C:\\DropZone\\pages.db 963f0b81-f2ac-488b-9b21-521852641ec4 localhost TightWiki_Katzebase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Notes.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Notes
|
||||||
|
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
### Entity Framework Core command-line tool
|
||||||
|
``dotnet tool install --global dotnet-ef``
|
||||||
|
``dotnet add package Microsoft.AspNetCore.Authentication.Google``
|
||||||
|
|
||||||
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# TightWiki
|
||||||
|
|
||||||
|
For years I’ve worked at places where we just needed a simple to use, searchable, unobtrusive, no-nonsense, collaborative and free place to dump documentation.
|
||||||
|
The first thing that comes to mind is a Wiki but for some reason I can never find anything that "checks all the boxes". Hopefully you'll find this one does for you.
|
||||||
|
|
||||||
|
:yum: TightWiki is an ASP.NET Core MVC Razor WIKI written in C# that sits on top of a SQLite database (zero configuration required).
|
||||||
|
|
||||||
|
:crossed_fingers: Play with the latest dev build at http://TightWiki.com/. If you want to edit, you can signup using google auth or native TightWiki login.
|
||||||
|
|
||||||
|
:eyes: Or check out the full wiki [documentation](https://tightwiki.com/Wiki%20Help%20::%20Wiki%20Help) to learn about the engine functionality.
|
||||||
|
|
||||||
|
:star: Ready to run it for yourself? Check out the [installation instructions](https://tightwiki.com/wiki_help::installation)!
|
||||||
|
|
||||||
|
:boom: Also be sure to check out the screenshots below the feature list...
|
||||||
|
|
||||||
|
:anguished: Its been like a modern retelling of Sisyphus, only this time the stone is RegEx.
|
||||||
|
|
||||||
|
# :astonished: Features (some of them anyway)
|
||||||
|
* MIT license, you can use it for free at home or at your business.
|
||||||
|
* Open source, you can make changes, submit fixes or just make suggestions.
|
||||||
|
* Completely customizable and rebrandable including name, title, footer, copyright and all images.
|
||||||
|
* User signup can be disabled, enabled and can require users to verify email before logging in.
|
||||||
|
* Multiple user roles are supported for admin, moderators, contributors and basic members.
|
||||||
|
* Easy page linking. Can even link to pages that do not exist and the link will subtly prompt you to create the page when logged in with a role that has page creation support.
|
||||||
|
* Admin shows missing pages, namespace metrics, users, roles, etc.
|
||||||
|
* Manual account creation, editing and deletion.
|
||||||
|
* All dates/times are stored in UTC and localized for logged in users.
|
||||||
|
* Admin moderation which is driven by page processing instructions for things like page deletions, review, drafts, etc.
|
||||||
|
* Page versioning. Revisions can be viewed by the original page URL with a /r/number route or by logging in a viewing the full page history.
|
||||||
|
* Revertible page history.
|
||||||
|
* Theme-able, with 25+ built in themes.
|
||||||
|
* Drag-drop fie uploads / page attachments, images.
|
||||||
|
* Versioned file uploads.
|
||||||
|
* Namespace support so you can have multiple pages with the same name in different namespaces.
|
||||||
|
* Fully baked in documentation of all wiki functions.
|
||||||
|
* Wiki Markup allows you post non-formatted code and even auto-syntax highlighting for things like C#, PHP, SQL, etc. Can also explicitly specify language.
|
||||||
|
* Wiki markup supports basic formatting, headings and sub-headings, tagging, tables, callouts, alerts, variables, bullets lists, dynamic glossaries, inline search results, dynamic tag clouds, related linking, expanding sections, auto-table of contents, and much more.
|
||||||
|
* Wiki page editing is syntax highlighted.
|
||||||
|
* Built in search supports fuzzy matching to support even mild misspellings.
|
||||||
|
|
||||||
|
# Default home page
|
||||||
|

|
||||||
|
|
||||||
|
# Site Metrics
|
||||||
|
We've beat the wiki up with more data than this, but this is our standard workload. ~45,000 pages, in ~400 namespaces, with ~250,000 revisions, created by ~1,000 users, manifesting ~5 million search tokens. The random fuzzy-match search time is 11 milliseconds. Not too shabby, right?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
# Page search
|
||||||
|

|
||||||
|
|
||||||
|
# Page History
|
||||||
|

|
||||||
|
|
||||||
|
# Example edit page
|
||||||
|

|
||||||
|
|
||||||
|
# Build in documentation list
|
||||||
|

|
||||||
|
|
||||||
|
# Build in documentation sample
|
||||||
|

|
||||||
|
|
||||||
|
# Configuration
|
||||||
|

|
||||||
|
|
||||||
|
# Admin page list
|
||||||
|

|
||||||
|
|
||||||
|
# Admin role list
|
||||||
|

|
||||||
|
|
||||||
|
Its been like a modern retelling of Sisyphus, only this time with RegEx.
|
||||||
7
TightWiki.Caching/IWikiCacheKey.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace TightWiki.Caching
|
||||||
|
{
|
||||||
|
public interface IWikiCacheKey
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
TightWiki.Caching/TightWiki.Caching.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<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>
|
||||||
|
<PackageReference Include="System.Runtime.Caching" Version="9.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
186
TightWiki.Caching/WikiCache.cs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Runtime.Caching;
|
||||||
|
|
||||||
|
namespace TightWiki.Caching
|
||||||
|
{
|
||||||
|
public class WikiCache
|
||||||
|
{
|
||||||
|
public enum Category
|
||||||
|
{
|
||||||
|
User,
|
||||||
|
Page,
|
||||||
|
Search,
|
||||||
|
Emoji,
|
||||||
|
Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int DefaultCacheSeconds { get; set; }
|
||||||
|
private static MemoryCache? _memCache;
|
||||||
|
public static ulong CachePuts { get; set; }
|
||||||
|
public static ulong CacheGets { get; set; }
|
||||||
|
public static ulong CacheHits { get; set; }
|
||||||
|
public static ulong CacheMisses { get; set; }
|
||||||
|
public static int CacheItemCount => MemCache.Count();
|
||||||
|
public static double CacheMemoryLimit => MemCache.CacheMemoryLimit;
|
||||||
|
|
||||||
|
public static MemoryCache MemCache => _memCache ?? throw new Exception("Cache has not been initialized.");
|
||||||
|
|
||||||
|
public static void Initialize(int cacheMemoryLimitMB, int defaultCacheSeconds)
|
||||||
|
{
|
||||||
|
DefaultCacheSeconds = defaultCacheSeconds;
|
||||||
|
var config = new NameValueCollection
|
||||||
|
{
|
||||||
|
//config.Add("pollingInterval", "00:05:00");
|
||||||
|
//config.Add("physicalMemoryLimitPercentage", "0");
|
||||||
|
{ "CacheMemoryLimitMegabytes", cacheMemoryLimitMB.ToString() }
|
||||||
|
};
|
||||||
|
_memCache?.Dispose();
|
||||||
|
_memCache = new MemoryCache("TightWikiCache", config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an item from the cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="cacheKey"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static T? Get<T>(IWikiCacheKey cacheKey)
|
||||||
|
{
|
||||||
|
CacheGets++;
|
||||||
|
var result = (T)MemCache.Get(cacheKey.Key);
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
CacheMisses++;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheHits++;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the cache contains a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="cacheKey"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool Contains(IWikiCacheKey cacheKey)
|
||||||
|
{
|
||||||
|
CacheGets++;
|
||||||
|
if (MemCache.Contains(cacheKey.Key))
|
||||||
|
{
|
||||||
|
CacheHits++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheMisses++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an item from the cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="cacheKey"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool TryGet<T>(IWikiCacheKey cacheKey, [NotNullWhen(true)] out T result)
|
||||||
|
{
|
||||||
|
CacheGets++;
|
||||||
|
if ((result = (T)MemCache.Get(cacheKey.Key)) == null)
|
||||||
|
{
|
||||||
|
CacheMisses++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheHits++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item to the cache. If the item is already in the cache, this will reset its expiration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cacheKey"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="seconds"></param>
|
||||||
|
public static void Put(IWikiCacheKey cacheKey, object value, int? seconds = null)
|
||||||
|
{
|
||||||
|
CachePuts++;
|
||||||
|
|
||||||
|
seconds ??= DefaultCacheSeconds;
|
||||||
|
if (seconds <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var policy = new CacheItemPolicy()
|
||||||
|
{
|
||||||
|
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(seconds ?? DefaultCacheSeconds)
|
||||||
|
};
|
||||||
|
MemCache.Add(cacheKey.Key, value, policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Put(IWikiCacheKey cacheKey, object value, CacheItemPolicy policy)
|
||||||
|
{
|
||||||
|
CachePuts++;
|
||||||
|
MemCache.Add(cacheKey.Key, value, policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all entries from the cache.
|
||||||
|
/// </summary>
|
||||||
|
public static void Clear()
|
||||||
|
{
|
||||||
|
if (_memCache == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = MemCache.ToList();
|
||||||
|
foreach (var a in items)
|
||||||
|
{
|
||||||
|
MemCache.Remove(a.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes cache entries that begin with the given cache key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="category"></param>
|
||||||
|
public static void ClearCategory(WikiCacheKey cacheKey)
|
||||||
|
{
|
||||||
|
var keys = new List<string>();
|
||||||
|
|
||||||
|
foreach (var item in MemCache)
|
||||||
|
{
|
||||||
|
if (item.Key.StartsWith(cacheKey.Key))
|
||||||
|
{
|
||||||
|
keys.Add(item.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.ForEach(o => MemCache.Remove(o));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes cache entries in a given category.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="category"></param>
|
||||||
|
public static void ClearCategory(Category category)
|
||||||
|
{
|
||||||
|
var cacheKey = WikiCacheKey.Build(category);
|
||||||
|
|
||||||
|
var keys = new List<string>();
|
||||||
|
|
||||||
|
foreach (var item in MemCache)
|
||||||
|
{
|
||||||
|
if (item.Key.StartsWith(cacheKey.Key))
|
||||||
|
{
|
||||||
|
keys.Add(item.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.ForEach(o => MemCache.Remove(o));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
TightWiki.Caching/WikiCacheKey.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using static TightWiki.Caching.WikiCache;
|
||||||
|
|
||||||
|
namespace TightWiki.Caching
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a verbatim cache key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
public class WikiCacheKey(string key) : IWikiCacheKey
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = key;
|
||||||
|
|
||||||
|
public static WikiCacheKey Build(Category category, object?[] segments)
|
||||||
|
=> new($"[{category}]:[{string.Join("]:[", segments)}]");
|
||||||
|
|
||||||
|
public static WikiCacheKey Build(Category category)
|
||||||
|
=> new($"[{category}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
26
TightWiki.Caching/WikiCacheKeyFunction.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using static TightWiki.Caching.WikiCache;
|
||||||
|
|
||||||
|
namespace TightWiki.Caching
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a verbatim cache key which also includes the calling function name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
public class WikiCacheKeyFunction(string key) : IWikiCacheKey
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = key;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a cache key which includes the calling function name.
|
||||||
|
/// </summary>
|
||||||
|
public static WikiCacheKeyFunction Build(Category category, object?[] segments, [CallerMemberName] string callingFunction = "")
|
||||||
|
=> new($"[{category}]:[{string.Join("]:[", segments)}]:[{callingFunction}]");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a cache key which includes the calling function name.
|
||||||
|
/// </summary>
|
||||||
|
public static WikiCacheKeyFunction Build(Category category, [CallerMemberName] string callingFunction = "")
|
||||||
|
=> new($"[{category}]:[{callingFunction}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
22
TightWiki.Email/TightWiki.Email.csproj
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<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>
|
||||||
|
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\TightWiki.Repository\TightWiki.Repository.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
56
TightWiki.Email/WikiEmailSender.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using MailKit.Net.Smtp;
|
||||||
|
using MailKit.Security;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MimeKit;
|
||||||
|
using TightWiki.Library.Interfaces;
|
||||||
|
using TightWiki.Repository;
|
||||||
|
|
||||||
|
namespace TightWiki.Email
|
||||||
|
{
|
||||||
|
public class WikiEmailSender : IWikiEmailSender
|
||||||
|
{
|
||||||
|
private readonly ILogger<WikiEmailSender> _logger;
|
||||||
|
|
||||||
|
public WikiEmailSender(ILogger<WikiEmailSender> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendEmailAsync(string email, string subject, string htmlMessage)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var values = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Email");
|
||||||
|
var smtpPassword = values.Value<string>("Password");
|
||||||
|
var smtpUsername = values.Value<string>("Username");
|
||||||
|
var smtpAddress = values.Value<string>("Address");
|
||||||
|
var smtpFromDisplayName = values.Value<string>("From Display Name");
|
||||||
|
var smtpUseSSL = values.Value<bool>("Use SSL");
|
||||||
|
int smtpPort = values.Value<int>("Port");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(smtpAddress) || string.IsNullOrEmpty(smtpUsername))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = new MimeMessage();
|
||||||
|
message.From.Add(new MailboxAddress(smtpFromDisplayName, smtpUsername));
|
||||||
|
message.To.Add(new MailboxAddress(email, email));
|
||||||
|
message.Subject = subject;
|
||||||
|
message.Body = new TextPart("html") { Text = htmlMessage };
|
||||||
|
|
||||||
|
using var client = new SmtpClient();
|
||||||
|
var options = smtpUseSSL ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTlsWhenAvailable;
|
||||||
|
await client.ConnectAsync(smtpAddress, smtpPort, options);
|
||||||
|
await client.AuthenticateAsync(smtpUsername, smtpPassword);
|
||||||
|
await client.SendAsync(message);
|
||||||
|
await client.DisconnectAsync(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, ex.Message);
|
||||||
|
ExceptionRepository.InsertException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace TightWiki.Engine.Function.Exceptions
|
||||||
|
{
|
||||||
|
public class WikiFunctionPrototypeNotDefinedException : Exception
|
||||||
|
{
|
||||||
|
public WikiFunctionPrototypeNotDefinedException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public WikiFunctionPrototypeNotDefinedException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace TightWiki.Engine.Function.Exceptions
|
||||||
|
{
|
||||||
|
public class WikiFunctionPrototypeSyntaxError : Exception
|
||||||
|
{
|
||||||
|
public WikiFunctionPrototypeSyntaxError()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public WikiFunctionPrototypeSyntaxError(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
210
TightWiki.Engine.Function/FunctionCall.cs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information about an actual function call, its supplied parameters, and is matched with a defined function.
|
||||||
|
/// </summary>
|
||||||
|
public class FunctionCall
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the function being called.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public FunctionPrototype Prototype { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The arguments supplied by the caller.
|
||||||
|
/// </summary>
|
||||||
|
public FunctionParameters Parameters { get; private set; }
|
||||||
|
|
||||||
|
public FunctionCall(FunctionPrototype prototype, List<string> args)
|
||||||
|
{
|
||||||
|
Prototype = prototype;
|
||||||
|
Parameters = new FunctionParameters(this);
|
||||||
|
Name = prototype.FunctionName;
|
||||||
|
|
||||||
|
foreach (var arg in args)
|
||||||
|
{
|
||||||
|
if (arg.StartsWith(':') && arg.Contains('='))
|
||||||
|
{
|
||||||
|
var parsed = arg.Substring(1); //Skip the colon.
|
||||||
|
int index = parsed.IndexOf('=');
|
||||||
|
var name = parsed.Substring(0, index).Trim().ToLower();
|
||||||
|
var value = parsed.Substring(index + 1).Trim();
|
||||||
|
|
||||||
|
Parameters.Named.Add(new NamedParameter(name, value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Parameters.Ordinals.Add(new OrdinalParameter(arg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyPrototype();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the passed value against the function prototype to ensure that the variable is the correct type, value, etc.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segment"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
private void EnforcePrototypeParamValue(PrototypeParameter param, string value)
|
||||||
|
{
|
||||||
|
if (param.Type == "bool")
|
||||||
|
{
|
||||||
|
if (bool.TryParse(value, out bool _) == false)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], the value [{value}] passed to parameter [{param.Name}] could not be converted to boolean.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (param.Type == "integer")
|
||||||
|
{
|
||||||
|
if (int.TryParse(value, out int _) == false)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], the value [{value}] passed to parameter [{param.Name}] could not be converted to integer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (param.Type == "float")
|
||||||
|
{
|
||||||
|
if (double.TryParse(value, out double _) == false)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], the value [{value}] passed to parameter [{param.Name}] could not be converted to float.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.AllowedValues != null && param.AllowedValues.Count > 0)
|
||||||
|
{
|
||||||
|
if (param.AllowedValues.Contains(value.ToLower()) == false)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], the value [{value}] passed to parameter [{param.Name}] is not allowed. Allowed values are [{string.Join(",", param.AllowedValues)}].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rolls through the supplied arguments and applies them to the prototype. Also identifies which supplied arguments are associated with each
|
||||||
|
/// prototype argument and adds the ordinal based arguments to the name based collection. Ensures that each argument conforms with the prototype.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
private void ApplyPrototype()
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
//Keep a list of the arguments as they are associated with the prototype so that we can later reference them by name.
|
||||||
|
var namedToAddLater = new List<NamedParameter>();
|
||||||
|
|
||||||
|
//Handle non-infinite ordinal based required parameters:
|
||||||
|
for (; index < Prototype.Parameters.Count; index++)
|
||||||
|
{
|
||||||
|
var param = Prototype.Parameters[index];
|
||||||
|
|
||||||
|
if (param.IsRequired == false)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (param.IsInfinite == true)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Parameters.Ordinals.Count > index)
|
||||||
|
{
|
||||||
|
//Good, we have a value.
|
||||||
|
string value = Parameters.Ordinals[index].Value;
|
||||||
|
Parameters.Ordinals[index].AssociateWithPrototypeParam(param.Name);
|
||||||
|
EnforcePrototypeParamValue(param, value.ToLower());
|
||||||
|
|
||||||
|
namedToAddLater.Add(new NamedParameter(param.Name, value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], the required parameter [{param.Name}] was not specified.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasEncounteredOptionalParameter = false;
|
||||||
|
|
||||||
|
//Handle remaining optional parameters:
|
||||||
|
for (; index < Prototype.Parameters.Count; index++)
|
||||||
|
{
|
||||||
|
var param = Prototype.Parameters[index];
|
||||||
|
|
||||||
|
if (param.IsInfinite == true)
|
||||||
|
{
|
||||||
|
if (param.IsRequired == true)
|
||||||
|
{
|
||||||
|
//Make sure we have at least one of these required infinite parameters passed.
|
||||||
|
if (Parameters.Ordinals.Count > index)
|
||||||
|
{
|
||||||
|
//Good, we have a value.
|
||||||
|
string value = Parameters.Ordinals[index].Value;
|
||||||
|
Parameters.Ordinals[index].AssociateWithPrototypeParam(param.Name);
|
||||||
|
EnforcePrototypeParamValue(param, value.ToLower());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], the required infinite parameter [{param.Name}] was not passed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Now that we have encountered an infinite parameter, it will swallow up all other ordinal based arguments. Might as well check the types and exit the loop.
|
||||||
|
for (; index < Parameters.Ordinals.Count; index++)
|
||||||
|
{
|
||||||
|
string value = Parameters.Ordinals[index].Value;
|
||||||
|
Parameters.Ordinals[index].AssociateWithPrototypeParam(param.Name);
|
||||||
|
EnforcePrototypeParamValue(param, value.ToLower());
|
||||||
|
namedToAddLater.Add(new NamedParameter(param.Name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.IsRequired == false)
|
||||||
|
{
|
||||||
|
hasEncounteredOptionalParameter = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.IsRequired == true && hasEncounteredOptionalParameter)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], the required parameter [{param.Name}] was found after other optional parameters.");
|
||||||
|
}
|
||||||
|
else if (param.IsInfinite == true)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], encountered an unexpected number of infinite parameters in prototype for [{param.Name}].");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Parameters.Ordinals.Count > index)
|
||||||
|
{
|
||||||
|
string value = Parameters.Ordinals[index].Value;
|
||||||
|
Parameters.Ordinals[index].AssociateWithPrototypeParam(param.Name);
|
||||||
|
EnforcePrototypeParamValue(param, value.ToLower());
|
||||||
|
namedToAddLater.Add(new NamedParameter(param.Name, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var named in Parameters.Named)
|
||||||
|
{
|
||||||
|
var param = Prototype.Parameters.Where(o => o.Name.Equals(named.Name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault()
|
||||||
|
?? throw new Exception($"Function [{Name}], the named parameter [{named.Name}] is not defined in the function prototype.");
|
||||||
|
|
||||||
|
EnforcePrototypeParamValue(param, named.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Parameters.Named.AddRange(namedToAddLater);
|
||||||
|
|
||||||
|
var unmatchedParams = Parameters.Ordinals.Where(o => o.IsMatched == false).ToList();
|
||||||
|
if (unmatchedParams.Count != 0)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{Name}], unmatched parameter value [{unmatchedParams.First().Value}].");
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonInfiniteParams = Prototype.Parameters.Where(o => o.IsInfinite == false).Select(o => o.Name.ToLower());
|
||||||
|
var groups = Parameters.Named.Where(o => nonInfiniteParams.Contains(o.Name.ToLower())).GroupBy(o => o.Name.ToLower()).Where(o => o.Count() > 1);
|
||||||
|
|
||||||
|
if (groups.Any())
|
||||||
|
{
|
||||||
|
var group = groups.First();
|
||||||
|
throw new Exception($"Function [{Name}], non-infinite parameter specified more than once: [{group.Key}].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
TightWiki.Engine.Function/FunctionParameters.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using NTDLS.Helpers;
|
||||||
|
|
||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public class FunctionParameters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Variables set by ordinal.
|
||||||
|
/// </summary>
|
||||||
|
public List<OrdinalParameter> Ordinals { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Variables set by name.
|
||||||
|
/// </summary>
|
||||||
|
public List<NamedParameter> Named { get; private set; } = new();
|
||||||
|
|
||||||
|
private readonly FunctionCall _owner;
|
||||||
|
|
||||||
|
public FunctionParameters(FunctionCall owner)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get<T>(string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = Named.Where(o => o.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault()?.Value;
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
var prototype = _owner.Prototype.Parameters.Where(o => o.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)).First();
|
||||||
|
return Converters.ConvertTo<T>(prototype.DefaultValue) ?? throw new Exception("Value cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Converters.ConvertTo<T>(value) ?? throw new Exception("Value cannot be null");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{_owner.Name}], {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get<T>(string name, T defaultValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = Named.Where(o => o.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault()?.Value;
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Converters.ConvertTo<T>(value) ?? throw new Exception("Value cannot be null");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{_owner.Name}], {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> GetList<T>(string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var values = Named.Where(o => o.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))?
|
||||||
|
.Select(o => Converters.ConvertTo<T>(o.Value) ?? throw new Exception("Value cannot be null"))?.ToList();
|
||||||
|
|
||||||
|
return values ?? new List<T>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{_owner.Name}], {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
TightWiki.Engine.Function/FunctionParser.cs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using TightWiki.Engine.Function.Exceptions;
|
||||||
|
|
||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public static partial class FunctionParser
|
||||||
|
{
|
||||||
|
[GeneratedRegex(@"(##|{{|@@)([a-zA-Z_\s{][a-zA-Z0-9_\s{]*)\(((?<BR>\()|(?<-BR>\))|[^()]*)+\)")]
|
||||||
|
private static partial Regex FunctionCallParser();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsed a function call, its parameters and matches it to a defined function and its prototype.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="functionCall"></param>
|
||||||
|
/// <param name="parseEndIndex"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static FunctionCall ParseAndGetFunctionCall(FunctionPrototypeCollection prototypes, string functionCall, out int parseEndIndex)
|
||||||
|
{
|
||||||
|
var rawArguments = new List<string>();
|
||||||
|
|
||||||
|
var parsed = ParseFunctionCall(prototypes, functionCall);
|
||||||
|
|
||||||
|
var prototype = prototypes.Get(parsed.Prefix, parsed.Name);
|
||||||
|
if (prototype == null)
|
||||||
|
{
|
||||||
|
throw new WikiFunctionPrototypeNotDefinedException($"Function ({parsed.Name}) does not have a defined prototype.");
|
||||||
|
}
|
||||||
|
|
||||||
|
parseEndIndex = parsed.EndIndex;
|
||||||
|
|
||||||
|
return new FunctionCall(prototype, parsed.RawArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ParsedFunctionCall ParseFunctionCall(FunctionPrototypeCollection prototypes, string functionCall)
|
||||||
|
{
|
||||||
|
string functionName = string.Empty;
|
||||||
|
int parseEndIndex = 0;
|
||||||
|
var rawArguments = new List<string>();
|
||||||
|
|
||||||
|
var firstLine = functionCall.Split('\n')?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (firstLine == null || firstLine.Where(x => x == '(').Count() != firstLine.Where(x => x == ')').Count())
|
||||||
|
{
|
||||||
|
throw new WikiFunctionPrototypeSyntaxError($"Function parentheses mismatch.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string functionPrefix = functionCall.Substring(0, 2);
|
||||||
|
|
||||||
|
var parameterMatches = FunctionCallParser().Matches(firstLine);
|
||||||
|
if (parameterMatches.Count > 0)
|
||||||
|
{
|
||||||
|
var match = parameterMatches[0];
|
||||||
|
int paramStartIndex = match.Value.IndexOf('(');
|
||||||
|
|
||||||
|
functionName = match.Value[..paramStartIndex].ToLower().TrimStart(['{', '#', '@']).Trim();
|
||||||
|
parseEndIndex = match.Index + match.Length;
|
||||||
|
|
||||||
|
string rawArgTrimmed = match.ToString().Substring(paramStartIndex, (match.ToString().Length - paramStartIndex));
|
||||||
|
rawArguments = ParseRawArguments(rawArgTrimmed);
|
||||||
|
}
|
||||||
|
else //The function call has no parameters.
|
||||||
|
{
|
||||||
|
int endOfLine = functionCall.Substring(2).TakeWhile(c => char.IsLetterOrDigit(c)).Count(); //Find the first non-alphanumeric after the function identifier (##, @@, etc).
|
||||||
|
functionName = functionCall.Substring(2, endOfLine).ToLower().TrimStart(['{', '#', '@']).Trim();
|
||||||
|
parseEndIndex = endOfLine + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ParsedFunctionCall(functionPrefix, functionName, parseEndIndex, rawArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses function parameters into a list of arguments based on comma separation.
|
||||||
|
/// String do not need to be enclosed in double-quotes unless they contain commas.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="paramString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static List<string> ParseRawArgumentsAddParenthesis(string paramString)
|
||||||
|
{
|
||||||
|
if (paramString.StartsWith('(') || paramString.EndsWith(')'))
|
||||||
|
{
|
||||||
|
throw new WikiFunctionPrototypeSyntaxError($"Unexpected '(' or ')'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseRawArguments($"({paramString})");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses function parameters into a list of arguments based on comma separation.
|
||||||
|
/// String do not need to be enclosed in double-quotes unless they contain commas.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="paramString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static List<string> ParseRawArguments(string paramString)
|
||||||
|
{
|
||||||
|
List<string> ps = new();
|
||||||
|
|
||||||
|
int readPos = 0;
|
||||||
|
|
||||||
|
var singleParam = new StringBuilder();
|
||||||
|
|
||||||
|
if (paramString[readPos] != '(')
|
||||||
|
{
|
||||||
|
throw new WikiFunctionPrototypeSyntaxError($"Expected '('.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int parenNest = 1;
|
||||||
|
|
||||||
|
readPos++; //Skip the (
|
||||||
|
|
||||||
|
while (readPos < paramString.Length && char.IsWhiteSpace(paramString[readPos])) readPos++;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (paramString[readPos] == '(')
|
||||||
|
{
|
||||||
|
parenNest++;
|
||||||
|
}
|
||||||
|
else if (paramString[readPos] == ')')
|
||||||
|
{
|
||||||
|
parenNest--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readPos == paramString.Length)
|
||||||
|
{
|
||||||
|
throw new WikiFunctionPrototypeSyntaxError($"Expected ')'.");
|
||||||
|
}
|
||||||
|
else if (paramString[readPos] == ')' && parenNest == 0)
|
||||||
|
{
|
||||||
|
readPos++; //Skip the )
|
||||||
|
|
||||||
|
if (parenNest == 0 && readPos != paramString.Length)
|
||||||
|
{
|
||||||
|
throw new WikiFunctionPrototypeSyntaxError($"Expected end of statement.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (singleParam.Length > 0)
|
||||||
|
{
|
||||||
|
ps.Add(singleParam.ToString());
|
||||||
|
}
|
||||||
|
singleParam.Clear();
|
||||||
|
|
||||||
|
if (parenNest == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (paramString[readPos] == '\"')
|
||||||
|
{
|
||||||
|
readPos++; //Skip the ".
|
||||||
|
|
||||||
|
bool escapeChar = false;
|
||||||
|
for (; ; readPos++)
|
||||||
|
{
|
||||||
|
if (readPos == paramString.Length)
|
||||||
|
{
|
||||||
|
throw new WikiFunctionPrototypeSyntaxError($"Expected end of string.");
|
||||||
|
}
|
||||||
|
else if (paramString[readPos] == '\\')
|
||||||
|
{
|
||||||
|
escapeChar = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (paramString[readPos] == '\"' && escapeChar == false)
|
||||||
|
{
|
||||||
|
//Found the end of the string:
|
||||||
|
readPos++; //Skip the ".
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
singleParam.Append(paramString[readPos]);
|
||||||
|
}
|
||||||
|
escapeChar = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (readPos < paramString.Length && char.IsWhiteSpace(paramString[readPos])) readPos++;
|
||||||
|
}
|
||||||
|
else if (paramString[readPos] == ',')
|
||||||
|
{
|
||||||
|
readPos++; //Skip the ,
|
||||||
|
while (readPos < paramString.Length && char.IsWhiteSpace(paramString[readPos])) readPos++;
|
||||||
|
|
||||||
|
ps.Add(singleParam.ToString());
|
||||||
|
singleParam.Clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
singleParam.Append(paramString[readPos]);
|
||||||
|
|
||||||
|
if (paramString[readPos] == '(')
|
||||||
|
{
|
||||||
|
readPos++;
|
||||||
|
while (readPos < paramString.Length && char.IsWhiteSpace(paramString[readPos])) readPos++;
|
||||||
|
}
|
||||||
|
else if (paramString[readPos] == ')')
|
||||||
|
{
|
||||||
|
readPos++;
|
||||||
|
while (readPos < paramString.Length && char.IsWhiteSpace(paramString[readPos])) readPos++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
readPos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < ps.Count; i++)
|
||||||
|
{
|
||||||
|
ps[i] = ps[i].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
TightWiki.Engine.Function/FunctionPrototype.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public class FunctionPrototype
|
||||||
|
{
|
||||||
|
public string FunctionPrefix { get; set; } = string.Empty;
|
||||||
|
public string ProperName { get; set; } = string.Empty;
|
||||||
|
public string FunctionName { get; set; } = string.Empty;
|
||||||
|
public List<PrototypeParameter> Parameters { get; set; }
|
||||||
|
|
||||||
|
public FunctionPrototype()
|
||||||
|
{
|
||||||
|
Parameters = new List<PrototypeParameter>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
TightWiki.Engine.Function/FunctionPrototypeCollection.cs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
using TightWiki.Engine.Function.Exceptions;
|
||||||
|
|
||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public class FunctionPrototypeCollection
|
||||||
|
{
|
||||||
|
public enum WikiFunctionType
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
Scoped,
|
||||||
|
Instruction
|
||||||
|
}
|
||||||
|
|
||||||
|
public WikiFunctionType FunctionTypes { get; private set; }
|
||||||
|
public List<PrototypeSet> Items { get; set; } = new();
|
||||||
|
|
||||||
|
public FunctionPrototypeCollection(WikiFunctionType functionTypes)
|
||||||
|
{
|
||||||
|
FunctionTypes = functionTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(string prototypeString)
|
||||||
|
{
|
||||||
|
var prototype = ParsePrototype(prototypeString);
|
||||||
|
|
||||||
|
Items.Add(new PrototypeSet()
|
||||||
|
{
|
||||||
|
FunctionPrefix = prototype.FunctionPrefix,
|
||||||
|
ProperName = prototype.ProperName,
|
||||||
|
FunctionName = prototype.FunctionName.ToLower(),
|
||||||
|
Value = prototype
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists(string functionPrefix, string functionName)
|
||||||
|
{
|
||||||
|
functionName = functionName.ToLower();
|
||||||
|
|
||||||
|
//$$ are scope functions and are not called by prefix, we only have prefixes to make it easier to parse
|
||||||
|
// the functions in the wikiText and scope functions are easy enough since they start with curly braces.
|
||||||
|
return Items.Any(o => (o.FunctionPrefix == functionPrefix || o.FunctionPrefix == "$$") && o.FunctionName == functionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FunctionPrototype Get(string functionPrefix, string functionName)
|
||||||
|
{
|
||||||
|
functionName = functionName.ToLower();
|
||||||
|
|
||||||
|
//$$ are scope functions and are not called by prefix, we only have prefixes to make it easier to parse
|
||||||
|
// the functions in the wikiText and scope functions are easy enough since they start with curly braces.
|
||||||
|
var functionPrototype = Items.Where(o => (o.FunctionPrefix == functionPrefix || o.FunctionPrefix == "$$") && o.FunctionName == functionName).FirstOrDefault()?.Value;
|
||||||
|
|
||||||
|
return functionPrototype
|
||||||
|
?? throw new WikiFunctionPrototypeNotDefinedException($"Function ({functionName}) does not have a defined prototype.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private FunctionPrototype ParsePrototype(string prototypeString)
|
||||||
|
{
|
||||||
|
int nameStartIndex = prototypeString.TakeWhile(c => char.IsLetterOrDigit(c) == false).Count();
|
||||||
|
int nameEndIndex = prototypeString.IndexOf(':');
|
||||||
|
string properName = prototypeString.Substring(nameStartIndex, nameEndIndex - nameStartIndex).Trim();
|
||||||
|
string functionName = properName.ToLower();
|
||||||
|
string functionPrefix = prototypeString.Substring(0, nameStartIndex).Trim();
|
||||||
|
|
||||||
|
prototypeString = prototypeString.Substring(nameEndIndex + 1).Trim();
|
||||||
|
|
||||||
|
var prototype = new FunctionPrototype() { FunctionPrefix = functionPrefix, ProperName = properName, FunctionName = functionName };
|
||||||
|
|
||||||
|
if (prototypeString.Length == 0)
|
||||||
|
{
|
||||||
|
//No parameters.
|
||||||
|
return prototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segments = prototypeString.Trim().Split('|').Select(o => o.Trim());
|
||||||
|
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
var prototypeSegment = new PrototypeParameter();
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
if (segment[index] == '<')
|
||||||
|
{
|
||||||
|
index++; //Skip the '<'
|
||||||
|
prototypeSegment.Type = Tok(segment, ref index);
|
||||||
|
index++; //Skip the '>'
|
||||||
|
|
||||||
|
if (prototypeSegment.Type.Contains(':'))
|
||||||
|
{
|
||||||
|
var splitSeg = prototypeSegment.Type.Split(':');
|
||||||
|
prototypeSegment.Type = splitSeg[0];
|
||||||
|
if (splitSeg[1].Equals("infinite", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
prototypeSegment.IsInfinite = true;
|
||||||
|
if (prototype.Parameters.Any(o => o.IsInfinite))
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{functionName}], prototype error: cannot contain more than one [infinite] parameter.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{functionName}], prototype error: expected [infinite] got [{splitSeg[1]}].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipWhiteSpace(segment, ref index);
|
||||||
|
|
||||||
|
if (index < segment.Length && segment[index] == '{' || segment[index] == '[')
|
||||||
|
{
|
||||||
|
if (index < segment.Length && segment[index] == '[')
|
||||||
|
{
|
||||||
|
prototypeSegment.IsRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
index++; //Skip the '[' or '{'
|
||||||
|
|
||||||
|
prototypeSegment.Name = Tok(segment, ref index);
|
||||||
|
|
||||||
|
if (index < segment.Length && segment[index] == '(') //Parse allowed values.
|
||||||
|
{
|
||||||
|
int allowedValueEndIndex = segment.IndexOf(')', index);
|
||||||
|
string roteRequiredValues = segment.Substring(index + 1, allowedValueEndIndex - index - 1);
|
||||||
|
prototypeSegment.AllowedValues = roteRequiredValues.Trim().Split(',').Select(o => o.Trim().ToLower()).ToList();
|
||||||
|
|
||||||
|
index = allowedValueEndIndex;
|
||||||
|
index++; //Skip the ')'
|
||||||
|
SkipWhiteSpace(segment, ref index);
|
||||||
|
}
|
||||||
|
|
||||||
|
index++; //Skip the ']' or '}'
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{functionName}], prototype error: expected [{{] or [[].");
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipWhiteSpace(segment, ref index);
|
||||||
|
|
||||||
|
if (index < segment.Length && segment[index] == '=')
|
||||||
|
{
|
||||||
|
index++; //Skip the '='
|
||||||
|
SkipWhiteSpace(segment, ref index);
|
||||||
|
|
||||||
|
if (segment[index] != '\'')
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{functionName}], prototype error: expected [\'].");
|
||||||
|
}
|
||||||
|
|
||||||
|
index++; //Skip the '''
|
||||||
|
|
||||||
|
prototypeSegment.DefaultValue = segment.Substring(index, (segment.Length - index) - 1);
|
||||||
|
|
||||||
|
index = segment.Length - 1;
|
||||||
|
|
||||||
|
if (index < segment.Length && segment[index] != '\'')
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{functionName}], prototype error: expected [\'].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception($"Function [{functionName}], prototype error: expected [<].");
|
||||||
|
}
|
||||||
|
|
||||||
|
prototype.Parameters.Add(prototypeSegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the next token in a string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str"></param>
|
||||||
|
/// <param name="index"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string Tok(string str, ref int index)
|
||||||
|
{
|
||||||
|
var token = string.Empty;
|
||||||
|
|
||||||
|
SkipWhiteSpace(str, ref index);
|
||||||
|
|
||||||
|
for (; index < str.Length; index++)
|
||||||
|
{
|
||||||
|
if ("<>{}[]()".Contains(str[index]))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
token += str[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipWhiteSpace(str, ref index);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SkipWhiteSpace(string str, ref int index)
|
||||||
|
{
|
||||||
|
while (index < str.Length && char.IsWhiteSpace(str[index]))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
TightWiki.Engine.Function/NamedParameter.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public class NamedParameter
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
public NamedParameter(string name, string value)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
TightWiki.Engine.Function/OrdinalParameter.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public class OrdinalParameter
|
||||||
|
{
|
||||||
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has been matched to a prototype parameter?
|
||||||
|
/// </summary>
|
||||||
|
public bool IsMatched { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If matched to a prototype parameter, this is the name of the parameter.
|
||||||
|
/// </summary>
|
||||||
|
public string ParameterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public OrdinalParameter(string value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AssociateWithPrototypeParam(string paramName)
|
||||||
|
{
|
||||||
|
IsMatched = true;
|
||||||
|
ParameterName = paramName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
TightWiki.Engine.Function/ParsedFunctionCall.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public class ParsedFunctionCall
|
||||||
|
{
|
||||||
|
public string Prefix { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int EndIndex { get; set; }
|
||||||
|
public List<string> RawArguments { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public ParsedFunctionCall(string prefix, string name, int endIndex, List<string> rawArguments)
|
||||||
|
{
|
||||||
|
Prefix = prefix;
|
||||||
|
Name = name;
|
||||||
|
EndIndex = endIndex;
|
||||||
|
RawArguments = rawArguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
TightWiki.Engine.Function/PrototypeParameter.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public class PrototypeParameter
|
||||||
|
{
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string DefaultValue { get; set; } = string.Empty;
|
||||||
|
public bool IsRequired { get; set; } = false;
|
||||||
|
public bool IsInfinite { get; set; } = false;
|
||||||
|
public List<string> AllowedValues { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
TightWiki.Engine.Function/PrototypeSet.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public class PrototypeSet
|
||||||
|
{
|
||||||
|
public string FunctionPrefix { get; set; } = string.Empty;
|
||||||
|
public string ProperName { get; set; } = string.Empty;
|
||||||
|
public string FunctionName { get; set; } = string.Empty;
|
||||||
|
public FunctionPrototype Value { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
176
TightWiki.Engine.Function/SelfDocument.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
namespace TightWiki.Engine.Function
|
||||||
|
{
|
||||||
|
public static class SelfDocument
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Don't ever look at this. :(
|
||||||
|
/// </summary>
|
||||||
|
public static void CreateNotExisting()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
System.Threading.Thread.Sleep(500);
|
||||||
|
foreach (var item in FunctionPrototypeDefinitions.Collection.Items)
|
||||||
|
{
|
||||||
|
string functionType = "Function";
|
||||||
|
string functionPrefix = item.FunctionPrefix;
|
||||||
|
|
||||||
|
if (item.FunctionPrefix == "##")
|
||||||
|
{
|
||||||
|
functionType = "Standard Function";
|
||||||
|
}
|
||||||
|
if (item.FunctionPrefix == "@@")
|
||||||
|
{
|
||||||
|
functionType = "Instruction Function";
|
||||||
|
}
|
||||||
|
if (item.FunctionPrefix == "$$")
|
||||||
|
{
|
||||||
|
functionType = "Scope Function";
|
||||||
|
functionPrefix = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
string topic = $"Wiki Help :: {item.ProperName}";
|
||||||
|
|
||||||
|
if (functionType == "Instruction Function")
|
||||||
|
{
|
||||||
|
topic = $"Wiki Help :: {item.ProperName} - Instruction";
|
||||||
|
}
|
||||||
|
|
||||||
|
string navigation = CanonicalNavigation.CleanAndValidate(topic);
|
||||||
|
|
||||||
|
var page = PageRepository.GetPageInfoByNavigation(navigation);
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
var html = new System.Text.StringBuilder();
|
||||||
|
html.AppendLine("@@draft");
|
||||||
|
html.AppendLine("@@protect(true)");
|
||||||
|
html.AppendLine("##Image(Wiki Help :: Wiki Help/TightWiki Logo.png, 15)");
|
||||||
|
html.AppendLine($"##title ##Tag(Official-Help, Help, Wiki, Official, {functionType})");
|
||||||
|
html.AppendLine("{{Card(Default, Table of Contents) ##toc }}");
|
||||||
|
html.AppendLine("");
|
||||||
|
html.AppendLine("${metaColor = #ee2401}");
|
||||||
|
html.AppendLine("${keywordColor = #318000}");
|
||||||
|
html.AppendLine("${identifierColor = #c6680e}");
|
||||||
|
html.AppendLine("==Overview");
|
||||||
|
html.AppendLine($"The {item.ProperName} {functionType.ToLower()} is !!FILL_IN_THE_BLANK!!");
|
||||||
|
html.AppendLine("");
|
||||||
|
html.AppendLine("");
|
||||||
|
html.AppendLine("==Prototype");
|
||||||
|
html.Append($"##Color(${{keywordColor}}, **#{{ {functionPrefix}{item.ProperName} }}#**)");
|
||||||
|
if ((item.Value.Parameters?.Count ?? 0) == 0)
|
||||||
|
{
|
||||||
|
html.AppendLine("()");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
html.Append("(");
|
||||||
|
foreach (var p in item.Value.Parameters)
|
||||||
|
{
|
||||||
|
html.Append($"##Color(${{keywordColor}}, {p.Type}{(p.IsInfinite ? ":Infinite" : "")})");
|
||||||
|
if (p.IsRequired)
|
||||||
|
{
|
||||||
|
html.Append($" [##Color(${{identifierColor}}, {p.Name})]");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
html.Append($" {{##Color(${{identifierColor}}, {p.Name})}}");
|
||||||
|
}
|
||||||
|
html.Append(", ");
|
||||||
|
}
|
||||||
|
html.Length -= 3;
|
||||||
|
html.Append(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
html.AppendLine("");
|
||||||
|
html.AppendLine("");
|
||||||
|
html.AppendLine("");
|
||||||
|
html.AppendLine("===Parameters");
|
||||||
|
html.AppendLine("{{Bullets");
|
||||||
|
|
||||||
|
if (item.Value.Parameters.Count == 0)
|
||||||
|
{
|
||||||
|
html.AppendLine($"None.");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var p in item.Value.Parameters)
|
||||||
|
{
|
||||||
|
html.AppendLine($"**Name:** ##Color(${{identifierColor}}, {p.Name}) ##Color(${{metaColor}}, {(p.IsRequired ? "[Required]" : "{Optional}")})");
|
||||||
|
html.AppendLine($">**Type:** ##Color(${{keywordColor}}, {p.Type}{(p.IsInfinite ? ":Infinite" : "")})");
|
||||||
|
if (string.IsNullOrEmpty(p.DefaultValue) == false)
|
||||||
|
{
|
||||||
|
html.AppendLine($">**Default:** ##Color(${{identifierColor}}, {p.DefaultValue})");
|
||||||
|
}
|
||||||
|
if (p.AllowedValues != null)
|
||||||
|
{
|
||||||
|
html.AppendLine($">**Values:** ##Color(${{identifierColor}}, \"{string.Join(", ", p.AllowedValues)}\")");
|
||||||
|
}
|
||||||
|
html.AppendLine($">**Description:** !!FILL_IN_THE_BLANK!!");
|
||||||
|
}
|
||||||
|
html.AppendLine("}}");
|
||||||
|
html.AppendLine("");
|
||||||
|
|
||||||
|
html.AppendLine("==Examples");
|
||||||
|
html.AppendLine("{{Code(wiki)#{");
|
||||||
|
|
||||||
|
|
||||||
|
if (item.FunctionPrefix == "$$")
|
||||||
|
{
|
||||||
|
html.Append("{{ " + $"{item.ProperName}");
|
||||||
|
if ((item.Value.Parameters?.Count ?? 0) == 0)
|
||||||
|
{
|
||||||
|
html.AppendLine("()");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
html.AppendLine($"({string.Join(", ", item.Value.Parameters.Select(o => o.Name))})");
|
||||||
|
}
|
||||||
|
|
||||||
|
html.AppendLine("This is the body content of the function scope.");
|
||||||
|
|
||||||
|
html.AppendLine("}}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
html.Append($"{item.FunctionPrefix}{item.ProperName}");
|
||||||
|
if ((item.Value.Parameters?.Count ?? 0) == 0)
|
||||||
|
{
|
||||||
|
html.AppendLine("()");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
html.AppendLine($"({string.Join(", ", item.Value.Parameters.Select(o => o.Name))})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.AppendLine("}#}}");
|
||||||
|
html.AppendLine("");
|
||||||
|
|
||||||
|
html.AppendLine("==See Also");
|
||||||
|
html.AppendLine("[[Wiki Help :: Function Calling Convention]]");
|
||||||
|
html.AppendLine("[[Wiki Help :: Scope Function]]");
|
||||||
|
html.AppendLine("[[Wiki Help :: Instruction Function]]");
|
||||||
|
html.AppendLine("[[Wiki Help :: Standard Function]]");
|
||||||
|
html.AppendLine("");
|
||||||
|
html.AppendLine("");
|
||||||
|
|
||||||
|
html.AppendLine("==Related");
|
||||||
|
html.AppendLine("##related");
|
||||||
|
|
||||||
|
page = new Page()
|
||||||
|
{
|
||||||
|
Navigation = navigation,
|
||||||
|
Name = topic,
|
||||||
|
Description = $"Documentation of the built-in {item.ProperName.ToLower()} {functionType.ToLower()} !!FILL_IN_THE_BLANK!!.",
|
||||||
|
CreatedByUserId = 1,
|
||||||
|
CreatedDate = DateTime.UtcNow,
|
||||||
|
ModifiedByUserId = 1,
|
||||||
|
ModifiedDate = DateTime.UtcNow,
|
||||||
|
Body = html.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
PageRepository.SavePage(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
TightWiki.Engine.Function/TightWiki.Engine.Function.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<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>
|
||||||
|
<PackageReference Include="NTDLS.Helpers" Version="1.3.11" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
9
TightWiki.Engine.Implementation/AggregatedSearchToken.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
TightWiki.Engine.Implementation/CommentHandler.cs
Normal 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] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
TightWiki.Engine.Implementation/CompletionHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
TightWiki.Engine.Implementation/EmojiHandler.cs
Normal 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] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
TightWiki.Engine.Implementation/ExceptionHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
TightWiki.Engine.Implementation/ExternalLinkHandler.cs
Normal 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]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
TightWiki.Engine.Implementation/HeadingHandler.cs
Normal 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] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
TightWiki.Engine.Implementation/Helpers.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
TightWiki.Engine.Implementation/InternalLinkHandler.cs
Normal 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]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
TightWiki.Engine.Implementation/MarkupHandler.cs
Normal 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] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
TightWiki.Engine.Implementation/PostProcessingFunctionHandler.cs
Normal 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] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
373
TightWiki.Engine.Implementation/ScopeFunctionHandler.cs
Normal 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] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1187
TightWiki.Engine.Implementation/StandardFunctionHandler.cs
Normal 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>
|
||||||
70
TightWiki.Engine.Implementation/Utility/BGFGStyle.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
TightWiki.Engine.Implementation/Utility/Differentiator.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
TightWiki.Engine.Implementation/Utility/SearchCloud.cs
Normal 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 + " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudHtml.Append("</p></td></tr></table>");
|
||||||
|
|
||||||
|
return cloudHtml.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
TightWiki.Engine.Implementation/Utility/TagCloud.cs
Normal 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 + " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudHtml.Append("</p></td></tr></table>");
|
||||||
|
|
||||||
|
return cloudHtml.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
TightWiki.Engine.Implementation/WeightedSearchToken.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace TightWiki.Engine.Implementation
|
||||||
|
{
|
||||||
|
public class WeightedSearchToken
|
||||||
|
{
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
public double Weight { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
44
TightWiki.Engine.Library/Constants.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
namespace TightWiki.Engine.Library
|
||||||
|
{
|
||||||
|
public class Constants
|
||||||
|
{
|
||||||
|
public const string SoftBreak = "<!--SoftBreak-->"; //These will remain as \r\n in the final HTML.
|
||||||
|
public const string HardBreak = "<!--HardBreak-->"; //These will remain as <br /> in the final HTML.
|
||||||
|
|
||||||
|
public enum WikiMatchType
|
||||||
|
{
|
||||||
|
ScopeFunction,
|
||||||
|
Emoji,
|
||||||
|
Instruction,
|
||||||
|
Comment,
|
||||||
|
Variable,
|
||||||
|
Markup,
|
||||||
|
Error,
|
||||||
|
StandardFunction,
|
||||||
|
Link,
|
||||||
|
Heading,
|
||||||
|
Literal
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HandlerResultInstruction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Does not process the match, allowing it to be processed by another handler.
|
||||||
|
/// </summary>
|
||||||
|
Skip,
|
||||||
|
/// <summary>
|
||||||
|
/// Removes any single trailing newline after match.
|
||||||
|
/// </summary>
|
||||||
|
TruncateTrailingLine,
|
||||||
|
/// <summary>
|
||||||
|
/// Will not continue to process content in this block.
|
||||||
|
/// </summary>
|
||||||
|
DisallowNestedProcessing,
|
||||||
|
/// <summary>
|
||||||
|
/// As opposed to the default functionality of replacing all matches, this will cause ony the first match to be replaced.
|
||||||
|
/// This also means that each match will be processed individually, which can impact performance.
|
||||||
|
/// </summary>
|
||||||
|
OnlyReplaceFirstMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
TightWiki.Engine.Library/HandlerResult.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using static TightWiki.Engine.Library.Constants;
|
||||||
|
|
||||||
|
namespace TightWiki.Engine.Library
|
||||||
|
{
|
||||||
|
public class HandlerResult
|
||||||
|
{
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<HandlerResultInstruction> Instructions { get; set; } = new();
|
||||||
|
|
||||||
|
public HandlerResult()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public HandlerResult(string content)
|
||||||
|
{
|
||||||
|
Content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
TightWiki.Engine.Library/Interfaces/ICommentHandler.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles wiki comments. These are generally removed from the result.
|
||||||
|
/// </summary>
|
||||||
|
public interface 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
TightWiki.Engine.Library/Interfaces/ICompletionHandler.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles wiki completion events.
|
||||||
|
/// </summary>
|
||||||
|
public interface 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
TightWiki.Engine.Library/Interfaces/IEmojiHandler.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles wiki emojis.
|
||||||
|
/// </summary>
|
||||||
|
public interface 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
TightWiki.Engine.Library/Interfaces/IExceptionHandler.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles exceptions thrown by the wiki engine.
|
||||||
|
/// </summary>
|
||||||
|
public interface 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
TightWiki.Engine.Library/Interfaces/IExternalLinkHandler.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles links the wiki to another site.
|
||||||
|
/// </summary>
|
||||||
|
public interface 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
TightWiki.Engine.Library/Interfaces/IFunctionHandler.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using TightWiki.Engine.Function;
|
||||||
|
|
||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base function handler for standard, post-processing, scoped and processing-instruction functions.
|
||||||
|
/// </summary>
|
||||||
|
public interface IFunctionHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a collection of function prototypes.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public FunctionPrototypeCollection Prototypes { get; }
|
||||||
|
|
||||||
|
/// <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">For scope functions, this is the text that the function is designed to affect.</param>
|
||||||
|
public HandlerResult Handle(ITightEngineState state, FunctionCall function, string? scopeBody = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
TightWiki.Engine.Library/Interfaces/IHeadingHandler.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles wiki headings. These are automatically added to the table of contents.
|
||||||
|
/// </summary>
|
||||||
|
public interface 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
TightWiki.Engine.Library/Interfaces/IInternalLinkHandler.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using TightWiki.Library;
|
||||||
|
|
||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles links from one wiki page to another.
|
||||||
|
/// </summary>
|
||||||
|
public interface 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
TightWiki.Engine.Library/Interfaces/IMarkupHandler.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles basic markup/style instructions like bole, italic, underline, etc.
|
||||||
|
/// </summary>
|
||||||
|
public interface 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles post-processing function calls.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPostProcessingFunctionHandler : IFunctionHandler
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles processing-instruction function calls.
|
||||||
|
/// </summary>
|
||||||
|
public interface IProcessingInstructionFunctionHandler : IFunctionHandler
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles scope function calls.
|
||||||
|
/// </summary>
|
||||||
|
public interface IScopeFunctionHandler : IFunctionHandler
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles standard function calls.
|
||||||
|
/// </summary>
|
||||||
|
public interface IStandardFunctionHandler : IFunctionHandler
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
24
TightWiki.Engine.Library/Interfaces/ITightEngine.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using TightWiki.Library.Interfaces;
|
||||||
|
using static TightWiki.Engine.Library.Constants;
|
||||||
|
|
||||||
|
namespace TightWiki.Engine.Library.Interfaces
|
||||||
|
{
|
||||||
|
|
||||||
|
public interface ITightEngine
|
||||||
|
{
|
||||||
|
IScopeFunctionHandler ScopeFunctionHandler { get; }
|
||||||
|
IStandardFunctionHandler StandardFunctionHandler { get; }
|
||||||
|
IProcessingInstructionFunctionHandler ProcessingInstructionFunctionHandler { get; }
|
||||||
|
IPostProcessingFunctionHandler PostProcessingFunctionHandler { get; }
|
||||||
|
IMarkupHandler MarkupHandler { get; }
|
||||||
|
IHeadingHandler HeadingHandler { get; }
|
||||||
|
ICommentHandler CommentHandler { get; }
|
||||||
|
IEmojiHandler EmojiHandler { get; }
|
||||||
|
IExternalLinkHandler ExternalLinkHandler { get; }
|
||||||
|
IInternalLinkHandler InternalLinkHandler { get; }
|
||||||
|
IExceptionHandler ExceptionHandler { get; }
|
||||||
|
ICompletionHandler CompletionHandler { get; }
|
||||||
|
ITightEngineState Transform(ISessionState? sessionState, IPage page, int? revision = null, WikiMatchType[]? omitMatches = null);
|
||||||
|
//ITightEngineState TransformChild(ITightEngineState parent, IPage page, int? revision = null);
|
||||||
|
}
|
||||||
|
}
|
||||||