Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Source/docs/Documentation/code-snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ The snippet will include:
- The code with proper syntax highlighting
- A link to the source file in GitHub with line numbers

## Multi-language Snippets

To render language tabs from language-specific snippet files, use:

```markdown
:::multilang title="Appending Events":::
```

The title is converted to `appending-events.md`, and snippets are
resolved from a `snippets/` folder in the same hierarchy as the
markdown file. Language variants are discovered in predefined folders:

- Current folder (C# tab)
- `<FolderName>.TypeScript` sibling folder (TypeScript tab)

If a language file is missing, that language tab is omitted.

## Naming Conventions

Snippet names should:
Expand Down
7 changes: 4 additions & 3 deletions Source/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
"build:storybooks": "NODE_OPTIONS=\"--loader ts-node/esm\" node build-storybooks.ts",
"preprocess:storybooks": "NODE_OPTIONS=\"--loader ts-node/esm\" node preprocess-storybooks.ts",
"postprocess:storybooks": "NODE_OPTIONS=\"--loader ts-node/esm\" node postprocess-storybooks.ts",
"build": "yarn build:storybooks && yarn build:ts && yarn extract-snippets && yarn insert-snippets && yarn preprocess:storybooks && dotnet build -t:BuildApiDependencies -c Release -p:TargetFramework=net10.0 && dotnet docfx && yarn postprocess:storybooks",
"build:articles": "yarn preprocess:storybooks && dotnet docfx build && yarn postprocess:storybooks",
"build": "yarn build:storybooks && yarn build:ts && yarn extract-snippets && yarn insert-snippets && yarn preprocess:multilang && yarn preprocess:storybooks && dotnet build -t:BuildApiDependencies -c Release -p:TargetFramework=net10.0 && dotnet docfx && yarn postprocess:storybooks",
"build:articles": "yarn preprocess:multilang && yarn preprocess:storybooks && dotnet docfx build && yarn postprocess:storybooks",
"serve:landing": "yarn dlx browser-sync start --server . --startPath index.html --files index.html --port 8089 --no-ui --no-notify",
"serve": "yarn http-server ./_site",
"fixts": "NODE_OPTIONS=\"--loader ts-node/esm\" node fix-tsdocs.ts",
"extract-snippets": "NODE_OPTIONS=\"--loader ts-node/esm\" node extract-code-snippets.ts",
"preprocess:multilang": "NODE_OPTIONS=\"--loader ts-node/esm\" node preprocess-multilang.ts",
"insert-snippets": "NODE_OPTIONS=\"--loader ts-node/esm\" node insert-sample-snippets.ts",
"postinstall": "yarn restore && yarn build"
},
Expand Down Expand Up @@ -65,7 +66,7 @@
"@types/node": "22.10.2",
"concurrently": "7.3.0",
"cross-env": "7.0.3",
"glob": "11.0.0",
"glob": "11.1.0",
"http-server": "14.1.1",
"js-yaml": "4.1.0",
"nodemon": "3.1.9",
Expand Down
120 changes: 120 additions & 0 deletions Source/preprocess-multilang.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { glob } from 'glob';
import * as fs from 'fs';
import * as path from 'path';

interface Language {
displayName: string;
tabId: string;
resolveRootFolder: (folderName: string) => string;
}

const languages: Language[] = [
{
displayName: 'C#',
tabId: 'csharp',
resolveRootFolder: folderName => folderName
},
{
displayName: 'TypeScript',
tabId: 'typescript',
resolveRootFolder: folderName => `${folderName}.TypeScript`
}
];

const markdownFiles = await glob('**/*.md', {
ignore: [
'**/node_modules/**',
'**/.yarn/**',
'**/_site/**',
'**/obj/**',
'**/bin/**'
],
follow: true
});

const multilangRegex = /:::multilang\s+title="([^"]+)"\s*:::/g;

const rootPath = process.cwd();

function getSnippetPathForLanguage(markdownFile: string, title: string, language: Language): string {
const markdownAbsolutePath = path.resolve(markdownFile);
const markdownDirectory = path.dirname(markdownAbsolutePath);
const fileName = title
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') + '.md';
const currentSnippetPath = path.join(markdownDirectory, 'snippets', fileName);
const relativePath = path.relative(rootPath, currentSnippetPath);
const pathParts = relativePath.split(path.sep);

if (pathParts.length === 0) {
return currentSnippetPath;
}

const languageRoot = language.resolveRootFolder(pathParts[0]);
const languageSnippetPath = path.join(rootPath, languageRoot, ...pathParts.slice(1));
return languageSnippetPath;
}

