更新客户端渲染,更新了壳

This commit is contained in:
QWQLwToo
2026-07-06 23:05:40 +08:00
parent e7dd87bf7e
commit 31d778710b
1311 changed files with 172662 additions and 1582 deletions
+78
View File
@@ -0,0 +1,78 @@
@import "https://fonts.googleapis.com/css2?family=Montserrat:wght@100;300;500;700;900&family=Raleway:wght@300;500;700;900&display=swap";
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
$enable-important-utilities: false;
$container-max-widths: (
xxl: 1768px
) !default;
@import "mixins";
@import "bootstrap/scss/bootstrap";
@import "highlight";
@import "layout";
@import "nav";
@import "toc";
@import "markdown";
@import "search";
@import "dotnet";
@import "wpfui";
h1,
h2,
h3,
h4,
h5,
h6,
.xref,
.text-break {
word-wrap: break-word;
word-break: break-word;
}
.divider {
margin: 0 5px;
color: #ccc;
}
article {
// For REST API view source link
span.small.pull-right {
float: right;
}
img {
max-width: 100%;
height: auto;
}
}
.codewrapper {
position: relative;
}
.sample-response .response-content {
max-height: 200px;
}
@media (width <= 768px) {
#mobile-indicator {
display: block;
}
.mobile-hide {
display: none;
}
/* workaround for #hashtag url is no longer needed */
h1::before,
h2::before,
h3::before,
h4::before {
content: "";
display: none;
}
}
+51
View File
@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import 'bootstrap'
import { DocfxOptions } from './options'
import { highlight } from './highlight'
import { renderMarkdown } from './markdown'
import { enableSearch } from './search'
import { renderToc } from './toc'
import { initTheme } from './theme'
import { renderBreadcrumb, renderInThisArticle, renderNavbar } from './nav'
import { renderIndexStats } from './wpfui-index-stats'
import 'bootstrap-icons/font/bootstrap-icons.scss'
import './docfx.scss'
declare global {
interface Window {
docfx: DocfxOptions & {
ready?: boolean,
searchReady?: boolean,
searchResultReady?: boolean,
}
}
}
export async function init() {
const options = {
defaultTheme: 'dark'
} as DocfxOptions
window.docfx = Object.assign({}, options)
initTheme()
enableSearch()
renderInThisArticle()
renderIndexStats()
await Promise.all([
renderMarkdown(),
renderNav(),
highlight()
])
window.docfx.ready = true
async function renderNav() {
const [navbar, toc] = await Promise.all([renderNavbar(), renderToc()])
renderBreadcrumb([...navbar, ...toc])
}
}
+119
View File
@@ -0,0 +1,119 @@
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
body[data-yaml-mime="ManagedReference"] article {
h1[data-uid] {
position: relative;
padding-right: 1.6rem;
}
h3[data-uid] {
position: relative;
font-weight: 400;
margin-top: 3rem;
padding-bottom: 5px;
padding-right: 1.6rem;
}
h2.section {
margin-top: 3rem;
+h3[data-uid], +a+h3[data-uid] {
margin-top: 1rem;
}
}
h4.section {
font-weight: 300;
margin-top: 1.6rem;
}
dl>dt {
font-weight: normal;
}
dl>dd {
margin-left: 1rem;
}
dl.typelist {
>dt {
font-weight: 600;
}
>dd {
margin-left: 0;
}
>dd>div {
display: inline-block;
&:not(:last-child)::after {
content: ', ';
}
}
&.inheritance>dd>div:not(:last-child)::after {
font-family: bootstrap-icons;
content: '\F12C';
position: relative;
top: .2em;
opacity: .8;
}
}
dl.parameters {
>dt>code {
margin-right: .2em;
}
}
div.facts {
font-size: 14px;
margin: 2rem 0 1rem;
>dl {
margin: 0;
>dd {
margin-left: .25rem;
display: inline-block;
}
>dt {
display: inline-block;
}
>dt::after {
content: ":";
}
}
}
.header-action {
position: absolute;
right: 0;
bottom: .2rem;
font-size: 1.2rem;
}
td.term {
font-weight: 600;
}
summary {
display: block;
cursor: inherit;
}
li>span.term {
font-weight: 600;
&::after {
content: '-';
margin: 0 .5em;
}
}
}
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { breakWord } from './helper'
test('break-text', () => {
expect(breakWord('Other APIs')).toEqual(['Other APIs'])
expect(breakWord('System.CodeDom')).toEqual(['System.', 'Code', 'Dom'])
expect(breakWord('System.Collections.Dictionary<string, object>')).toEqual(['System.', 'Collections.', 'Dictionary<', 'string,', ' object>'])
expect(breakWord('https://github.com/dotnet/docfx')).toEqual(['https://github.', 'com/', 'dotnet/', 'docfx'])
})
+56
View File
@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { html, TemplateResult } from 'lit-html'
/**
* Get the value of an HTML meta tag.
*/
export function meta(name: string): string {
return (document.querySelector(`meta[name="${name}"]`) as HTMLMetaElement)?.content
}
/**
* Add <wbr> into long word.
*/
export function breakWord(text: string): string[] {
const regex = /([a-z0-9])([A-Z]+[a-z])|([a-zA-Z0-9][.,/<>_])/g
const result = []
let start = 0
while (true) {
const match = regex.exec(text)
if (!match) {
break
}
const index = match.index + (match[1] || match[3]).length
result.push(text.slice(start, index))
start = index
}
if (start < text.length) {
result.push(text.slice(start))
}
return result
}
/**
* Add <wbr> into long word.
*/
export function breakWordLit(text: string): TemplateResult {
const result = []
breakWord(text).forEach(word => {
if (result.length > 0) {
result.push(html`<wbr>`)
}
result.push(html`${word}`)
})
return html`${result}`
}
/**
* Check if the url is external.
* @param url The url to check.
* @returns True if the url is external.
*/
export function isExternalHref(url: URL): boolean {
return url.hostname !== window.location.hostname || url.protocol !== window.location.protocol
}
@@ -0,0 +1,26 @@
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
@import "highlight.js/scss/vs";
@include color-mode(dark) {
/* stylelint-disable-next-line no-invalid-position-at-import-rule */
@import "highlight.js/scss/vs2015";
}
.hljs {
background-color: #f5f5f5;
}
/* For code snippet line highlight */
pre > code .line-highlight {
background-color: yellow;
}
@include color-mode(dark) {
pre > code .line-highlight {
background-color: #4a4a00;
}
}
+59
View File
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
export async function highlight() {
const codeBlocks = document.querySelectorAll('pre code')
if (codeBlocks.length <= 0) {
return
}
const { default: hljs } = await import('highlight.js')
window.docfx.configureHljs?.(hljs)
document.querySelectorAll('pre code').forEach(block => {
hljs.highlightElement(block as HTMLElement)
})
document.querySelectorAll('pre code[highlight-lines]').forEach(block => {
if (block.innerHTML === '') {
return
}
const queryString = block.getAttribute('highlight-lines')
if (!queryString) {
return
}
const lines = block.innerHTML.split('\n')
const ranges = queryString.split(',')
for (const range of ranges) {
let start = 0
let end = 0
const found = range.match(/^(\d+)-(\d+)?$/)
if (found) {
// consider region as `{startlinenumber}-{endlinenumber}`, in which {endlinenumber} is optional
start = +found[1]
end = +found[2]
if (isNaN(end) || end > lines.length) {
end = lines.length
}
} else {
// consider region as a sigine line number
if (isNaN(Number(range))) {
continue
}
start = +range
end = start
}
if (start <= 0 || end <= 0 || start > end || start > lines.length) {
// skip current region if invalid
continue
}
lines[start - 1] = '<span class="line-highlight">' + lines[start - 1]
lines[end - 1] = lines[end - 1] + '</span>'
}
block.innerHTML = lines.join('\n')
})
}
+186
View File
@@ -0,0 +1,186 @@
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
$header-height: 80px;
$footer-height: 120px;
$main-padding-top: 1.6rem;
$main-padding-bottom: 4rem;
// Makes a div sticky to top
@mixin sticky-top {
@include media-breakpoint-up(md) {
position: sticky;
top: 0;
z-index: 1030;
}
}
@mixin stick-to-header {
@include media-breakpoint-up(md) {
position: sticky;
top: calc($header-height + $main-padding-top);
}
}
html {
width: calc(100vw - var(--scrollbar-width));
min-height: 100vh;
overflow-x: hidden;
}
body,
body[data-layout="landing"] {
width: calc(100vw - var(--scrollbar-width));
min-height: 100vh;
display: flex;
flex-direction: column;
>header {
display: flex;
align-items: stretch;
@include sticky-top;
@include media-breakpoint-up(md) {
height: $header-height;
}
>nav {
flex: 1;
}
}
>footer {
padding: 2rem 1rem;
height: $footer-height;
>div {
display: flex;
align-items: center;
}
}
>main {
display: flex;
flex: 1;
padding-top: $main-padding-top;
padding-bottom: $main-padding-bottom;
>.content {
>:not(article) {
display: none;
}
@include media-breakpoint-up(md) {
>article [id] {
scroll-margin-top: $header-height;
}
}
}
>:not(.content) {
display: none;
}
}
@media print {
>header, >footer {
display: none;
}
}
}
@media not print {
// Search layout
body[data-search] {
>main {
display: none;
}
>.search-results {
display: block;
flex: 1;
padding-top: $main-padding-top;
padding-bottom: $main-padding-bottom;
}
}
body:not([data-search]) {
>.search-results {
display: none;
}
// Default layout: with header, footer, actionbar, affix, and toc
&[data-layout=""],
&[data-layout="conceptual"] {
>main {
padding-bottom: 0;
>.toc-offcanvas {
flex: .35;
display: block;
overflow-x: hidden;
overflow-y: auto;
max-width: 360px;
max-height: calc(100vh - $header-height - $main-padding-top);
@include stick-to-header;
@include media-breakpoint-down(md) {
flex: 0;
}
}
>.content {
flex: 1;
min-width: 0;
margin: 0 3rem;
padding-bottom: $main-padding-bottom;
>.actionbar {
display: flex;
align-items: flex-start;
margin-top: .5rem;
min-height: 40px;
}
>.contribution,
>.next-article {
display: flex;
}
@include media-breakpoint-down(lg) {
margin: 0 1rem;
}
@include media-breakpoint-down(md) {
margin: 0;
}
}
>.affix {
display: block;
width: 230px;
max-height: calc(100vh - #{$header-height});
overflow-x: hidden;
overflow-y: auto;
@include stick-to-header;
@media only screen and (width <= 1140px) {
display: none;
}
}
}
}
// Chromeless layout: with no header, footer, actionbar, affix, and toc
&[data-layout="chromeless"] {
>header, >footer {
display: none;
}
}
}
}
@@ -0,0 +1,86 @@
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
/* External link icon */
a.external[href]::after {
font-family: bootstrap-icons;
content: "\F1C5";
font-size: .6rem;
margin: 0 .2em;
display: inline-block;
}
/* Alerts */
.alert h5 {
text-transform: uppercase;
font-weight: bold;
font-size: 1rem;
&::before {
@include adjust-icon;
}
}
.alert-info h5::before {
content: "\F431";
}
.alert-warning h5::before {
content: "\F333";
}
.alert-danger h5::before {
content: "\F623";
}
/* For Embedded Video */
div.embeddedvideo {
padding-top: 56.25%;
position: relative;
width: 100%;
margin-bottom: 1em;
}
div.embeddedvideo iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
/* For code actions */
pre {
position: relative;
>.code-action {
display: none;
position: absolute;
top: .25rem;
right: .2rem;
.bi-check-lg {
font-size: 1.2rem;
}
}
&:hover {
>.code-action {
display: block;
}
}
}
/* For tabbed content */
.tabGroup {
margin-bottom: 1rem;
>section {
margin: 0;
padding: 1rem;
border-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
+454
View File
@@ -0,0 +1,454 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { breakWord, meta } from './helper'
import AnchorJs from 'anchor-js'
import { html, render } from 'lit-html'
import { getTheme } from './theme'
/**
* Initialize markdown rendering.
*/
export async function renderMarkdown() {
renderWordBreaks()
renderTables()
renderAlerts()
renderLinks()
renderTabs()
renderAnchor()
renderCodeCopy()
renderClickableImage()
await Promise.all([
renderMath(),
renderMermaid()
])
}
async function renderMath() {
const math = document.querySelectorAll('.math')
if (math.length > 0) {
await import('mathjax/es5/tex-svg-full.js')
}
}
let mermaidRenderCount = 0
/**
* Render mermaid diagrams.
*/
async function renderMermaid() {
const diagrams = document.querySelectorAll<HTMLElement>('pre code.lang-mermaid')
if (diagrams.length <= 0) {
return
}
const { default: mermaid } = await import('mermaid')
const theme = getTheme() === 'dark' ? 'dark' : 'default'
// Turn off deterministic ids on re-render
const deterministicIds = mermaidRenderCount === 0
mermaid.initialize(Object.assign({ startOnLoad: false, deterministicIds, theme }, window.docfx.mermaid))
mermaidRenderCount++
const nodes = []
diagrams.forEach(e => {
// Rerender when elements becomes visible due to https://github.com/mermaid-js/mermaid/issues/1846
if (e.offsetParent) {
nodes.push(e.parentElement)
e.parentElement.classList.add('mermaid')
e.parentElement.innerHTML = e.innerHTML
}
})
await mermaid.run({ nodes })
}
/**
* Add <wbr> to break long text.
*/
function renderWordBreaks() {
document.querySelectorAll<HTMLElement>('article h1,h2,h3,h4,h5,h6,.xref,.text-break').forEach(e => {
if (e.innerHTML?.trim() === e.innerText?.trim()) {
const children: (string | Node)[] = []
for (const text of breakWord(e.innerText)) {
if (children.length > 0) {
children.push(document.createElement('wbr'))
}
children.push(text)
}
e.replaceChildren(...children)
}
})
}
/**
* Make images in articles clickable by wrapping the image in an anchor tag.
* The image is clickable only if its size is larger than 200x200 and it is not already been wrapped in an anchor tag.
*/
function renderClickableImage() {
const MIN_CLICKABLE_IMAGE_SIZE = 200
const imageLinks = Array.from(document.querySelectorAll<HTMLImageElement>('article a img[src]'))
document.querySelectorAll<HTMLImageElement>('article img[src]').forEach(img => {
if (shouldMakeClickable()) {
makeClickable()
} else {
img.addEventListener('load', () => {
if (shouldMakeClickable()) {
makeClickable()
}
})
}
function makeClickable() {
const a = document.createElement('a')
a.target = '_blank'
a.rel = 'noopener noreferrer nofollow'
a.href = img.src
img.replaceWith(a)
a.appendChild(img)
}
function shouldMakeClickable(): boolean {
return img.naturalWidth > MIN_CLICKABLE_IMAGE_SIZE &&
img.naturalHeight > MIN_CLICKABLE_IMAGE_SIZE &&
!imageLinks.includes(img)
}
})
}
/**
* Styling for tables in conceptual documents using Bootstrap.
* See http://getbootstrap.com/css/#tables
*/
function renderTables() {
document.querySelectorAll('table').forEach(table => {
table.classList.add('table', 'table-bordered', 'table-condensed')
const wrapper = document.createElement('div')
wrapper.className = 'table-responsive'
table.parentElement.insertBefore(wrapper, table)
wrapper.appendChild(table)
})
}
/**
* Styling for alerts.
*/
function renderAlerts() {
document.querySelectorAll('.NOTE, .TIP').forEach(e => e.classList.add('alert', 'alert-info'))
document.querySelectorAll('.WARNING').forEach(e => e.classList.add('alert', 'alert-warning'))
document.querySelectorAll('.IMPORTANT, .CAUTION').forEach(e => e.classList.add('alert', 'alert-danger'))
}
/**
* Open external links to different host in a new window.
*/
function renderLinks() {
if (meta('docfx:disablenewtab') === 'true') {
return
}
document.querySelectorAll<HTMLAnchorElement>('article a[href]').forEach(a => {
if (a.hostname !== window.location.hostname && a.innerText.trim() !== '') {
a.target = '_blank'
a.rel = 'noopener noreferrer nofollow'
a.classList.add('external')
}
})
}
/**
* Render anchor # for headings
*/
function renderAnchor() {
const anchors = new AnchorJs()
anchors.options = Object.assign({
visible: 'hover',
icon: '#'
}, window.docfx.anchors)
anchors.add('article h2:not(.no-anchor), article h3:not(.no-anchor), article h4:not(.no-anchor)')
}
/**
* Render code copy button.
*/
function renderCodeCopy() {
document.querySelectorAll<HTMLElement>('pre>code').forEach(code => {
if (code.innerText.trim().length === 0) {
return
}
let copied = false
renderCore()
function renderCore() {
const dom = copied
? html`<a class='btn border-0 link-success code-action'><i class='bi bi-check-lg'></i></a>`
: html`<a class='btn border-0 code-action' title='copy' href='#' @click=${copy}><i class='bi bi-clipboard'></i></a>`
render(dom, code.parentElement)
async function copy(e) {
e.preventDefault()
await navigator.clipboard.writeText(code.innerText)
copied = true
renderCore()
setTimeout(() => {
copied = false
renderCore()
}, 1000)
}
}
})
}
/**
* Render tabbed content.
*/
function renderTabs() {
updateTabStyle()
const contentAttrs = {
id: 'data-bi-id',
name: 'data-bi-name',
type: 'data-bi-type'
}
const Tab = (function() {
function Tab(li, a, section) {
this.li = li
this.a = a
this.section = section
}
Object.defineProperty(Tab.prototype, 'tabIds', {
get: function() { return this.a.getAttribute('data-tab').split(' ') },
enumerable: true,
configurable: true
})
Object.defineProperty(Tab.prototype, 'condition', {
get: function() { return this.a.getAttribute('data-condition') },
enumerable: true,
configurable: true
})
Object.defineProperty(Tab.prototype, 'visible', {
get: function() { return !this.li.hasAttribute('hidden') },
set: function(value) {
if (value) {
this.li.removeAttribute('hidden')
this.li.removeAttribute('aria-hidden')
} else {
this.li.setAttribute('hidden', 'hidden')
this.li.setAttribute('aria-hidden', 'true')
}
},
enumerable: true,
configurable: true
})
Object.defineProperty(Tab.prototype, 'selected', {
get: function() { return !this.section.hasAttribute('hidden') },
set: function(value) {
if (value) {
this.a.setAttribute('aria-selected', 'true')
this.a.classList.add('active')
this.a.tabIndex = 0
this.section.removeAttribute('hidden')
this.section.removeAttribute('aria-hidden')
} else {
this.a.setAttribute('aria-selected', 'false')
this.a.classList.remove('active')
this.a.tabIndex = -1
this.section.setAttribute('hidden', 'hidden')
this.section.setAttribute('aria-hidden', 'true')
}
},
enumerable: true,
configurable: true
})
Tab.prototype.focus = function() {
this.a.focus()
}
return Tab
}())
initTabs(document.body)
function initTabs(container) {
const queryStringTabs = readTabsQueryStringParam()
const elements = container.querySelectorAll('.tabGroup')
const state = { groups: [], selectedTabs: [] }
for (let i = 0; i < elements.length; i++) {
const group = initTabGroup(elements.item(i))
if (!group.independent) {
updateVisibilityAndSelection(group, state)
state.groups.push(group)
}
}
container.addEventListener('click', function(event) { return handleClick(event, state) })
if (state.groups.length === 0) {
return state
}
selectTabs(queryStringTabs)
updateTabsQueryStringParam(state)
return state
}
function initTabGroup(element) {
const group = {
independent: element.hasAttribute('data-tab-group-independent'),
tabs: []
}
let li = element.firstElementChild.firstElementChild
while (li) {
const a = li.firstElementChild
a.setAttribute(contentAttrs.name, 'tab')
const dataTab = a.getAttribute('data-tab').replace(/\+/g, ' ')
a.setAttribute('data-tab', dataTab)
const section = element.querySelector('[id="' + a.getAttribute('aria-controls') + '"]')
const tab = new Tab(li, a, section)
group.tabs.push(tab)
li = li.nextElementSibling
}
element.setAttribute(contentAttrs.name, 'tab-group')
element.tabGroup = group
return group
}
function updateVisibilityAndSelection(group, state) {
let anySelected = false
let firstVisibleTab
for (let _i = 0, _a = group.tabs; _i < _a.length; _i++) {
const tab = _a[_i]
tab.visible = tab.condition === null || state.selectedTabs.indexOf(tab.condition) !== -1
if (tab.visible) {
if (!firstVisibleTab) {
firstVisibleTab = tab
}
}
tab.selected = tab.visible && arraysIntersect(state.selectedTabs, tab.tabIds)
anySelected = anySelected || tab.selected
}
if (!anySelected) {
for (let _b = 0, _c = group.tabs; _b < _c.length; _b++) {
const tabIds = _c[_b].tabIds
for (let _d = 0, tabIds1 = tabIds; _d < tabIds1.length; _d++) {
const tabId = tabIds1[_d]
const index = state.selectedTabs.indexOf(tabId)
if (index === -1) {
continue
}
state.selectedTabs.splice(index, 1)
}
}
const tab = firstVisibleTab
tab.selected = true
state.selectedTabs.push(tab.tabIds[0])
}
}
function getTabInfoFromEvent(event) {
if (!(event.target instanceof HTMLElement)) {
return null
}
const anchor = event.target.closest('a[data-tab]')
if (anchor === null) {
return null
}
const tabIds = anchor.getAttribute('data-tab').split(' ')
const group = anchor.parentElement.parentElement.parentElement.tabGroup
if (group === undefined) {
return null
}
return { tabIds, group, anchor }
}
function handleClick(event, state) {
const info = getTabInfoFromEvent(event)
if (info === null) {
return
}
event.preventDefault()
info.anchor.href = 'javascript:'
setTimeout(function() {
info.anchor.href = '#' + info.anchor.getAttribute('aria-controls')
})
const tabIds = info.tabIds; const group = info.group
const originalTop = info.anchor.getBoundingClientRect().top
if (group.independent) {
for (let _i = 0, _a = group.tabs; _i < _a.length; _i++) {
const tab = _a[_i]
tab.selected = arraysIntersect(tab.tabIds, tabIds)
}
} else {
if (arraysIntersect(state.selectedTabs, tabIds)) {
return
}
const previousTabId = group.tabs.filter(function(t) { return t.selected })[0].tabIds[0]
state.selectedTabs.splice(state.selectedTabs.indexOf(previousTabId), 1, tabIds[0])
for (let _b = 0, _c = state.groups; _b < _c.length; _b++) {
const group1 = _c[_b]
updateVisibilityAndSelection(group1, state)
}
updateTabsQueryStringParam(state)
}
notifyContentUpdated()
const top = info.anchor.getBoundingClientRect().top
if (top !== originalTop && event instanceof MouseEvent) {
window.scrollTo(0, window.pageYOffset + top - originalTop)
}
}
function selectTabs(tabIds) {
for (let _i = 0, tabIds1 = tabIds; _i < tabIds1.length; _i++) {
const tabId = tabIds1[_i]
const a = document.querySelector('.tabGroup > ul > li > a[data-tab="' + tabId + '"]:not([hidden])')
if (a === null) {
return
}
a.dispatchEvent(new CustomEvent('click', { bubbles: true }))
}
}
function readTabsQueryStringParam() {
const qs = new URLSearchParams(window.location.search)
const t = qs.get('tabs')
if (!t) {
return []
}
return t.split(',')
}
function updateTabsQueryStringParam(state) {
const qs = new URLSearchParams(window.location.search)
qs.set('tabs', state.selectedTabs.join())
const url = location.protocol + '//' + location.host + location.pathname + '?' + qs.toString() + location.hash
if (location.href === url) {
return
}
history.replaceState({}, document.title, url)
}
function arraysIntersect(a, b) {
for (let _i = 0, a1 = a; _i < a1.length; _i++) {
const itemA = a1[_i]
for (let _a = 0, b1 = b; _a < b1.length; _a++) {
const itemB = b1[_a]
if (itemA === itemB) {
return true
}
}
}
return false
}
function updateTabStyle() {
document.querySelectorAll('div.tabGroup>ul').forEach(e => e.classList.add('nav', 'nav-tabs'))
document.querySelectorAll('div.tabGroup>ul>li').forEach(e => e.classList.add('nav-item'))
document.querySelectorAll('div.tabGroup>ul>li>a').forEach(e => e.classList.add('nav-link'))
document.querySelectorAll('div.tabGroup>section').forEach(e => e.classList.add('card'))
}
function notifyContentUpdated() {
renderMermaid()
}
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
@mixin adjust-icon {
font-family: bootstrap-icons;
position: relative;
margin-right: 0.5em;
top: 0.2em;
font-size: 1.25em;
font-weight: normal;
}
@mixin underline-on-hover {
text-decoration: none;
&:hover, &:focus {
text-decoration: underline;
}
}
+159
View File
@@ -0,0 +1,159 @@
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
.breadcrumb {
font-size: 14px;
a {
@include underline-on-hover;
}
}
.next-article {
display: flex;
&:not(:has(div)) {
border-top-width: 0;
}
&:has(div) {
margin-top: 3rem;
padding-top: 1rem;
}
&>div {
flex: 1;
&.next {
text-align: right;
}
&>span {
opacity: .66;
font-size: 14px;
}
&>a {
display: block;
}
}
}
.navbar {
padding: 2rem 1rem;
.navbar-brand {
display: flex;
align-items: center;
}
.navbar-nav {
display: flex;
flex-wrap: nowrap;
}
#navbar {
display: flex;
flex: 1;
justify-content: flex-end;
form {
display: flex;
position: relative;
align-items: center;
>i.bi {
position: absolute;
left: .8rem;
opacity: .5;
}
>input {
padding-left: 2.5rem;
}
&.search {
order: 50;
}
&.icons {
margin-left: auto;
}
}
}
@include media-breakpoint-down(md) {
#navbar {
flex-direction: column;
align-items: flex-start;
form {
margin: 1rem 0 0;
&.search {
align-self: stretch;
order: 30;
}
&.icons {
align-self: center;
order: 40;
margin: 1rem 0;
}
}
}
}
}
.affix {
font-size: 14px;
h5 {
display: inline-block;
font-weight: 300;
text-transform: uppercase;
padding: 1em 0 .5em;
font-size: 14px;
letter-spacing: 2px;
}
h6 {
font-size: 14px;
}
ul {
flex-direction: column;
list-style-type: none;
padding-left: 0;
margin-left: 0;
h6 {
margin-top: 1rem;
}
li {
margin: .4rem 0;
a {
@include underline-on-hover;
}
}
}
}
.contribution {
margin-top: 2rem;
a.edit-link {
@include underline-on-hover;
&::before {
content: "\F4CA";
display: inline-block;
@include adjust-icon;
}
}
}
+161
View File
@@ -0,0 +1,161 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { html, render, TemplateResult } from 'lit-html'
import { breakWordLit, isExternalHref, meta } from './helper'
import { themePicker } from './theme'
import { TocNode } from './toc'
export type NavItem = {
name: string
href: URL
}
export type NavItemContainer = {
name: string
items: NavItem[]
}
/**
* @returns active navbar items
*/
export async function renderNavbar(): Promise<NavItem[]> {
const navbar = document.getElementById('navbar')
if (!navbar) {
return
}
const navItems = await loadNavItems()
const activeItem = findActiveItem(navItems)
const menuItem = item => {
const current = (item === activeItem ? 'page' : false)
const active = (item === activeItem ? 'active' : null)
return html`<li class='nav-item'><a class='nav-link ${active}' aria-current=${current} href=${item.href}>${breakWordLit(item.name)}</a></li>`
}
const menu = html`
<ul class='navbar-nav'>${navItems.map(item => {
if ('items' in item) {
const active = item.items.some(i => i === activeItem) ? 'active' : null
return html`
<li class='nav-item dropdown'>
<a class='nav-link dropdown-toggle ${active}' href='#' role='button' data-bs-toggle='dropdown' aria-expanded='false'>
${breakWordLit(item.name)}
</a>
<ul class='dropdown-menu'>${item.items.map(menuItem)}</ul>
</li>`
} else {
return menuItem(item)
}
})
}</ul>`
function renderCore() {
const icons = html`
<form class="icons">
${window.docfx.iconLinks?.map(i => html`<a href="${i.href}" title="${i.title}" class="btn border-0"><i class="bi bi-${i.icon}"></i></a>`)}
<a href="https://github.com/lepoco/wpfui" target="_blank" rel="noopener noreferrer" title="WPF UI on GitHub" class="btn border-0"><i class="bi bi-github"></i></a>
${themePicker(renderCore)}
<a class="btn btn-border-0 btn-colorful mr-05" target="_blank" rel="noopener noreferrer" href="https://github.com/sponsors/pomianowski">Sponsor</a>
</form>`
render(html`${menu} ${icons}`, navbar)
}
renderCore()
return activeItem ? [activeItem] : []
async function loadNavItems(): Promise<(NavItem | NavItemContainer)[]> {
const navrel = meta('docfx:navrel')
if (!navrel) {
return []
}
const navUrl = new URL(navrel.replace(/.html$/gi, '.json'), window.location.href)
const { items } = await fetch(navUrl).then(res => res.json())
return items.map((a: NavItem | NavItemContainer) => {
if ('items' in a) {
return { name: a.name, items: a.items.map(i => ({ name: i.name, href: new URL(i.href, navUrl) })) }
}
return { name: a.name, href: new URL(a.href, navUrl) }
})
}
}
export function renderBreadcrumb(breadcrumb: (NavItem | TocNode)[]) {
const container = document.getElementById('breadcrumb')
if (container) {
render(
html`
<ol class="breadcrumb">
${breadcrumb.map(i => html`<li class="breadcrumb-item"><a href="${i.href}">${breakWordLit(i.name)}</a></li>`)}
</ol>`,
container)
}
}
export function renderInThisArticle() {
const affix = document.getElementById('affix')
const windowPathname = window.location.pathname
if (windowPathname === '' || windowPathname === '/' || windowPathname === '/index.html') {
return
}
if (affix) {
render(document.body.getAttribute('data-yaml-mime') === 'ManagedReference' ? inThisArticleForManagedReference() : inThisArticleForConceptual(), affix)
}
}
function inThisArticleForConceptual() {
const headings = document.querySelectorAll<HTMLHeadingElement>('article h2')
if (headings.length > 0) {
return html`
<h5 class="border-bottom">In this article</h5>
<ul>${Array.from(headings).map(h => html`<li><a class="link-secondary" href="#${h.id}">${breakWordLit(h.innerText)}</a></li>`)}</ul>`
}
}
function inThisArticleForManagedReference(): TemplateResult {
let headings = Array.from(document.querySelectorAll<HTMLHeadingElement>('article h2, article h3'))
headings = headings.filter((h, i) => h.tagName === 'H3' || headings[i + 1]?.tagName === 'H3')
if (headings.length > 0) {
return html`
<h5 class="border-bottom">In this article</h5>
<ul>${headings.map(h => {
return h.tagName === 'H2'
? html`<li><h6>${breakWordLit(h.innerText)}</h6></li>`
: html`<li><a class="link-secondary" href="#${h.id}">${breakWordLit(h.innerText)}</a></li>`
})}</ul>`
}
}
function findActiveItem(items: (NavItem | NavItemContainer)[]): NavItem {
const url = new URL(window.location.href)
let activeItem: NavItem
let maxPrefix = 0
for (const item of items.map(i => 'items' in i ? i.items : i).flat()) {
if (isExternalHref(item.href)) {
continue
}
const prefix = commonUrlPrefix(url, item.href)
if (prefix > maxPrefix) {
maxPrefix = prefix
activeItem = item
}
}
return activeItem
}
function commonUrlPrefix(url: URL, base: URL): number {
const urlSegments = url.pathname.split('/')
const baseSegments = base.pathname.split('/')
let i = 0
while (i < urlSegments.length && i < baseSegments.length && urlSegments[i] === baseSegments[i]) {
i++
}
return i
}
+40
View File
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import BootstrapIcons from 'bootstrap-icons/font/bootstrap-icons.json'
import { HLJSApi } from 'highlight.js'
import { AnchorJSOptions } from 'anchor-js'
import { MermaidConfig } from 'mermaid'
export type Theme = 'light' | 'dark' | 'auto'
export type IconLink = {
/** A [bootstrap-icons](https://icons.getbootstrap.com/) name */
icon: keyof typeof BootstrapIcons,
/** The URL of this icon link */
href: string,
/** The title of this icon link shown on mouse hover */
title?: string
}
/**
* Enables customization of the website through the global `window.docfx` object.
*/
export type DocfxOptions = {
/** Configures the default theme */
defaultTheme?: Theme,
/** A list of icons to show in the header next to the theme picker */
iconLinks?: IconLink[],
/** Configures [anchor-js](https://www.bryanbraun.com/anchorjs#options) options */
anchors?: AnchorJSOptions,
/** Configures mermaid diagram options */
mermaid?: MermaidConfig,
/** Configures [hightlight.js](https://highlightjs.org/) */
configureHljs?: (hljs: HLJSApi) => void,
}
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import lunr from 'lunr'
let lunrIndex
let stopWords = null
let searchData = {}
lunr.tokenizer.separator = /[\s\-.()]+/
const stopWordsRequest = new XMLHttpRequest()
stopWordsRequest.open('GET', '../search-stopwords.json')
stopWordsRequest.onload = function() {
if (this.status !== 200) {
return
}
stopWords = JSON.parse(this.responseText)
buildIndex()
}
stopWordsRequest.send()
const searchDataRequest = new XMLHttpRequest()
searchDataRequest.open('GET', '../index.json')
searchDataRequest.onload = function() {
if (this.status !== 200) {
return
}
searchData = JSON.parse(this.responseText)
buildIndex()
postMessage({ e: 'index-ready' })
}
searchDataRequest.send()
onmessage = function(oEvent) {
const q = oEvent.data.q
const results = []
if (lunrIndex) {
const hits = lunrIndex.search(q)
hits.forEach(function(hit) {
const item = searchData[hit.ref]
results.push({ href: item.href, title: item.title, keywords: item.keywords })
})
}
postMessage({ e: 'query-ready', q, d: results })
}
function buildIndex() {
if (stopWords !== null && !isEmpty(searchData)) {
lunrIndex = lunr(function() {
this.pipeline.remove(lunr.stopWordFilter)
this.ref('href')
this.field('title', { boost: 50 })
this.field('keywords', { boost: 20 })
for (const prop in searchData) {
if (Object.prototype.hasOwnProperty.call(searchData, prop)) {
this.add(searchData[prop])
}
}
const docfxStopWordFilter = lunr.generateStopWordFilter(stopWords)
lunr.Pipeline.registerFunction(docfxStopWordFilter, 'docfxStopWordFilter')
this.pipeline.add(docfxStopWordFilter)
this.searchPipeline.add(docfxStopWordFilter)
})
}
}
function isEmpty(obj) {
if (!obj) return true
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) { return false }
}
return true
}
+34
View File
@@ -0,0 +1,34 @@
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
#search-results {
line-height: 1.8;
>.search-list {
font-size: .9em;
color: $secondary;
}
>.sr-items {
flex: 1;
.sr-item {
margin-bottom: 1.5em;
>.item-title {
font-size: x-large;
}
>.item-href {
color: #093;
font-size: small;
}
>.item-brief {
font-size: small;
}
}
}
}
+171
View File
@@ -0,0 +1,171 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { meta } from './helper'
import { html, render, TemplateResult } from 'lit-html'
import { classMap } from 'lit-html/directives/class-map.js'
type SearchHit = {
href: string
title: string
keywords: string
}
let query
/**
* Support full-text-search
*/
export function enableSearch() {
const searchQuery = document.getElementById('search-query') as HTMLInputElement
if (!searchQuery || !window.Worker) {
return
}
const relHref = meta('docfx:rel') || ''
const worker = new Worker(relHref + 'public/search-worker.min.js', { type: 'module' })
worker.onmessage = function(oEvent) {
switch (oEvent.data.e) {
case 'index-ready':
searchQuery.disabled = false
searchQuery.addEventListener('input', onSearchQueryInput)
window.docfx.searchReady = true
break
case 'query-ready':
document.body.setAttribute('data-search', 'true')
renderSearchResults(oEvent.data.d, 0)
window.docfx.searchResultReady = true
break
}
}
function onSearchQueryInput() {
query = searchQuery.value
if (query.length < 3) {
document.body.removeAttribute('data-search')
} else {
worker.postMessage({ q: query })
}
}
function relativeUrlToAbsoluteUrl(currentUrl, relativeUrl) {
const currentItems = currentUrl.split(/\/+/)
const relativeItems = relativeUrl.split(/\/+/)
let depth = currentItems.length - 1
const items = []
for (let i = 0; i < relativeItems.length; i++) {
if (relativeItems[i] === '..') {
depth--
} else if (relativeItems[i] !== '.') {
items.push(relativeItems[i])
}
}
return currentItems.slice(0, depth).concat(items).join('/')
}
function extractContentBrief(content) {
const briefOffset = 512
const words = query.split(/\s+/g)
const queryIndex = content.indexOf(words[0])
if (queryIndex > briefOffset) {
return '...' + content.slice(queryIndex - briefOffset, queryIndex + briefOffset) + '...'
} else if (queryIndex <= briefOffset) {
return content.slice(0, queryIndex + briefOffset) + '...'
}
}
function renderSearchResults(hits: SearchHit[], page: number) {
const numPerPage = 10
const totalPages = Math.ceil(hits.length / numPerPage)
render(
renderPage(page),
document.getElementById('search-results'))
function renderPage(page: number): TemplateResult {
if (hits.length === 0) {
return html`<div class="search-list">No results for "${query}"</div>`
}
const start = page * numPerPage
const curHits = hits.slice(start, start + numPerPage)
const items = html`
<div class="search-list">${hits.length} results for "${query}"</div>
<div class="sr-items">${curHits.map(hit => {
const currentUrl = window.location.href
const itemRawHref = relativeUrlToAbsoluteUrl(currentUrl, relHref + hit.href)
const itemHref = relHref + hit.href + '?q=' + query
const itemBrief = extractContentBrief(hit.keywords)
return html`
<div class="sr-item">
<div class="item-title"><a href="${itemHref}" target="_blank" rel="noopener noreferrer">${mark(hit.title, query)}</a></div>
<div class="item-href">${mark(itemRawHref, query)}</div>
<div class="item-brief">${mark(itemBrief, query)}</div>
</div>`
})}
</div>`
return html`${items} ${renderPagination()}`
}
function renderPagination() {
const maxVisiblePages = 5
const startPage = Math.max(0, Math.min(page - 2, totalPages - maxVisiblePages))
const endPage = Math.min(totalPages, startPage + maxVisiblePages)
const pages = Array.from(new Array(endPage - startPage).keys()).map(i => i + startPage)
if (pages.length <= 1) {
return null
}
return html`
<nav>
<ul class="pagination">
<li class="page-item">
<a class="page-link ${classMap({ disabled: page <= 0 })}" href="#" aria-label="Previous"
@click="${() => gotoPage(page - 1)}">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
${pages.map(i => html`
<li class="page-item">
<a class="page-link ${classMap({ active: page === i })}" href="#"
@click="${() => gotoPage(i)}">${i + 1}</a></li>`)}
<li class="page-item">
<a class="page-link ${classMap({ disabled: page >= totalPages - 1 })}" href="#" aria-label="Next"
@click="${() => gotoPage(page + 1)}">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>`
function gotoPage(page: number) {
if (page >= 0 && page < totalPages) {
renderSearchResults(hits, page)
}
}
}
}
}
function mark(text: string, query: string): TemplateResult {
const words = query.split(/\s+/g)
const wordsLower = words.map(w => w.toLowerCase())
const textLower = text.toLowerCase()
const result = []
let lastEnd = 0
for (let i = 0; i < wordsLower.length; i++) {
const word = wordsLower[i]
const index = textLower.indexOf(word, lastEnd)
if (index >= 0) {
result.push(html`${text.slice(lastEnd, index)}`)
result.push(html`<b>${text.slice(index, index + word.length)}</b>`)
lastEnd = index + word.length
}
}
result.push(html`${text.slice(lastEnd)}`)
return html`${result}`
}
+49
View File
@@ -0,0 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { html } from 'lit-html'
import { Theme } from './options'
function setTheme(theme: Theme) {
localStorage.setItem('theme', theme)
if (theme === 'auto') {
document.documentElement.setAttribute('data-bs-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
function getDefaultTheme() {
return localStorage.getItem('theme') as Theme || window.docfx.defaultTheme || 'auto'
}
export function initTheme() {
setTheme(getDefaultTheme())
}
export function getTheme(): 'light' | 'dark' {
return document.documentElement.getAttribute('data-bs-theme') as 'light' | 'dark'
}
export function themePicker(refresh: () => void) {
const theme = getDefaultTheme()
const icon = theme === 'light' ? 'sun' : theme === 'dark' ? 'moon' : 'circle-half'
return html`
<div class='dropdown'>
<a title='Change theme' class='btn border-0 dropdown-toggle mr-05' data-bs-toggle='dropdown' aria-expanded='false'>
<i class='bi bi-${icon}'></i>
</a>
<ul class='dropdown-menu'>
<li><a class='dropdown-item' href='#' @click=${e => changeTheme(e, 'light')}><i class='bi bi-sun'></i> Light</a></li>
<li><a class='dropdown-item' href='#' @click=${e => changeTheme(e, 'dark')}><i class='bi bi-moon'></i> Dark</a></li>
<li><a class='dropdown-item' href='#' @click=${e => changeTheme(e, 'auto')}><i class='bi bi-circle-half'></i> Auto</a></li>
</ul>
</div>`
function changeTheme(e, theme: Theme) {
e.preventDefault()
setTheme(theme)
refresh()
}
}
+92
View File
@@ -0,0 +1,92 @@
/**
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*/
$expand-stub-width: .85rem;
.toc {
min-width: 0;
width: 100%;
ul {
font-size: 14px;
flex-direction: column;
list-style-type: none;
padding-left: 0;
overflow-wrap: break-word;
}
li {
font-weight: normal;
margin: .6em 0;
padding-left: $expand-stub-width;
position: relative;
}
li > a {
display: inline;
@include underline-on-hover;
}
li > ul {
display: none;
}
li.expanded > ul {
display: block;
}
.expand-stub::before {
display: inline-block;
width: $expand-stub-width;
cursor: pointer;
font-family: bootstrap-icons;
font-size: .8em;
content: "\F285";
position: absolute;
margin-top: .2em;
margin-left: -$expand-stub-width;
transition: transform 0.35s ease;
transform-origin: .5em 50%;
@media (prefers-reduced-motion) {
& {
transition: none;
}
}
}
li.expanded > .expand-stub::before {
transform: rotate(90deg);
}
span.name-only {
font-weight: 600;
display: inline-block;
margin: .4rem 0;
}
form.filter {
display: flex;
position: relative;
align-items: center;
margin-bottom: 1rem;
>i.bi {
position: absolute;
left: .6rem;
opacity: .5;
}
>input {
padding-left: 2rem;
}
}
>.no-result {
font-size: .9em;
color: $secondary;
}
}
+177
View File
@@ -0,0 +1,177 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { TemplateResult, html, render } from 'lit-html'
import { classMap } from 'lit-html/directives/class-map.js'
import { breakWordLit, meta, isExternalHref } from './helper'
export type TocNode = {
name: string
href?: string
expanded?: boolean
items?: TocNode[]
}
/**
* @returns active TOC nodes
*/
export async function renderToc(): Promise<TocNode[]> {
const tocrel = meta('docfx:tocrel')
if (!tocrel) {
return []
}
const disableTocFilter = meta('docfx:disabletocfilter') === 'true'
let tocFilter = disableTocFilter ? '' : (localStorage?.getItem('tocFilter') || '')
const tocUrl = new URL(tocrel.replace(/.html$/gi, '.json'), window.location.href)
const { items } = await (await fetch(tocUrl)).json()
const activeNodes = []
const selectedNodes = []
items.forEach(initTocNodes)
const tocContainer = document.getElementById('toc')
if (tocContainer) {
renderToc()
const activeElements = tocContainer.querySelectorAll('li.active')
const lastActiveElement = activeElements[activeElements.length - 1]
if (lastActiveElement) {
lastActiveElement.scrollIntoView({ block: 'nearest' })
}
}
if (selectedNodes.length > 0) {
renderNextArticle(items, selectedNodes[0])
}
return activeNodes.slice(0, -1)
function initTocNodes(node: TocNode): boolean {
let active
if (node.href) {
const url = new URL(node.href, tocUrl)
node.href = url.href
active = isExternalHref(url) ? false : normalizeUrlPath(url) === normalizeUrlPath(window.location)
if (active) {
if (node.items) {
node.expanded = true
}
selectedNodes.push(node)
}
}
if (node.items) {
for (const child of node.items) {
if (initTocNodes(child)) {
active = true
node.expanded = true
}
}
}
if (active) {
activeNodes.unshift(node)
return true
}
return false
}
function renderToc() {
render(html`${renderTocFilter()} ${renderTocNodes(items) || renderNoFilterResult()}`, tocContainer)
}
function renderTocNodes(nodes: TocNode[]): TemplateResult {
const result = nodes.map(node => {
const { href, name, items, expanded } = node
const isLeaf = !items || items.length <= 0
const children = isLeaf ? null : renderTocNodes(items)
if (tocFilter !== '' && !children && !name.toLowerCase().includes(tocFilter.toLowerCase())) {
return null
}
const dom = href
? html`<a class='${classMap({ 'nav-link': !activeNodes.includes(node) })}' href=${href}>${breakWordLit(name)}</a>`
: (isLeaf
? html`<span class='text-body-tertiary name-only'>${breakWordLit(name)}</a>`
: html`<a class='${classMap({ 'nav-link': !activeNodes.includes(node) })}' href='#' @click=${toggleExpand}>${breakWordLit(name)}</a>`)
const isExpanded = (tocFilter !== '' && expanded !== false && children != null) || expanded === true
return html`
<li class=${classMap({ expanded: isExpanded, active: activeNodes.includes(node) })}>
${isLeaf ? null : html`<span class='expand-stub' @click=${toggleExpand}></span>`}
${dom}
${children}
</li>`
function toggleExpand(e) {
e.preventDefault()
node.expanded = !isExpanded
renderToc()
}
}).filter(node => node)
return result.length > 0 ? html`<ul>${result}</ul>` : null
}
function renderTocFilter(): TemplateResult {
return disableTocFilter
? null
: html`
<form class='filter'>
<i class='bi bi-filter'></i>
<input class='form-control' @input=${filterToc} value='${tocFilter}' type='search' placeholder='Filter by title' autocomplete='off' aria-label='Filter by title'>
</form>`
function filterToc(e: Event) {
tocFilter = (<HTMLInputElement>e.target).value.trim()
localStorage?.setItem('tocFilter', tocFilter)
renderToc()
}
}
function renderNoFilterResult(): TemplateResult {
return tocFilter === '' ? null : html`<div class='no-result'>No results for "${tocFilter}"</div>`
}
function normalizeUrlPath(url: { pathname: string }): string {
return url.pathname.replace(/\/index\.html$/gi, '/')
}
}
function renderNextArticle(items: TocNode[], node: TocNode) {
const nextArticle = document.getElementById('nextArticle')
if (!nextArticle) {
return
}
const tocNodes = flattenTocNodesWithHref(items)
const i = tocNodes.findIndex(n => n === node)
const prev = tocNodes[i - 1]
const next = tocNodes[i + 1]
if (!prev && !next) {
return
}
const prevButton = prev ? html`<div class="prev"><span><i class='bi bi-chevron-left'></i> Previous</span> <a href="${prev.href}" rel="prev">${breakWordLit(prev.name)}</a></div>` : null
const nextButton = next ? html`<div class="next"><span>Next <i class='bi bi-chevron-right'></i></span> <a href="${next.href}" rel="next">${breakWordLit(next.name)}</a></div>` : null
render(html`${prevButton} ${nextButton}`, nextArticle)
function flattenTocNodesWithHref(items: TocNode[]) {
const result = []
for (const item of items) {
if (item.href) {
result.push(item)
}
if (item.items) {
result.push(...flattenTocNodesWithHref(item.items))
}
}
return result
}
}
@@ -0,0 +1,11 @@
function updateBaseStats() {
console.debug('Index stats initialized')
}
export function renderIndexStats() {
const windowPathname = window.location.pathname
if (windowPathname === '' || windowPathname === '/' || windowPathname === '/index.html') {
updateBaseStats()
}
}
+172
View File
@@ -0,0 +1,172 @@
.h1,
.h2,
.h3,
.h4,
.h5,
h1,
h2,
h3,
h4,
h5 {
font-family: Montserrat, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji,
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}
.navbar-brand {
font-family: Montserrat, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji,
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-weight: 700;
}
.typelist.derived {
display: none;
}
.mr-05 {
margin-right: 0.5rem;
}
.card-call-to-action {
border-radius: 1rem;
padding: 1rem;
background-color: rgb(15, 163, 180);
background-image: linear-gradient(
140deg,
rgb(0, 128, 154),
rgb(19, 104, 145) 50%,
rgb(32, 135, 135) 75%
);
transition: background-position 0.5s ease-in-out;
background-size: 200% 200%;
background-position: 0% 0%;
&:hover {
background-position: 100% 100%;
}
}
.btn-colorful {
background-color: rgb(15, 163, 180);
background-image: linear-gradient(
140deg,
rgb(0, 128, 154),
rgb(19, 104, 145) 50%,
rgb(32, 135, 135) 75%
);
transition: background-position 0.5s ease-in-out;
background-size: 200% 200%;
background-position: 0% 0%;
color: white;
&:hover {
background-position: 100% 100%;
}
}
img {
max-width: 100%;
}
h2 {
margin-top: 4rem;
font-weight: 700;
}
h3 {
margin-top: 2rem;
}
.navbar-brand {
#logo {
max-width: 30px;
margin-right: 0.7rem;
}
}
.spaced-page {
padding: 6rem 0;
}
.display-1,
.display-4 {
font-weight: 700;
}
.spaced-page-separator {
padding: 1rem;
}
.colorful {
background-color: #1fc8db;
background-image: linear-gradient(
140deg,
#55e2fd,
#58b2dd 50%,
#61cece 75%
);
border-radius: 1rem;
padding: 1rem;
}
.btn-image {
max-width: 185px;
transition: transform 0.2s;
}
.btn-image:hover {
transform: scale(1.05);
}
.p-2 {
padding: 2rem;
}
.mr-05 {
margin-right: 0.5rem;
}
.stats {
padding: 4rem 0;
text-align: center;
text-transform: uppercase;
strong {
font-family: Montserrat, -apple-system, BlinkMacSystemFont, Segoe UI,
Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-weight: 700;
margin: 0;
width: 100%;
display: block;
}
}
footer {
// border-top: 1px solid #2c2c2c;
// color: #6b6b6b;
font-size: 0.8rem;
padding: 1rem;
a {
color: #71a1ff;
text-decoration: none;
}
}
.col-12 {
.card {
height: 100%;
}
}
.card {
.card-body {
img {
margin-bottom: 2rem;
max-width: 100%;
width: 100px;
}
}
}