Skip to content

Commit

Permalink
refactor feature utils into service
Browse files Browse the repository at this point in the history
  • Loading branch information
sharpchen committed Apr 10, 2024
1 parent ea2e19f commit 88819bf
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 170 deletions.
149 changes: 3 additions & 146 deletions docs/data/Features.data.ts
Original file line number Diff line number Diff line change
@@ -1,151 +1,8 @@
import matter from 'gray-matter';
import Enumerable from 'linq';
import { type Feature } from 'vitepress/dist/client/theme-default/components/VPFeatures.vue';
import { DirectoryInfo, Path, documentRoot } from '../shared/FileSystem';
import { featureService } from '../services/FeatureService';

// const matter = require('gray-matter');
const featuresLiteral = `---
features:
- title: SQL
details: SQL syntax for beginners with MySQL
icon: 📝
linkText: Get started
- title: Docker
details: Ultimate Docker
icon: 🐋
linkText: Get started
- title: C# Design Patterns
details: Design Patterns in C#
icon: 👾
linkText: Get started
- title: JavaScript
details: JavaScript for C# developer
icon: 😅
linkText: Get started
- title: TypeScript
details: TypeScript for C# developer
icon: ⌨
link: /
linkText: Get started
- title: Rust
details: Rust for C# developer
icon: 🦀
link: /
linkText: Get started
- title: Python
details: Python for C# developer
icon: 🐍
linkText: Get started
- title: Vue3
details: Vue3 for .NET blazor developer
icon: ⚡
linkText: Get started
- title: VBA
details: VBA for excel
icon: 💩
linkText: Get started
- title: Modern C#
details: Modernized C# since 2015?
icon: 🐱‍👤
linkText: Get started
- title: Avalonia
details: AvaloniaUI
icon: 😱
linkText: Get started
- title: Git
details: Git mastery
icon: 🐱
linkText: Get started
---
`;
const articleLiteral = `---
features:
- title: Articles
details: Regular articles
icon: 📰
linkText: Let's go
---`;
const getIndexLink = (title: string): string | undefined => {
const docs = documentRoot()
.getDirectories()
.find(x => x.name.toLowerCase() === title.toLowerCase())
?.getDirectories()
.find(x => x.name === 'docs');
if (!docs) return;
// has multiple chapters
if (docs.getDirectories().length > 0) {
const { first: folder, level } = findFirstFolder(docs);
const file = folder?.getFiles()[0];
let name = `${documentRoot().name}/${title}/docs/`;
for (let i = level - 1; i > 0; i--) {
name += file?.directory.up(i)?.name + '/';
}
return `${name}${folder?.name}/${Path.GetFileNameWithoutExtension(file?.name!)}`;
}
// no chapter
if (docs.getFiles().length > 0) {
const file = Enumerable.from(docs.getFiles())
.orderBy(x => x.name)
.firstOrDefault();
return `${documentRoot().name}/${title}/docs/${Path.GetFileNameWithoutExtension(file?.name!)}`;
}
};
function addLinkToFeatures(features: Feature[]): Feature[] {
/**
* folder names
*/
const names = documentRoot()
.getDirectories()
.map(x => x.name);
for (const key in features) {
if (Object.prototype.hasOwnProperty.call(features, key)) {
const feature = features[key];
/**
* Handel exceptions for C#
* @param name folder name
* @returns handled
*/
const predicate = (name: string) => {
const replaced = name.toLowerCase().includes('csharp')
? name.toLowerCase().replace('csharp', 'C#')
: name;
return feature.title.toLowerCase() === replaced.toLowerCase();
};
const match = names.find(x => predicate(x));
if (match) {
// cs design pattern has conflict that I just leave it with a simple solution.
const title = feature.title.includes('C#')
? feature.title.replace('C#', 'CSharp')
: feature.title;
const link = getIndexLink(title);
feature.link = link ? link : '/';
}
}
}
return features;
}

function findFirstFolder(
current: DirectoryInfo,
level: number = 1
): { first?: DirectoryInfo; level: number } {
// has direct file in sub folder
const first = Enumerable.from(current.getDirectories())
.where(x => x.getFiles().length > 0)
.orderBy(x => x.name)
.firstOrDefault();
if (!first) {
const next = Enumerable.from(current.getDirectories())
.orderBy(x => x.name)
.firstOrDefault();
if (!next) return { first: undefined, level: level + 1 };
return findFirstFolder(next, level + 1);
}
return { first, level };
}

const featuresItems: Feature[] = addLinkToFeatures(matter(featuresLiteral).data.features);
const articleFeature: Feature[] = addLinkToFeatures(matter(articleLiteral).data.features);
const featuresItems: Feature[] = featureService.getFeatures();
const articleFeature: Feature[] = featureService.getArticleFeature();
const loader = {
load: (): FeatureCompose => ({ features: featuresItems, articleFeature: articleFeature }),
};
Expand Down
89 changes: 83 additions & 6 deletions docs/services/DocumentService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
import { DocumentName, documentMap } from '../services/IDocumentService';
// import { DocumentName, documentMap } from '../services/IDocumentService';
import Enumerable from 'linq';
import * as File from '../shared/FileSystem';
import { IDocumentService } from './IDocumentService';

