diff --git a/.gitignore b/.gitignore index 2633a56..d61c66c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ public/** resources/** tmp/** content/** +examples/** # Local Netlify folder .netlify diff --git a/Makefile b/Makefile index 9ca04f2..1ccdc5c 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ LOGLEVEL ?= INFO ENV ?= development HUGO_CONTENT ?= ./content/en HUGO_BUILD ?= --gc -HUGO_SERVER ?= --quiet --disableFastRender -b http://$(IP) --bind $(IP) +HUGO_SERVER ?= --disableFastRender -b http://$(IP) --bind $(IP) DOCKER_IMAGE=image-redis-stack-website DOCKER_CONTAINER=container-$(DOCKER_IMAGE) DOCKER_PORT=-p 1313:1313 @@ -41,7 +41,7 @@ up: clean: # @rm -f data/*.json - @rm -rf $(HUGO_CONTENT) public tmp + @rm -rf $(HUGO_CONTENT) examples public tmp ifneq ($(VOL),) DOCKER_VOL=-v $(VOL):$(VOL) diff --git a/assets/css/index.css b/assets/css/index.css index b72bb84..a8a1dbc 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -444,8 +444,39 @@ select { line-height: 1.25rem; } -.codetabs > .panel .highlight .chroma .lntd > pre { - @apply p-0; +.codetabs > .panel .highlight .chroma > .lntable td:first-child .chroma { + @apply rounded-none p-0; +} + +.codetabs > .panel .highlight .chroma > .lntable td:last-child .chroma { + @apply rounded-none p-0; +} + +.codetabs > .panel:not([aria-expanded]) .highlight .chroma > code > .line:not(.hl), +.codetabs > .panel:not([aria-expanded]) .highlight .chroma > .lntable .lntd code > .lnt, +.codetabs > .panel:not([aria-expanded]) .highlight .chroma > .lntable .lntd code > .line:not(.hl) { + @apply hidden; +} + +.codetabs > .panel:where([aria-expanded]) .highlight .chroma > code > .line:not(.hl), +.codetabs > .panel:where([aria-expanded]) .highlight .chroma > .lntable .lntd code > .lnt, +.codetabs > .panel:where([aria-expanded]) .highlight .chroma > .lntable .lntd code > .line:not(.hl) { + @apply opacity-60; +} + +.codetabs > .panel .highlight .chroma > code > .hl, +.codetabs > .panel .highlight .chroma > .lntable .lntd code > .hl { + @apply bg-inherit; +} + +.codetabs > .panel:where([aria-expanded]) #visibility-on, +.codetabs > .panel:not([aria-expanded]) #visibility-off { + @apply hidden; +} + +.codetabs > .panel:where([aria-expanded]) #visibility-off, +.codetabs > .panel:not([aria-expanded]) #visibility-on { + @apply block; } .download-cards { @@ -558,24 +589,32 @@ html { @apply align-top p-0 m-0 border-0; } +.chroma .lntd:last-child { + @apply flex-1; +} + /* LineTable */ .chroma .lntable { - @apply p-0 m-0 border-0 w-auto; + @apply p-0 m-0 border-0 min-w-full; } /* LineHighlight */ .chroma .hl { - @apply bg-yellow-100; + @apply flex bg-indigo-900/80; } /* LineNumbersTable */ .chroma .lnt { - @apply text-slate-400 whitespace-pre mr-4 px-2; + @apply text-slate-400 whitespace-pre pl-2 pr-4; } /* LineNumbers */ .chroma .ln { - @apply text-slate-400 whitespace-pre mr-4 px-4; + @apply text-slate-400 whitespace-pre px-4; +} + +.chroma .lntable tr { + @apply flex items-stretch; } /* Line */ diff --git a/build/stack/component.py b/build/stack/component.py index ee69e95..c4e05de 100644 --- a/build/stack/component.py +++ b/build/stack/component.py @@ -1,5 +1,3 @@ -from ast import dump -from email.mime import base import logging import os import semver @@ -208,8 +206,8 @@ def _preview_mode(self) -> bool: def _skip_checkout(self, obj) -> bool: if obj.get('git_uri') == self._repo_uri() and self._preview_mode(): - return False - return True + return True + return False def _checkout(self, ref, dest, obj): if not self._skip_checkout(obj): @@ -227,6 +225,7 @@ def __init__(self, filepath: str, root: dict = None, args: dict = None): self._website = self.get('website') self._skip_clone = self.get('skip_clone') self._content = f'{self._website.get("path")}/{self._website.get("content")}' + self._examples = {} mkdir_p(self._content) def _persist_commands(self) -> None: @@ -239,6 +238,11 @@ def _persist_groups(self) -> None: logging.info(f'Persisting {self._id} groups: {filepath}') dump_dict(filepath, self._groups) + def _persist_examples(self) -> None: + filepath = f'{self._website.get("path")}/{self._website.get("examples")}' + logging.info(f'Persisting {self._id} examples: {filepath}') + dump_dict(filepath, self._examples) + def _persist_versions(self) -> None: filepath = f'{self._website.get("path")}/{self._website.get("versions")}' logging.info(f'Persisting {self._id} versions: {filepath}') @@ -296,7 +300,7 @@ def _process_docs(self) -> None: md.process_doc(self._commands) def apply(self) -> None: - for kind in ['core', 'docs', 'modules', 'assets']: + for kind in ['core', 'docs', 'modules', 'clients', 'assets']: for component in self.get(kind): if type(component) == str: basename, ext = os.path.splitext(component) @@ -312,6 +316,8 @@ def apply(self) -> None: c = Module(filename, self) else: continue + elif kind == 'clients': + c = Client(filename, self) elif kind == 'assets': c = Asset(filename, self) else: @@ -319,6 +325,7 @@ def apply(self) -> None: c.apply() self._persist_commands() self._persist_groups() + self._persist_examples() self._persist_versions() self._process_commands() self._process_docs() @@ -455,6 +462,45 @@ def apply(self) -> None: self._get_groups() return files +class Client(Component): + def __init__(self, filepath: str, root: dict = None): + super().__init__(filepath, root) + + def _copy_examples(self): + if ex := self.get('examples'): + repo = self._git_clone(ex) + dev_branch = ex.get('dev_branch') + self._checkout(dev_branch, repo, ex) + path = ex.get('path', '') + logging.info(f'Copying {self._id} examples') + src = f'{repo}/{path}/' + dst = f'{self._root._website.get("path")}/{self._root._website.get("examples_path")}' + + _, dirs, _ = next(os.walk(src)) + for d in dirs: + meta = f'{src}/{d}/example.json' + try: + ex = load_dict(meta) + exid = ex.pop('id') + except FileNotFoundError: + logging.warn(f'Example "{meta}" not found for {self._id}: skipping.') + continue + ex['source'] = f'{src}/{d}/{ex.get("file")}' + if not os.path.isfile(ex['source']): + logging.warn(f'"Source {ex.get("source")}" not found for {self._id}: skipping.') + continue + + mkdir_p(f'{dst}/{exid}') + rsync(ex['source'],f'{dst}/{exid}/') + ex['target'] = f'{dst}/{exid}/{ex.get("file")}' + examples = self._root._examples + if not examples.get(exid): + examples[exid] = {} + examples[exid][self.get('language')] = ex + + def apply(self) -> None: + logging.info(f'Applying client {self._id}') + self._copy_examples() class Asset(Component): def __init__(self, filepath: str, root: dict = None): @@ -462,9 +508,7 @@ def __init__(self, filepath: str, root: dict = None): def apply(self) -> None: logging.info(f'Applying asset {self._id}') - if not self._repository: - return repo = self._git_clone(self._repository) dev_branch = self._repository.get('dev_branch') - self._checkout(dev_branch, repo, self._repository) + self._checkout(dev_branch, repo, self._repository) # return Component._dump_payload(repo, './', self._repository.get('payload')) diff --git a/build/stack/example.py b/build/stack/example.py new file mode 100644 index 0000000..e67a60a --- /dev/null +++ b/build/stack/example.py @@ -0,0 +1,60 @@ +import logging +import re + +START_ANCHOR = 'HIDE_START' +END_ANCHOR = 'HIDE_END' +PREFIXES = { + 'python': '#', + 'javascript': '//', +} + +class Example(): + language = None + path = None + content = None + hidden = [] + + def __init__(self, language: str, path: str) -> None: + if not PREFIXES.get(language): + logging.error(f'Unknown language "{language}" for example {path}') + return + self.language = language + self.path = path + with open(path, 'r') as f: + self.content = f.readlines() + + def persist(self, path: str = None) -> None: + if not path: + path = self.path + with open(path,'w') as f: + f.writelines(self.content) + + def make_ranges(self) -> None: + curr = 0 + hidden = None + content = [] + start = re.compile(f'^{PREFIXES[self.language]}\\s?{START_ANCHOR}') + end = re.compile(f'^{PREFIXES[self.language]}\\s?{END_ANCHOR}') + while curr < len(self.content): + l = self.content[curr] + if re.search(start, l): + if hidden is not None: + logging.error(f'Nested hidden anchor in {self.path}:L{curr+1} - aborting.') + return + hidden = len(content) + elif re.search(end, l): + if hidden is None: + logging.error(f'Closing hidden anchor w/o a start in {self.path}:L{curr+1} - aborting.') + return + if len(content) - hidden == 1: + self.hidden.append(f'{hidden+1}') + else: + self.hidden.append(f'{hidden+1}-{len(content)}') + hidden = None + else: + content.append(l) + curr += 1 + if hidden is not None: + logging.error(f'Unclosed hidden anchor in {self.path}:L{hidden+1} - aborting.') + return + self.content = content diff --git a/build/stack/util.py b/build/stack/util.py index d7bf11b..5f718cf 100644 --- a/build/stack/util.py +++ b/build/stack/util.py @@ -105,9 +105,10 @@ def die(msg: str = 'aborting - have a nice day!') -> None: exit(1) -def rsync(src: str, dst: str, exclude: list = ['.*']): +def rsync(src: str, dst: str, exclude: list = ['.*'], include: list = ['*']): ex = [f'"{x}"' for x in exclude] - cmd = f'rsync -av --no-owner --no-group --exclude={{{",".join(ex)}}} {src} {dst}' + ix = [f'"{x}"' for x in include] + cmd = f'rsync -av --no-owner --no-group --include={{{",".join(ix)}}} --exclude={{{",".join(ex)}}} {src} {dst}' ret = run(cmd) return ret.split('\n') diff --git a/config.toml b/config.toml index aaa9c6f..55bbbd7 100644 --- a/config.toml +++ b/config.toml @@ -41,6 +41,8 @@ anchor = "smart" [params] tagManagerId = "GTM-T4MTBKP" + # Display and sort order for client examples + clientsExamples = ["Python", "JavaScript"] # Language configuration diff --git a/data/stack/index.json b/data/stack/index.json index 852b06f..1e1e05c 100644 --- a/data/stack/index.json +++ b/data/stack/index.json @@ -9,6 +9,10 @@ "docs": [ "stack_docs" ], + "clients": [ + "node_redis", + "redis_py" + ], "modules": [ "redisearch", "redisjson", @@ -24,6 +28,8 @@ "website": { "path": "./", "content": "content/en", + "examples": "data/examples.json", + "examples_path": "examples", "commands": "data/commands.json", "groups": "data/groups.json", "versions": "data/versions.json", diff --git a/data/stack/node_redis.json b/data/stack/node_redis.json new file mode 100644 index 0000000..823f6ab --- /dev/null +++ b/data/stack/node_redis.json @@ -0,0 +1,14 @@ +{ + "id": "node_redis", + "type": "client", + "name": "node-redis", + "language": "JavaScript", + "repository": { + "git_uri": "https://github.com/redis/node-redis" + }, + "examples": { + "git_uri": "https://github.com/itamarhaber/node-redis", + "dev_branch": "emb-examples", + "path": "examples" + } +} diff --git a/data/stack/redis_py.json b/data/stack/redis_py.json new file mode 100644 index 0000000..b8392c8 --- /dev/null +++ b/data/stack/redis_py.json @@ -0,0 +1,14 @@ +{ + "id": "redis_py", + "type": "client", + "name": "redis-py", + "language": "Python", + "repository": { + "git_uri": "https://github.com/redis/redis-py" + }, + "examples": { + "git_uri": "https://github.com/itamarhaber/redis-py", + "dev_branch": "emb-examples", + "path": "docs/examples" + } +} diff --git a/data/stack/stack_docs.json b/data/stack/stack_docs.json index c9acc74..aac9892 100644 --- a/data/stack/stack_docs.json +++ b/data/stack/stack_docs.json @@ -5,8 +5,8 @@ "description": "Redis Stack docs", "stack_path": "docs", "docs": { - "git_uri": "https://github.com/redis-stack/redis-stack-docs", - "dev_branch": "main", + "git_uri": "https://github.com/itamarhaber/redis-stack-docs", + "dev_branch": "emb-examples", "path": "docs" }, "misc": { diff --git a/layouts/partials/tabbed-clients-example.html b/layouts/partials/tabbed-clients-example.html new file mode 100644 index 0000000..0d2c752 --- /dev/null +++ b/layouts/partials/tabbed-clients-example.html @@ -0,0 +1,21 @@ +{{ $id := .Scratch.Get "example" }} +{{ if not (isset $.Site.Data.examples $id) }} + {{ warnf "[tabbed-clients-example] Example not found %q for %q" $id $.Page }} +{{ end }} + +{{ $tabs := slice }} +{{ $languages := index $.Site.Data.examples $id }} +{{ range $language := $.Site.Params.clientsexamples }} + {{ $example := index $languages $language }} + {{ $examplePath := index $example "target" }} + {{ $options := printf "linenos=false" }} + {{ if isset $example "highlight" }} + {{ $options = printf "%s,hl_lines=%s" $options (delimit (index $example "highlight") " ") }} + {{ end }} + {{ $params := dict "language" $language "contentPath" $examplePath "options" $options }} + {{ $content := partial "tabs/source.html" $params }} + {{ $tabs = $tabs | append (dict "title" $language "content" $content) }} +{{ end }} + +{{ $params := dict "id" $id "tabs" $tabs }} +{{ partial "tabs/wrapper.html" $params }} diff --git a/layouts/partials/tabs/README.md b/layouts/partials/tabs/README.md new file mode 100644 index 0000000..0a40490 --- /dev/null +++ b/layouts/partials/tabs/README.md @@ -0,0 +1,6 @@ +# Tabs + +Some credits: + +* Radio tabs: https://www.codeinwp.com/snippets/accessible-pure-css-tabs/ +* Hugo scaffold: https://blog.jverkamp.com/2021/01/27/a-tabbed-view-for-hugo/ diff --git a/layouts/partials/tabs/source.html b/layouts/partials/tabs/source.html new file mode 100644 index 0000000..a7dcf0d --- /dev/null +++ b/layouts/partials/tabs/source.html @@ -0,0 +1,3 @@ +{{ $rawContent := readFile .contentPath }} +{{ $content := highlight $rawContent .language .options }} +{{ return $content }} diff --git a/layouts/partials/tabs/wrapper.html b/layouts/partials/tabs/wrapper.html new file mode 100644 index 0000000..479d828 --- /dev/null +++ b/layouts/partials/tabs/wrapper.html @@ -0,0 +1,32 @@ +{{ $id := (or (.id) (substr (.Inner | md5) 0 4)) }} +{{ $tabs := or (.tabs) (dict ("title" "default" "content" .InnerDeindent)) }} + +