From 906828769487524d5e533cf96b0e8e263290a78a Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Sun, 25 Jan 2026 11:27:07 +0100 Subject: [PATCH] feat: add Markdown learning module with 8 beginner lessons - Add markdown-basics module with lessons for headings, text formatting, lists, links, and inline code - Integrate markdown section with blue color theme (#5b8dd9) - Add markdown mode support in CodeEditor and LessonEngine - Add markdown preview rendering using marked library - Add section overview page with educational content - Add markdown reference page with syntax guide - Add i18n translations for 6 languages (EN, DE, PL, ES, AR, UK) - Update router to recognize #markdown as section route - Add all section-specific CSS styles for markdown theme --- lessons/40-markdown-basics.json | 197 +++++++++++++++++++++++ package-lock.json | 58 +++++-- package.json | 2 + schemas/code-crispies-module-schema.json | 6 +- src/app.js | 192 +++++++++++++++++++++- src/config/lessons.js | 13 ++ src/config/sections.js | 8 + src/helpers/router.js | 3 +- src/i18n.js | 6 + src/impl/CodeEditor.js | 3 +- src/impl/LessonEngine.js | 65 ++++++++ src/index.html | 8 + src/main.css | 90 +++++++++++ tests/unit/lessons.test.js | 2 +- 14 files changed, 627 insertions(+), 26 deletions(-) create mode 100644 lessons/40-markdown-basics.json diff --git a/lessons/40-markdown-basics.json b/lessons/40-markdown-basics.json new file mode 100644 index 0000000..9223b95 --- /dev/null +++ b/lessons/40-markdown-basics.json @@ -0,0 +1,197 @@ +{ + "$schema": "../schemas/code-crispies-module-schema.json", + "id": "markdown-basics", + "title": "Markdown Basics", + "description": "Learn to format text documents with Markdown, a simple and readable markup language used everywhere from GitHub to note-taking apps.", + "mode": "markdown", + "difficulty": "beginner", + "lessons": [ + { + "id": "md-headings", + "title": "Headings", + "description": "Markdown uses hash symbols # to create headings. One # creates the largest heading (h1), two ## creates a smaller heading (h2), and so on up to six levels.

# Main Title\n## Section\n### Subsection
", + "task": "Create a main heading by typing # Hello", + "previewHTML": "", + "previewBaseCSS": "", + "sandboxCSS": "", + "initialCode": "", + "solution": "# Hello", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "^#\\s+.+", + "message": "Start with # followed by a space and your heading text" + }, + { + "type": "contains", + "value": "Hello", + "message": "Your heading should contain Hello" + } + ] + }, + { + "id": "md-heading-levels", + "title": "Heading Levels", + "description": "Use more # symbols for smaller headings. ## creates an h2, ### an h3. This creates a clear document structure with visual hierarchy.", + "task": "Create an h2 heading with ## About followed by an h3 heading with ### Details", + "previewHTML": "", + "previewBaseCSS": "", + "sandboxCSS": "", + "initialCode": "", + "solution": "## About\n\n### Details", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "^##\\s+About", + "message": "Start with ## About" + }, + { + "type": "regex", + "value": "###\\s+Details", + "message": "Add ### Details for the h3 heading" + } + ] + }, + { + "id": "md-bold", + "title": "Bold Text", + "description": "Wrap text in double asterisks ** or double underscores __ to make it bold. This emphasizes important words or phrases.", + "task": "Make the word important bold by wrapping it with **", + "previewHTML": "", + "previewBaseCSS": "", + "sandboxCSS": "", + "initialCode": "This is important text.", + "solution": "This is **important** text.", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "\\*\\*important\\*\\*", + "message": "Wrap important with double asterisks: **important**" + } + ] + }, + { + "id": "md-italic", + "title": "Italic Text", + "description": "Wrap text in single asterisks * or single underscores _ to make it italic. Use this for subtle emphasis or titles of works.", + "task": "Make the word elegant italic by wrapping it with *", + "previewHTML": "", + "previewBaseCSS": "", + "sandboxCSS": "", + "initialCode": "A simple and elegant solution.", + "solution": "A simple and *elegant* solution.", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "\\*elegant\\*", + "message": "Wrap elegant with single asterisks: *elegant*" + }, + { + "type": "not_contains", + "value": "**elegant**", + "message": "Use single asterisks for italic, not double" + } + ] + }, + { + "id": "md-unordered-list", + "title": "Bullet Lists", + "description": "Create bullet lists using -, *, or + at the start of each line. Each item goes on its own line.", + "task": "Create a bullet list with three items: Apple, Banana, Cherry", + "previewHTML": "", + "previewBaseCSS": "", + "sandboxCSS": "", + "initialCode": "", + "solution": "- Apple\n- Banana\n- Cherry", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "^[-*+]\\s+Apple", + "message": "Start with a dash, asterisk, or plus followed by Apple" + }, + { + "type": "regex", + "value": "[-*+]\\s+Banana", + "message": "Add Banana as a list item" + }, + { + "type": "regex", + "value": "[-*+]\\s+Cherry", + "message": "Add Cherry as a list item" + } + ] + }, + { + "id": "md-ordered-list", + "title": "Numbered Lists", + "description": "Create numbered lists by starting lines with 1., 2., etc. Markdown automatically numbers them in sequence.", + "task": "Create a numbered list: Wake up, Eat breakfast, Start coding", + "previewHTML": "", + "previewBaseCSS": "", + "sandboxCSS": "", + "initialCode": "", + "solution": "1. Wake up\n2. Eat breakfast\n3. Start coding", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "\\d+\\.\\s+Wake up", + "message": "Start with a number and period: 1. Wake up" + }, + { + "type": "regex", + "value": "\\d+\\.\\s+Eat breakfast", + "message": "Add Eat breakfast as a numbered item" + }, + { + "type": "regex", + "value": "\\d+\\.\\s+Start coding", + "message": "Add Start coding as a numbered item" + } + ] + }, + { + "id": "md-links", + "title": "Links", + "description": "Create links with [text](url). The text in brackets is what readers see; the URL in parentheses is where they go when clicked.", + "task": "Create a link that shows Google and goes to https://google.com", + "previewHTML": "", + "previewBaseCSS": "", + "sandboxCSS": "", + "initialCode": "", + "solution": "[Google](https://google.com)", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "\\[Google\\]\\(https?://google\\.com\\)", + "message": "Use the format [Google](https://google.com)" + } + ] + }, + { + "id": "md-inline-code", + "title": "Inline Code", + "description": "Wrap text in backticks ` to format it as code. This is useful for variable names, commands, or short code snippets in your text.", + "task": "Format npm install as inline code using backticks", + "previewHTML": "", + "previewBaseCSS": "", + "sandboxCSS": "", + "initialCode": "Run npm install to install dependencies.", + "solution": "Run `npm install` to install dependencies.", + "previewContainer": "preview-area", + "validations": [ + { + "type": "regex", + "value": "`npm install`", + "message": "Wrap npm install with backticks: `npm install`" + } + ] + } + ] +} diff --git a/package-lock.json b/package-lock.json index 3492660..510a9c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,14 @@ "@codemirror/commands": "^6.10.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-markdown": "^6.5.0", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.4", "@emmetio/codemirror6-plugin": "^0.4.0", "@supabase/supabase-js": "^2.90.1", "codemirror": "^6.0.2", + "marked": "^17.0.1", "whatwg-fetch": "^3.6.20" }, "devDependencies": { @@ -156,7 +158,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -169,7 +170,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz", "integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", @@ -182,7 +182,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -196,7 +195,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -224,12 +222,26 @@ "@lezer/javascript": "^1.0.0" } }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.11.3", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -266,7 +278,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -288,7 +299,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz", "integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -384,7 +394,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -408,7 +417,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -974,9 +982,9 @@ } }, "node_modules/@lezer/common": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz", - "integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz", + "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==", "license": "MIT" }, "node_modules/@lezer/css": { @@ -1030,6 +1038,16 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", @@ -2336,7 +2354,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -2433,6 +2450,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -2579,7 +2608,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3123,7 +3151,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -3222,7 +3249,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/package.json b/package.json index 3abcaf9..9386db1 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,14 @@ "@codemirror/commands": "^6.10.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-markdown": "^6.5.0", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.4", "@emmetio/codemirror6-plugin": "^0.4.0", "@supabase/supabase-js": "^2.90.1", "codemirror": "^6.0.2", + "marked": "^17.0.1", "whatwg-fetch": "^3.6.20" } } diff --git a/schemas/code-crispies-module-schema.json b/schemas/code-crispies-module-schema.json index 81d2a1e..2f2fc5b 100644 --- a/schemas/code-crispies-module-schema.json +++ b/schemas/code-crispies-module-schema.json @@ -19,8 +19,8 @@ }, "mode": { "type": "string", - "enum": ["css", "tailwind", "html"], - "description": "Whether this module teaches CSS, Tailwind, or HTML" + "enum": ["css", "tailwind", "html", "markdown"], + "description": "Whether this module teaches CSS, Tailwind, HTML, or Markdown" }, "difficulty": { "type": "string", @@ -60,7 +60,7 @@ }, "mode": { "type": "string", - "enum": ["css", "tailwind", "html"], + "enum": ["css", "tailwind", "html", "markdown"], "description": "Override module mode for individual lessons" }, "tailwindConfig": { diff --git a/src/app.js b/src/app.js index a073ba6..0014d40 100644 --- a/src/app.js +++ b/src/app.js @@ -573,6 +573,11 @@ function updateEditorForMode(mode) { label: "CSS Editor", cmMode: "css" }, + markdown: { + placeholder: "# Heading\n\nWrite your **Markdown** here...", + label: "Markdown Editor", + cmMode: "markdown" + }, playground: { placeholder: "\n\n", label: "HTML & CSS", @@ -1409,6 +1414,85 @@ const sectionContent = { + `, + markdown: ` +
+

Markdown is a lightweight markup language created by John Gruber in 2004. It lets you write formatted text using plain text syntax that's easy to read and write. Markdown is used everywhere—from GitHub READMEs to documentation, note-taking apps, and content management systems.

+

The beauty of Markdown is its simplicity: # Heading creates a heading, **bold** makes text bold, and [link](url) creates a link. No complex HTML tags needed. Markdown files can be converted to HTML, PDF, or many other formats.

+
+ +
+
+

Headings & Structure

+

Create document structure with headings using # symbols. One # for h1, two ## for h2, up to six levels. This creates a clear hierarchy in your documents.

+

+ Practice headings → +

+
+
+
+
# Main Title
+## Section
+### Subsection
+#### Detail
+
+
+
+ +
+
+

Text Formatting

+

Emphasize text with **bold** or *italic*. Combine them with ***bold italic***. Use backticks for \`inline code\` to highlight commands or code snippets in your text.

+

+ Practice formatting → +

+
+
+
+
This is **bold** text.
+This is *italic* text.
+This is \`inline code\`.
+
+
+
+ +
+
+

Lists

+

Create bullet lists with -, *, or +. Numbered lists use 1., 2., etc. Indent items with spaces to create nested lists for complex outlines.

+

+ Practice lists → +

+
+
+
+
- First item
+- Second item
+  - Nested item
+
+1. Step one
+2. Step two
+3. Step three
+
+
+
+ +
+
+

Links & Images

+

Create links with [text](url) syntax. Images use the same format with an exclamation mark: ![alt text](image-url). The alt text describes the image for accessibility.

+

+ Practice links → +

+
+
+
+
[Visit Google](https://google.com)
+
+![Logo](https://example.com/logo.png)
+
+
+
` }; @@ -1925,6 +2009,105 @@ const referenceContent = {

Learn: HTML Section | Style with: CSS Properties

+ `, + + markdown: ` +

Markdown Syntax Reference

+

A quick guide to Markdown syntax for formatting text documents. Markdown is used in GitHub, documentation, and note-taking apps.

+ +
+

Text Formatting

+ + + + + + + + + +
SyntaxResultNotes
**bold**boldOr use __bold__
*italic*italicOr use _italic_
***bold italic***bold italicCombine both
~~strikethrough~~strikethroughGFM extension
\`inline code\`inline codeMonospace font
+
+ +
+

Headings

+ + + + + + + + + + +
SyntaxLevelUsage
# Heading 1h1Document title
## Heading 2h2Main sections
### Heading 3h3Subsections
#### Heading 4h4Minor sections
##### Heading 5h5Rarely used
###### Heading 6h6Smallest heading
+
+ +
+

Lists

+ + + + + + + + + +
SyntaxTypeNotes
- ItemUnorderedOr use * or +
1. ItemOrderedNumbers auto-increment
- NestedNested list2-space indent
- [x] TaskTask listGFM extension
- [ ] TaskUnchecked taskInteractive checkboxes
+
+ +
+

Links & Images

+ + + + + + + + + +
SyntaxPurposeExample
[text](url)Inline link[Google](https://google.com)
[text](url "title")Link with tooltipHover text
![alt](url)ImageAlt text for accessibility
<url>Auto-linkURLs become clickable
[ref]: urlReference linkDefine at doc bottom
+
+ +
+

Code Blocks

+ + + + + + + +
SyntaxPurposeNotes
\`\`\`Fenced code3 backticks or tildes
\`\`\`jsSyntax highlightAdd language identifier
codeIndented code4-space indent
+
+ +
+

Block Elements

+ + + + + + + +
SyntaxElementNotes
> QuoteBlockquoteNest with >>
---Horizontal ruleOr *** or ___
| A | B |TableGFM extension
+
+ +
+

Tables (GFM)

+
+
| Header 1 | Header 2 |
+|----------|----------|
+| Cell 1   | Cell 2   |
+| Cell 3   | Cell 4   |
+
+

Use colons for alignment: :--- (left), :---: (center), ---: (right)

+
+ +

Learn: Markdown Section | Also try: HTML Elements

` }; @@ -1970,7 +2153,7 @@ function updatePageMeta(route) { break; case RouteType.SECTION: { - const sectionNames = { css: "CSS", html: "HTML", tailwind: "Tailwind CSS" }; + const sectionNames = { css: "CSS", html: "HTML", tailwind: "Tailwind CSS", markdown: "Markdown" }; const sectionName = sectionNames[route.sectionId] || route.sectionId; title = `${sectionName} Lessons - CODE CRISPIES | Learn ${sectionName}`; description = `Learn ${sectionName} through interactive coding exercises. Hands-on practice with instant feedback.`; @@ -1994,7 +2177,8 @@ function updatePageMeta(route) { selectors: "CSS Selectors", flexbox: "Flexbox", grid: "CSS Grid", - html: "HTML Elements" + html: "HTML Elements", + markdown: "Markdown Syntax" }; const refName = refNames[route.refId] || "Reference"; title = `${refName} Reference - CODE CRISPIES`; @@ -2163,7 +2347,7 @@ function renderFooterLessonLinks() { * Update progress indicators on landing page */ function updateLandingProgress() { - ["css", "html", "tailwind"].forEach((sectionId) => { + ["css", "html", "tailwind", "markdown"].forEach((sectionId) => { const progressEl = document.getElementById(`${sectionId}-progress`); if (progressEl) { const sectionModules = getModulesBySection(lessonEngine.modules, sectionId); @@ -2249,7 +2433,7 @@ function showReferencePage(refId) { const activeRef = refId || "css"; // Map reference to section for color coding - const refToSection = { css: "css", selectors: "css", flexbox: "css", grid: "css", html: "html" }; + const refToSection = { css: "css", selectors: "css", flexbox: "css", grid: "css", html: "html", markdown: "markdown" }; updateSectionColor(refToSection[activeRef] || "css"); // Track reference page view diff --git a/src/config/lessons.js b/src/config/lessons.js index 71a159a..33f422c 100644 --- a/src/config/lessons.js +++ b/src/config/lessons.js @@ -30,6 +30,7 @@ import gradientsEN from "../../lessons/09-gradients.json"; import filtersEN from "../../lessons/11-filters.json"; import positioningEN from "../../lessons/12-positioning.json"; import pseudoElementsEN from "../../lessons/13-pseudo-elements.json"; +import markdownBasicsEN from "../../lessons/40-markdown-basics.json"; import playgroundEN from "../../lessons/98-playground.json"; import goodbyeEN from "../../lessons/99-goodbye.json"; @@ -162,6 +163,8 @@ const moduleStoreEN = [ htmlFieldsetEN, htmlDatalistEN, htmlTablesEN, + // Markdown + markdownBasicsEN, // Outro goodbyeEN, playgroundEN @@ -201,6 +204,8 @@ const moduleStoreDE = [ htmlFieldsetDE, htmlDatalistDE, htmlTablesDE, + // Markdown + markdownBasicsEN, // Using EN fallback until translated // Outro goodbyeEN, playgroundEN @@ -240,6 +245,8 @@ const moduleStorePL = [ htmlFieldsetPL, htmlDatalistPL, htmlTablesPL, + // Markdown + markdownBasicsEN, // Using EN fallback until translated // Outro goodbyeEN, playgroundEN @@ -279,6 +286,8 @@ const moduleStoreES = [ htmlFieldsetES, htmlDatalistES, htmlTablesES, + // Markdown + markdownBasicsEN, // Using EN fallback until translated // Outro goodbyeEN, playgroundEN @@ -318,6 +327,8 @@ const moduleStoreAR = [ htmlFieldsetAR, htmlDatalistAR, htmlTablesAR, + // Markdown + markdownBasicsEN, // Using EN fallback until translated // Outro goodbyeEN, playgroundEN @@ -357,6 +368,8 @@ const moduleStoreUK = [ htmlFieldsetUK, htmlDatalistUK, htmlTablesUK, + // Markdown + markdownBasicsEN, // Using EN fallback until translated // Outro goodbyeEN, playgroundEN diff --git a/src/config/sections.js b/src/config/sections.js index 71c42fd..29f8f08 100644 --- a/src/config/sections.js +++ b/src/config/sections.js @@ -24,6 +24,13 @@ export const sections = { description: "Utility-first CSS framework", color: "#26a69a", order: 3 + }, + markdown: { + id: "markdown", + title: "Markdown", + description: "Lightweight markup language for formatting text", + color: "#5b8dd9", + order: 4 } }; @@ -57,6 +64,7 @@ export function getModuleSection(module) { const mode = module.mode || "css"; if (mode === "html") return "html"; if (mode === "tailwind") return "tailwind"; + if (mode === "markdown") return "markdown"; return "css"; } diff --git a/src/helpers/router.js b/src/helpers/router.js index 4b54b4e..c6baece 100644 --- a/src/helpers/router.js +++ b/src/helpers/router.js @@ -8,6 +8,7 @@ * - #css -> CSS section landing * - #html -> HTML section landing * - #tailwind -> Tailwind section landing + * - #markdown -> Markdown section landing * - #reference/css -> CSS cheatsheet * - #module/index -> Lesson (e.g., #flexbox/2) */ @@ -26,7 +27,7 @@ export const RouteType = { /** * Valid section IDs */ -const SECTIONS = ["css", "html", "tailwind"]; +const SECTIONS = ["css", "html", "tailwind", "markdown"]; /** * Valid language codes for URL-based switching diff --git a/src/i18n.js b/src/i18n.js index 09885db..d3e6049 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -143,6 +143,7 @@ const translations = { landingCssDesc: "Styling, layout, and animations", landingHtmlDesc: "Semantic markup and native elements", landingTailwindDesc: "Utility-first CSS framework", + landingMarkdownDesc: "Format text with simple syntax", comingSoon: "Coming Soon", landingCtaTitle: "Start Learning Today", landingCtaSub: "Free and open source. No account required. Progress saved locally.", @@ -376,6 +377,7 @@ const translations = { landingCssDesc: "Styling, Layout und Animationen", landingHtmlDesc: "Semantisches Markup und native Elemente", landingTailwindDesc: "Utility-first CSS-Framework", + landingMarkdownDesc: "Text mit einfacher Syntax formatieren", comingSoon: "Bald verfügbar", landingCtaTitle: "Jetzt gleich anfangen", landingCtaSub: "Kostenlos und Open Source. Kein Konto erforderlich. Fortschritt wird lokal gespeichert.", @@ -605,6 +607,7 @@ const translations = { landingCssDesc: "Stylowanie, układy i animacje", landingHtmlDesc: "Semantyczne znaczniki i natywne elementy", landingTailwindDesc: "Framework CSS oparty na klasach utility", + landingMarkdownDesc: "Formatuj tekst prostą składnią", comingSoon: "Wkrótce", landingCtaTitle: "Zacznij naukę już dziś", landingCtaSub: "Darmowe i open source. Bez konta. Postęp zapisywany lokalnie.", @@ -836,6 +839,7 @@ const translations = { landingCssDesc: "Estilos, diseño y animaciones", landingHtmlDesc: "Marcado semántico y elementos nativos", landingTailwindDesc: "Framework CSS basado en utilidades", + landingMarkdownDesc: "Formatea texto con sintaxis simple", comingSoon: "Próximamente", landingCtaTitle: "Empieza a aprender hoy", landingCtaSub: "Gratis y de código abierto. Sin cuenta requerida. Progreso guardado localmente.", @@ -1062,6 +1066,7 @@ const translations = { landingCssDesc: "التنسيق والتخطيط والرسوم المتحركة", landingHtmlDesc: "الترميز الدلالي والعناصر الأصلية", landingTailwindDesc: "إطار CSS قائم على الأدوات", + landingMarkdownDesc: "تنسيق النص بصيغة بسيطة", comingSoon: "قريباً", landingCtaTitle: "ابدأ التعلم اليوم", landingCtaSub: "مجاني ومفتوح المصدر. لا حاجة لحساب. يُحفظ التقدم محليًا.", @@ -1290,6 +1295,7 @@ const translations = { landingCssDesc: "Стилізація, макети та анімації", landingHtmlDesc: "Семантична розмітка та нативні елементи", landingTailwindDesc: "CSS-фреймворк на основі утиліт", + landingMarkdownDesc: "Форматуй текст простим синтаксисом", comingSoon: "Незабаром", landingCtaTitle: "Почни вчитися сьогодні", landingCtaSub: "Безкоштовно та з відкритим кодом. Без реєстрації. Прогрес зберігається локально.", diff --git a/src/impl/CodeEditor.js b/src/impl/CodeEditor.js index e916aff..d34bd4f 100644 --- a/src/impl/CodeEditor.js +++ b/src/impl/CodeEditor.js @@ -7,6 +7,7 @@ import { defaultKeymap, historyKeymap, indentMore, indentLess, undo, redo } from import { history } from "@codemirror/commands"; import { html } from "@codemirror/lang-html"; import { css } from "@codemirror/lang-css"; +import { markdown } from "@codemirror/lang-markdown"; import { autocompletion } from "@codemirror/autocomplete"; import { abbreviationTracker, expandAbbreviation } from "@emmetio/codemirror6-plugin"; import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; @@ -180,7 +181,7 @@ export class CodeEditor { const fullDoc = prefix + initialValue + suffix; // Get language extension based on mode - const langExtension = this.mode === "html" ? html() : css(); + const langExtension = this.mode === "html" ? html() : this.mode === "markdown" ? markdown() : css(); // Create read-only zones decorations const readOnlyMark = Decoration.mark({ class: "cm-readonly-zone" }); diff --git a/src/impl/LessonEngine.js b/src/impl/LessonEngine.js index 795e6b7..3604e16 100644 --- a/src/impl/LessonEngine.js +++ b/src/impl/LessonEngine.js @@ -3,6 +3,7 @@ * Single source of truth for lesson state and progress */ import { validateUserCode } from "../helpers/validator.js"; +import { marked } from "marked"; // Auth sync - lazy loaded to avoid circular dependencies let authModule = null; @@ -255,6 +256,38 @@ export class LessonEngine { ${htmlWithClasses} + `); + } else if (mode === "markdown") { + // For Markdown mode, parse user code to HTML + const renderedHtml = marked.parse(this.userCode || ""); + iframeDoc.write(` + + + + + + + + + + ${renderedHtml} + + `); } else { // Original CSS mode @@ -349,6 +382,38 @@ export class LessonEngine { ${htmlWithClasses} + `); + } else if (mode === "markdown") { + // For Markdown mode, parse solution to HTML + const renderedHtml = marked.parse(solutionCode || ""); + iframeDoc.write(` + + + + + + + + + + ${renderedHtml} + + `); } else { // CSS mode - wrap solution with prefix/suffix diff --git a/src/index.html b/src/index.html index 70e2cd2..81e6c22 100644 --- a/src/index.html +++ b/src/index.html @@ -75,6 +75,7 @@ CSS HTML Tailwind + Markdown Reference @@ -168,6 +169,12 @@

Utility-first CSS framework

+ +
MD
+

Markdown

+

Lightweight markup for formatting text

+ +

Best on desktop or tablet (landscape). Mobile works, but larger screens make coding easier. @@ -328,6 +335,7 @@ Flexbox Grid HTML Elements + Markdown

diff --git a/src/main.css b/src/main.css index 489b33d..fca0d98 100644 --- a/src/main.css +++ b/src/main.css @@ -283,6 +283,14 @@ kbd { background: #1aafb8; } +[data-section="markdown"] .logo h1 .code-text { + color: #5b8dd9; +} + +[data-section="markdown"] .logo h1 .crispies-text { + background: #5b8dd9; +} + .help-toggle { width: 28px; height: 28px; @@ -3602,6 +3610,14 @@ input:checked + .toggle-slider::before { --section-color-rgb: 26, 175, 184; } +/* Markdown Section - Blue */ +[data-section="markdown"] { + --section-color: #5b8dd9; + --section-color-light: #7ba3e5; + --section-color-dark: #4070b8; + --section-color-rgb: 91, 141, 217; +} + /* Apply section colors to nav links */ .nav-link[data-section="css"] { color: #9163b8; @@ -3615,6 +3631,10 @@ input:checked + .toggle-slider::before { color: #1aafb8; } +.nav-link[data-section="markdown"] { + color: #5b8dd9; +} + .nav-link[data-section="css"]:hover, .nav-link[data-section="css"].active { background: rgba(145, 99, 184, 0.1); @@ -3633,6 +3653,12 @@ input:checked + .toggle-slider::before { color: #0d8f96; } +.nav-link[data-section="markdown"]:hover, +.nav-link[data-section="markdown"].active { + background: rgba(91, 141, 217, 0.1); + color: #4070b8; +} + /* Hint section colors */ body[data-section="css"] .hint { background: rgba(145, 99, 184, 0.3); @@ -3661,6 +3687,15 @@ body[data-section="tailwind"] .hint-progress { background: #1aafb8; } +body[data-section="markdown"] .hint { + background: rgba(91, 141, 217, 0.3); + border-left-color: #7ba3e5; +} + +body[data-section="markdown"] .hint-progress { + background: #5b8dd9; +} + /* RTL hint border */ [dir="rtl"] body[data-section="css"] .hint { border-right-color: #a98cd6; @@ -3674,6 +3709,10 @@ body[data-section="tailwind"] .hint-progress { border-right-color: #4db6ac; } +[dir="rtl"] body[data-section="markdown"] .hint { + border-right-color: #7ba3e5; +} + /* Reference nav link colors */ .ref-nav-link[data-ref="css"], .ref-nav-link[data-ref="selectors"], @@ -3759,6 +3798,24 @@ body[data-section="tailwind"] .cm-editor .cm-activeLine { background-color: rgba(26, 175, 184, 0.08) !important; } +body[data-section="markdown"] .cm-editor .cm-content { + caret-color: #5b8dd9 !important; +} + +body[data-section="markdown"] .cm-editor .cm-cursor, +body[data-section="markdown"] .cm-editor .cm-dropCursor { + border-left-color: #5b8dd9 !important; +} + +body[data-section="markdown"] .cm-editor .cm-selectionBackground, +body[data-section="markdown"] .cm-editor .cm-content ::selection { + background-color: rgba(91, 141, 217, 0.25) !important; +} + +body[data-section="markdown"] .cm-editor .cm-activeLine { + background-color: rgba(91, 141, 217, 0.08) !important; +} + /* Module pill section colors */ body[data-section="css"] .module-pill { background: rgba(145, 99, 184, 0.1); @@ -3787,6 +3844,15 @@ body[data-section="tailwind"] .module-pill .level-indicator { color: #0d8f96; } +body[data-section="markdown"] .module-pill { + background: rgba(91, 141, 217, 0.1); + color: #5b8dd9; +} + +body[data-section="markdown"] .module-pill .level-indicator { + color: #4070b8; +} + /* Code block border section colors */ body[data-section="css"] .code-block { border-color: rgba(145, 99, 184, 0.4); @@ -3800,6 +3866,10 @@ body[data-section="tailwind"] .code-block { border-color: rgba(26, 175, 184, 0.4); } +body[data-section="markdown"] .code-block { + border-color: rgba(91, 141, 217, 0.4); +} + /* Section code block CodeMirror syntax highlighting overrides */ body[data-section="css"] .code-block .cm-editor .cm-line { color: #c9c0e0; @@ -3813,6 +3883,10 @@ body[data-section="tailwind"] .code-block .cm-editor .cm-line { color: #c0e0e8; } +body[data-section="markdown"] .code-block .cm-editor .cm-line { + color: #c0d0e8; +} + /* Task instruction bubble section colors */ [data-section="css"] .task-instruction { background: rgba(145, 99, 184, 0.92); @@ -3826,6 +3900,10 @@ body[data-section="tailwind"] .code-block .cm-editor .cm-line { background: rgba(26, 175, 184, 0.92); } +[data-section="markdown"] .task-instruction { + background: rgba(91, 141, 217, 0.92); +} + /* Section page progress bar colors */ body[data-section="css"] .section-progress-bar .progress-fill { background: #9163b8; @@ -3839,6 +3917,10 @@ body[data-section="tailwind"] .section-progress-bar .progress-fill { background: #1aafb8; } +body[data-section="markdown"] .section-progress-bar .progress-fill { + background: #5b8dd9; +} + /* Section page header colors */ [data-section="css"] .section-hero h1 { color: #9163b8; @@ -3852,6 +3934,10 @@ body[data-section="tailwind"] .section-progress-bar .progress-fill { color: #1aafb8; } +[data-section="markdown"] .section-hero h1 { + color: #5b8dd9; +} + /* Lesson title h2 section colors */ body[data-section="css"] #lesson-title { color: #9163b8; @@ -3865,6 +3951,10 @@ body[data-section="tailwind"] #lesson-title { color: #1aafb8; } +body[data-section="markdown"] #lesson-title { + color: #5b8dd9; +} + /* Section and Reference footer - override landing-footer styles */ .section-footer.landing-footer, .reference-footer.landing-footer { diff --git a/tests/unit/lessons.test.js b/tests/unit/lessons.test.js index 7332b97..ec6ef70 100644 --- a/tests/unit/lessons.test.js +++ b/tests/unit/lessons.test.js @@ -27,7 +27,7 @@ describe("Lessons Config Module", () => { modules.forEach((module) => { module.lessons.forEach((lesson) => { expect(lesson.mode).toBeDefined(); - expect(["html", "css", "tailwind", "playground"]).toContain(lesson.mode); + expect(["html", "css", "tailwind", "markdown", "playground"]).toContain(lesson.mode); }); }); });