class DocumentService implements IDocumentService {
export type DocumentInfo = Record<string, { icon: string; description: string }>;
export const documentMap = {
'Csharp Design Patterns': { icon: '👾', description: 'Design Patterns in C#' },
'Modern CSharp': { icon: '🐱‍👤', description: 'Modernized C# since 2015?' },
Articles: { icon: '📰', description: 'Regular articles' },
Avalonia: { icon: '😱', description: 'AvaloniaUI' },
Docker: { icon: '🐋', description: 'Ultimate Docker' },
Git: { icon: '🐱', description: 'Git mastery' },
JavaScript: { icon: '😅', description: 'JavaScript for C# developer' },
SQL: { icon: '📝', description: 'SQL syntax for beginners with MySQL' },
TypeScript: { icon: '⌨', description: 'TypeScript for C# developer' },
VBA: { icon: '💩', description: 'VBA for excel' },
Vue3: { icon: '⚡', description: 'Vue3 for .NET blazor developer' },
} as const satisfies DocumentInfo;
export type DocumentName = keyof typeof documentMap;
export type DocumentIcon = (typeof documentMap)[DocumentName]['icon'];
export type DocumentDescription = (typeof documentMap)[DocumentName]['description'];
export class DocumentService implements IDocumentService {
isEmptyDocument(name: DocumentName): boolean {
try {
const entry = this.getMarkdownEntryFolder(name);
return entry.getFiles().length === 0 && entry.getDirectories().length === 0;
} catch (error) {
return true;
}
}
readonly documentInfo: DocumentInfo = documentMap;
getDocumentEntryFolder(name: DocumentName): File.DirectoryInfo {
const ret = this.registeredDocumentFolders().find(x => x.name === name);
if (!ret) throw new Error(`Document entry of ${name} not found.`);
if (!ret) throw new Error(`Document entry of "${name}" not found.`);
return ret;
}
registeredDocumentFolders(): File.DirectoryInfo[] {
Expand All @@ -18,7 +45,7 @@ class DocumentService implements IDocumentService {
const ret = this.getDocumentEntryFolder(name)
.getDirectories()
.find(x => x.name === 'docs');
if (!ret) throw new Error(`Markdown entry of ${name} not found.`);
if (!ret) throw new Error(`Markdown entry of "${name}" not found.`);
return ret;
}
registeredCount(): number {
Expand All @@ -30,8 +57,31 @@ class DocumentService implements IDocumentService {
physicalCountBy(f: (x: File.DirectoryInfo) => boolean): number {
return this.documentSrc.getDirectories().filter(x => f(x)).length;
}
getIndexLinkOfDocument(name: DocumentName): string {
throw new Error('Method not implemented.');
tryGetIndexLinkOfDocument(name: DocumentName): string {
if (this.isEmptyDocument(name)) return '/';
const solveSharpSign = (link: string) => {
if (link.includes('Csharp')) return link.replace('#', 'Csharp');
return link.replace('#', 'Sharp');
};
const shouldSolveSharpSign = (name: DocumentName) => name.includes('#');
const markdownEntry = this.getMarkdownEntryFolder(name);
let linkContext = `${this.documentSrc.name}/${name}/`;
if (markdownEntry.getFiles().length) {
const file = Enumerable.from(markdownEntry.getFiles())
.orderBy(x => x.name)
.first();
const link = `${linkContext}/docs/${File.Path.GetFileNameWithoutExtension(file?.name!)}`;
return shouldSolveSharpSign(name) ? solveSharpSign(link) : link;
}
const { firstFolder, depth } = this.tryGetFirstChapterFolderOfDocument(name);
const file = firstFolder?.getFiles()[0];
for (let i = depth - 1; i > 0; i--) {
linkContext += file?.directory.up(i)?.name + '/';
}
const link = `${linkContext}${firstFolder?.name}/${File.Path.GetFileNameWithoutExtension(
file?.name!
)}`;
return shouldSolveSharpSign(name) ? solveSharpSign(link) : link;
}
get documentSrc(): File.DirectoryInfo {
const ret = File.projectRoot()
Expand All @@ -40,6 +90,33 @@ class DocumentService implements IDocumentService {
if (!ret) throw new Error('Document source not found.');
return ret;
}
tryGetFirstChapterFolderOfDocument(name: DocumentName): {
firstFolder: File.DirectoryInfo;
depth: number;
} {
const markdownEntry = this.getMarkdownEntryFolder(name);
return getFirst(markdownEntry);

function getFirst(
current: File.DirectoryInfo,
depth: number = 1
): { firstFolder: File.DirectoryInfo; depth: number } {
const nextLevelsSorted = Enumerable.from(
current
.getDirectories()
.filter(x => x.getFiles().length > 0 || x.getDirectories().length > 0)
).orderBy(x => x.name);
//if no folder
if (!nextLevelsSorted.count()) return { firstFolder: current, depth: depth };
//if has folders
return getFirst(nextLevelsSorted.first(), depth + 1);
}
}
tryGetFormulaNameOfDocument(name: DocumentName): string {
if (name.includes('Csharp')) return name.replace('Csharp', 'C#');
if (name.includes('Sharp')) return name.replace('Sharp', '#');
return name;
}
}

export const documentService: IDocumentService = new DocumentService();
43 changes: 43 additions & 0 deletions docs/services/FeatureService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import matter from 'gray-matter';
import { type Feature } from 'vitepress/dist/client/theme-default/components/VPFeatures.vue';
import { DocumentName, documentService } from './DocumentService';
import { IFeatureService } from './IFeatureService';
export class FeatureService implements IFeatureService {
readonly linkText: string = 'Get started';
getFeaturesAsYaml(): string {
return matter.stringify('', { features: this.getFeatures() });
}
getArticleFeatureAsYaml(): string {
return matter.stringify('', { features: this.getArticleFeature() });
}
getArticleFeature(): Feature[] {
const info = documentService.documentInfo['Articles' as DocumentName];
return [
{
title: 'Articles' as DocumentName,
details: info.description,
icon: info.icon,
link: documentService.tryGetIndexLinkOfDocument('Articles' as DocumentName),
},
];
}
getFeatures(): Feature[] {
const features: Feature[] = [];
for (const key in documentService.documentInfo) {
if (Object.prototype.hasOwnProperty.call(documentService.documentInfo, key)) {
const documentInfo = documentService.documentInfo[key];
if ((key as DocumentName) !== 'Articles')
features.push({
title: documentService.tryGetFormulaNameOfDocument(key as DocumentName),
details: documentInfo.description,
icon: documentInfo.icon,
link: documentService.tryGetIndexLinkOfDocument(key as DocumentName),
linkText: this.linkText,
});
}
}
return features;
}
}

export const featureService: IFeatureService = new FeatureService();
25 changes: 9 additions & 16 deletions docs/services/IDocumentService.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import * as File from '../shared/FileSystem';
import { DocumentInfo, DocumentName } from './DocumentService';

export interface IDocumentService {
readonly documentInfo: DocumentInfo;
physicalCount(): number;
registeredCount(): number;
physicalCountBy(f: (x: File.DirectoryInfo) => boolean): number;
getIndexLinkOfDocument(name: DocumentName): string;
tryGetIndexLinkOfDocument(name: DocumentName): string;
get documentSrc(): File.DirectoryInfo;
getMarkdownEntryFolder(name: DocumentName): File.DirectoryInfo;
getDocumentEntryFolder(name: DocumentName): File.DirectoryInfo;
registeredDocumentFolders(): File.DirectoryInfo[];
physicalDocumentFolders(): File.DirectoryInfo[];
tryGetFirstChapterFolderOfDocument(name: DocumentName): {
firstFolder: File.DirectoryInfo;
depth: number;
};
tryGetFormulaNameOfDocument(name: DocumentName): string;
isEmptyDocument(name: DocumentName): boolean;
}
export const documentMap = {
'Csharp Design Patterns': '👾',
'Modern CSharp': '🐱‍👤',
Articles: '📰',
Avalonia: '😱',
Docker: '🐋',
Git: '🐱',
JavaScript: '😅',
SQL: '📝',
TypeScript: '⌨',
VBA: '💩',
Vue3: '⚡',
} as const;
export type DocumentName = keyof typeof documentMap;
export type DocumentIcon = (typeof documentMap)[keyof typeof documentMap];
8 changes: 8 additions & 0 deletions docs/services/IFeatureService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Feature } from 'vitepress/dist/client/theme-default/components/VPFeatures.vue';

export interface IFeatureService {
getFeaturesAsYaml(): string;
getArticleFeatureAsYaml(): string;
getFeatures(): Feature[];
getArticleFeature(): Feature[];
}
2 changes: 1 addition & 1 deletion docs/services/ISidebarService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DefaultTheme } from 'vitepress';
import { DocumentName } from './IDocumentService';
import { DocumentName } from './DocumentService';
import * as File from '../shared/FileSystem';
import { IDocumentService } from './IDocumentService';
export interface ISidebarService {
Expand Down
2 changes: 1 addition & 1 deletion docs/services/SidebarService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Enumerable from 'linq';
import { DefaultTheme } from 'vitepress';
import { DocumentName, documentMap } from './IDocumentService';
import { DocumentName, documentMap } from './DocumentService';
import { DirectoryInfo, FileInfo, Path, documentRoot } from '../shared/FileSystem';
import { documentService } from './DocumentService';
import { IDocumentService } from './IDocumentService';
Expand Down

0 comments on commit 88819bf

Please sign in to comment.