function buildMultilangTabs(markdownFile: string, title: string): string {
const tabs = languages
.map(language => {
const snippetPath = getSnippetPathForLanguage(markdownFile, title, language);
if (!fs.existsSync(snippetPath)) {
return null;
}

const content = fs.readFileSync(snippetPath, 'utf-8').trim();
if (content === '') {
return null;
}

return `# [${language.displayName}](#tab/${language.tabId})\n\n${content}`;
})
.filter((tab): tab is string => tab !== null);

return tabs.join('\n\n---\n\n');
}

console.log('\n\nPreprocess multilang directives\n');

let count = 0;

await Promise.all(markdownFiles.map(async file => {
const content = await fs.promises.readFile(file, 'utf-8');
let replaced = false;

const updatedContent = content.replace(multilangRegex, (match, title, offset) => {
const beforeMatch = content.substring(0, offset);

const codeBlocksBefore = (beforeMatch.match(/```/g) || []).length;
if (codeBlocksBefore % 2 !== 0) {
return match;
}

const closedCodeBlocksRegex = /```[\s\S]*?```/g;
const beforeMatchWithoutCodeBlocks = beforeMatch.replace(closedCodeBlocksRegex, '');
const backticksBefore = (beforeMatchWithoutCodeBlocks.match(/`/g) || []).length;
if (backticksBefore % 2 !== 0) {
return match;
}

const tabs = buildMultilangTabs(file, title);
if (tabs === '') {
console.warn(`No multilang snippets found for '${title}' in ${file}`);
return match;
}

replaced = true;
count++;
process.stdout.write('.');
return tabs;
});

if (replaced) {
await fs.promises.writeFile(file, updatedContent);
}
}));

console.log(`\n\n${count} multilang directives inserted\n`);
37 changes: 35 additions & 2 deletions Source/styles/docfx.js
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,7 @@ $(function () {
name: 'data-bi-name',
type: 'data-bi-type'
};
var tabsStorageKey = 'docfx-selected-tabs';

var Tab = (function () {
function Tab(li, a, section) {
Expand Down Expand Up @@ -855,8 +856,10 @@ $(function () {

function initTabs(container) {
var queryStringTabs = readTabsQueryStringParam();
var savedTabs = readTabsFromLocalStorage();
var initialTabs = queryStringTabs.length > 0 ? queryStringTabs : savedTabs;
var elements = container.querySelectorAll('.tabGroup');
var state = { groups: [], selectedTabs: [] };
var state = { groups: [], selectedTabs: initialTabs.slice() };
for (var i = 0; i < elements.length; i++) {
var group = initTabGroup(elements.item(i));
if (!group.independent) {
Expand All @@ -868,8 +871,9 @@ $(function () {
if (state.groups.length === 0) {
return state;
}
selectTabs(queryStringTabs, container);
selectTabs(initialTabs, container);
updateTabsQueryStringParam(state);
updateTabsLocalStorage(state);
notifyContentUpdated();
return state;
}
Expand Down Expand Up @@ -970,6 +974,7 @@ $(function () {
updateVisibilityAndSelection(group_1, state);
}
updateTabsQueryStringParam(state);
updateTabsLocalStorage(state);
}
notifyContentUpdated();
var top = info.anchor.getBoundingClientRect().top;
Expand Down Expand Up @@ -1008,6 +1013,34 @@ $(function () {
history.replaceState({}, document.title, url);
}

function readTabsFromLocalStorage() {
if (!window.localStorage) {
return [];
}
var tabs;
try {
tabs = window.localStorage.getItem(tabsStorageKey);
}
catch (_a) {
return [];
}
if (tabs === null || tabs === '') {
return [];
}
return tabs.split(',');
}

function updateTabsLocalStorage(state) {
if (!window.localStorage) {
return;
}
try {
window.localStorage.setItem(tabsStorageKey, state.selectedTabs.join(','));
}
catch (_a) {
}
}

function toQueryString(args) {
var parts = [];
for (var name_1 in args) {
Expand Down