bsc

Comprehensive codebase and cou...
Log | Files | Refs | Activity | README | LICENSE

commit 06f3484d846895709a713493bcc563bb92d09578
parent 67c149dd0f88197390f5cf6116c9239dcc17baea
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Mon, 24 Aug 2026 10:19:43 +0530

Merge pull request #62 from notamitgamer/feat/tag-labels

feat: add tag/label system for pages
Diffstat:
Mdocs/.vitepress/config.mts | 1+
Adocs/.vitepress/tags.data.ts | 46++++++++++++++++++++++++++++++++++++++++++++++
Adocs/.vitepress/theme/components/Labels.vue | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/.vitepress/theme/index.ts | 10+++++++---
Adocs/tags.md | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmd.py | 47+++++++++++++++++++++++++++++++++++++++++++----
6 files changed, 287 insertions(+), 7 deletions(-)

diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts @@ -73,6 +73,7 @@ const vitePressConfig = { { text: 'Semester 8', link: '/semester_8/' }, ] }, + { text: 'Tags', link: '/tags' }, { text: 'License', link: 'https://github.com/notamitgamer/bsc/blob/main/LICENSE'}, { text: 'Changelog', link: '/changelog' }, ], diff --git a/docs/.vitepress/tags.data.ts b/docs/.vitepress/tags.data.ts @@ -0,0 +1,46 @@ +import { createContentLoader } from 'vitepress' + +export interface TaggedPage { + title: string + url: string +} + +export interface TagsData { + [tag: string]: TaggedPage[] +} + +declare const data: TagsData +export { data } + +function stripTitleIcon(title: string): string { + if (!title) return title + return title.replace(/<svg[\s\S]*?<\/svg>\s*/i, '').trim() +} + +export default createContentLoader('**/*.md', { + includeSrc: false, + render: false, + excerpt: false, + transform(raw) { + const byTag: TagsData = {} + + for (const page of raw) { + const tags = page.frontmatter?.tags + if (!Array.isArray(tags) || tags.length === 0) continue + + const title = stripTitleIcon(page.frontmatter?.title || page.url) + + for (const tag of tags) { + if (!tag) continue + if (!byTag[tag]) byTag[tag] = [] + byTag[tag].push({ title, url: page.url }) + } + } + + for (const tag in byTag) { + byTag[tag].sort((a, b) => a.title.localeCompare(b.title)) + } + + return byTag + } +}) diff --git a/docs/.vitepress/theme/components/Labels.vue b/docs/.vitepress/theme/components/Labels.vue @@ -0,0 +1,77 @@ +<script setup> +import { computed } from 'vue' +import { useData } from 'vitepress' + +const { frontmatter } = useData() + +const tags = computed(() => { + const raw = frontmatter.value.tags + if (!raw) return [] + return Array.isArray(raw) ? raw.filter(Boolean) : [raw] +}) + +function humanize(tag) { + return tag.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) +} +</script> + +<template> + <div v-if="tags.length" class="bsc-labels" aria-label="Page tags"> + <a + v-for="tag in tags" + :key="tag" + class="bsc-tag" + :href="`/tags#${tag}`" + :title="`Browse pages tagged ${humanize(tag)}`" + >{{ humanize(tag) }}</a> + </div> +</template> + +<style scoped> +.bsc-labels { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 16px; +} + +.bsc-tag { + position: relative; + display: inline-flex; + align-items: center; + height: 26px; + padding: 0 12px 0 20px; + font-size: 12.5px; + font-weight: 500; + line-height: 1; + text-decoration: none; + white-space: nowrap; + color: var(--vp-c-brand-1); + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-left: none; + clip-path: polygon(11px 0, 100% 0, 100% 100%, 11px 100%, 0 50%); + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; +} + +.bsc-tag::before { + content: ''; + position: absolute; + left: 6px; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--vp-c-text-3); +} + +.bsc-tag:hover { + color: #ffffff; + background: var(--vp-c-brand-1); +} + +.bsc-tag:hover::before { + background: #ffffff; +} +</style> diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts @@ -8,6 +8,7 @@ import ExternalLinkWarning from './components/ExternalLinkWarning.vue' import CopyLinkButton from './components/CopyLinkButton.vue' import MarkdownMenu from './components/MarkdownMenu.vue' import Breadcrumbs from './components/Breadcrumbs.vue' +import Labels from './components/Labels.vue' import './style.css' export default { @@ -19,9 +20,12 @@ export default { h(Banner), h(ExternalLinkWarning) ]), - 'doc-before': () => h('div', { class: 'bsc-doc-toolbar' }, [ - h(Breadcrumbs), - h(CopyLinkButton) + 'doc-before': () => h(Fragment, [ + h(Labels), + h('div', { class: 'bsc-doc-toolbar' }, [ + h(Breadcrumbs), + h(CopyLinkButton) + ]) ]), 'doc-footer-before': () => h(MarkdownMenu), 'doc-after': () => h(Footer), diff --git a/docs/tags.md b/docs/tags.md @@ -0,0 +1,113 @@ +--- +title: Tags +description: Browse every page by tag. +--- + +<script setup> +import { data as tagsData } from './.vitepress/tags.data.ts' + +const tagNames = Object.keys(tagsData).sort() + +function humanize(tag) { + return tag.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) +} +</script> + +# Tags + +Browse pages grouped by tag. Click any tag chip across the site to jump here. + +<div v-if="!tagNames.length" class="bsc-tags-empty"> + No tags found yet. +</div> + +<div v-for="tag in tagNames" :key="tag" :id="tag" class="bsc-tag-section"> + <h2 class="bsc-tag-heading"> + <a class="bsc-tag" :href="`#${tag}`">{{ humanize(tag) }}</a> + <span class="bsc-tag-count">{{ tagsData[tag].length }}</span> + </h2> + <ul class="bsc-tag-pages"> + <li v-for="page in tagsData[tag]" :key="page.url"> + <a :href="page.url">{{ page.title }}</a> + </li> + </ul> +</div> + +<style scoped> +.bsc-tags-empty { + color: var(--vp-c-text-2); + font-size: 14px; +} + +.bsc-tag-section { + margin: 28px 0; + padding-top: 8px; + border-top: 1px solid var(--vp-c-divider); +} + +.bsc-tag-heading { + display: flex; + align-items: center; + gap: 10px; + margin: 16px 0 10px; + border: none; + padding: 0; +} + +.bsc-tag-count { + font-size: 12px; + color: var(--vp-c-text-3); + font-weight: 400; +} + +.bsc-tag { + position: relative; + display: inline-flex; + align-items: center; + height: 30px; + padding: 0 14px 0 22px; + font-size: 14px; + font-weight: 500; + line-height: 1; + text-decoration: none; + white-space: nowrap; + color: var(--vp-c-brand-1); + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-left: none; + clip-path: polygon(12px 0, 100% 0, 100% 100%, 12px 100%, 0 50%); +} + +.bsc-tag::before { + content: ''; + position: absolute; + left: 7px; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--vp-c-text-3); +} + +.bsc-tag-pages { + margin: 0; + padding-left: 4px; + list-style: none; +} + +.bsc-tag-pages li { + padding: 4px 0; +} + +.bsc-tag-pages a { + color: var(--vp-c-text-1); + text-decoration: none; + font-size: 14px; +} + +.bsc-tag-pages a:hover { + color: var(--vp-c-brand-1); + text-decoration: underline; +} +</style> diff --git a/md.py b/md.py @@ -64,6 +64,41 @@ def read_block_comment(lines, start): def esc_yaml(s: str) -> str: return s.replace("'", "''") + +def slugify_tag(s: str) -> str: + """Turn a folder/lang name into a clean tag slug.""" + s = s.strip().lower() + s = re.sub(r'[_\s]+', '-', s) + s = re.sub(r'[^a-z0-9\-]', '', s) + return re.sub(r'-+', '-', s).strip('-') + +def derive_tags(rel_path: str, lang_label: str = "") -> list: + """Derive tag slugs from a file's path relative to BSC_ROOT. + e.g. semester_2/algorithms/foo.c -> ['sem2', 'algorithms', 'c']""" + parts = [p for p in os.path.normpath(rel_path).split(os.sep) if p] + tags = [] + + for part in parts[:-1]: # exclude the filename itself + m = re.match(r'^semester[_-]?(\d+)$', part, re.IGNORECASE) + if m: + tag = f"sem{m.group(1)}" + else: + tag = slugify_tag(part) + if tag and tag not in tags: + tags.append(tag) + + if lang_label: + lang_tag = slugify_tag(lang_label) + if lang_tag and lang_tag not in tags: + tags.append(lang_tag) + + return tags + +def format_tags_yaml(tags: list) -> str: + if not tags: + return "tags: []" + items = ", ".join(f"'{esc_yaml(t)}'" for t in tags) + return f"tags: [{items}]" def esc_html(s: str) -> str: @@ -241,7 +276,7 @@ def parse_algo_md(content): return title, problem_statement, body_lines -def build_algo_md(filename_base, title, problem_statement, body_lines, source_path=""): +def build_algo_md(filename_base, title, problem_statement, body_lines, source_path="", tags=None): """Build VitePress .md from parsed algo content.""" desc = problem_statement if problem_statement else f"Algorithm — {title}" @@ -250,6 +285,7 @@ def build_algo_md(filename_base, title, problem_statement, body_lines, source_pa f"title: '{ALGO_ICON_SVG} {esc_yaml(title)}'", f"description: '{esc_yaml(desc)}'", f"source: '{source_path}'", + format_tags_yaml(tags or []), "---", "", ] @@ -294,7 +330,7 @@ def build_algo_md(filename_base, title, problem_statement, body_lines, source_pa def build_md(filename, lang_label, fence_lang, author, date, repo, license_str, - problem_statement, code, rel_url): + problem_statement, code, rel_url, tags=None): desc = problem_statement if problem_statement else f"{lang_label} program — {filename}" icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline; margin-bottom:-2px; margin-right:6px;"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1"/><path d="M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1"/></svg>' @@ -304,6 +340,7 @@ def build_md(filename, lang_label, fence_lang, author, date, repo, license_str, f"title: '{icon_svg} {esc_yaml(filename)}'", f"description: '{esc_yaml(desc)}'", f"source: '{rel_url}'", + format_tags_yaml(tags or []), "---", ] @@ -441,7 +478,8 @@ def main(): if not title: title = filename_base rel_url_algo = rel_path.replace('\\', '/') - md_content = build_algo_md(filename_base, title, problem_statement, body_lines, rel_url_algo) + algo_tags = derive_tags(rel_path) + md_content = build_algo_md(filename_base, title, problem_statement, body_lines, rel_url_algo, algo_tags) rel_path = os.path.relpath(full_path, BSC_ROOT) md_rel = os.path.splitext(rel_path)[0] + '.md' @@ -491,9 +529,10 @@ def main(): else: author, date, repo, license_str, problem_statement, code = parse_hash_style(content) + file_tags = derive_tags(rel_path, lang_info['label']) md_content = build_md( filename, lang_info['label'], lang_info['fence'], author, date, repo, license_str, - problem_statement, code, rel_url, + problem_statement, code, rel_url, file_tags, ) md_rel = os.path.splitext(rel_path)[0] + '.md'
© notamitgamer • Site Built: 2026-09-05 01:53:16 UTC • git-mirror commit: c170d72 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror