Skip to content

Commit

Permalink
[asciidoc] basic toc handling
Browse files Browse the repository at this point in the history
  • Loading branch information
rmannibucau committed Dec 30, 2023
1 parent 808d59c commit e8ed220
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ private Path assetsDir(final Configuration configuration, final String attribute

@Override
public void visitBody(final Body body) {
if (!"none".equals(attr("toc", "toc", "none", state.document.header().attributes()))) {
visitToc(body);
}

state.stackChain(body.children(), () -> Visitor.super.visitBody(body));
}

Expand Down Expand Up @@ -318,17 +322,22 @@ public void visitHeader(final Header header) {
@Override
public void visitSection(final Section element) {
state.stackChain(element.children(), () -> {
final var titleRenderer = new AsciidoctorLikeHtmlRenderer(configuration);
titleRenderer.visitElement(element.title() instanceof Text t && t.options().isEmpty() && t.style().isEmpty() ?
new Text(t.style(), t.value(), Map.of("nowrap", "")) :
element);
final var title = titleRenderer.result();

builder.append(" <div");
writeCommonAttributes(element.options(), c -> "sect" + (element.level() - 1) + (c == null ? "" : (' ' + c)));
if (!element.options().containsKey("id")) {
builder.append(" id=\"").append(IdGenerator.forTitle(title)).append("\"");
}
builder.append(">\n");
builder.append(" <h").append(element.level());
writeCommonAttributes(element.options(), null);
builder.append(">");
final var titleRenderer = new AsciidoctorLikeHtmlRenderer(configuration);
titleRenderer.visitElement(element.title() instanceof Text t && t.options().isEmpty() && t.style().isEmpty() ?
new Text(t.style(), t.value(), Map.of("nowrap", "")) :
element);
builder.append(titleRenderer.result());
builder.append(title);
builder.append("</h").append(element.level()).append(">\n");
builder.append(" <div class=\"sectionbody\">\n");
Visitor.super.visitSection(element);
Expand Down Expand Up @@ -748,6 +757,22 @@ public void visitMacro(final Macro element) {
}
}

protected void visitToc(final Body body) {
final int toclevels = Integer.parseInt(attr("toclevels", "toclevels", "2", state.document.header().attributes()));
if (toclevels < 1) {
return;
}

builder.append(" <div id=\"toc\" class=\"").append(attr("toc-class", "toc-class", "toc", state.document.header().attributes())).append("\">\n");
if (state.document.header().title() != null && !state.document.header().title().isBlank()) {
builder.append(" <div id=\"toctitle\">").append(state.document.header().title()).append("</div>\n");
}
final var toc = new TocVisitor(toclevels, 1);
toc.visitBody(body);
builder.append(toc.result());
builder.append(" </div>\n");
}

// todo: enhance
protected void visitXref(final Macro element) {
var target = element.label();
Expand Down Expand Up @@ -1026,7 +1051,9 @@ public Configuration setAttributes(final Map<String, String> attributes) {
}

private static class State implements AutoCloseable {
private Document document;
private static final Document EMPTY_DOC = new Document(new Header("", null, null, Map.of()), new Body(List.of()));

private Document document = EMPTY_DOC;
private List<Element> currentChain = null;
private boolean hasStem = false;
private boolean sawPreamble = false;
Expand All @@ -1035,7 +1062,7 @@ private static class State implements AutoCloseable {

@Override
public void close() {
document = null;
document = EMPTY_DOC;
currentChain = null;
sawPreamble = false;
inCallOut = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2020 - 2023 - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.asciidoc.renderer.html;

import java.util.regex.Pattern;

import static java.util.Locale.ROOT;

public final class IdGenerator {
private static final Pattern TAGS = Pattern.compile("<[^>]+>");
private static final Pattern FORBIDDEN_CHARS = Pattern.compile("[^\\w]+");

private IdGenerator() {
// no-op
}

public static String forTitle(final String title) {
return "_" + FORBIDDEN_CHARS.matcher(TAGS.matcher(title).replaceAll("").toLowerCase(ROOT)
.replace(" ", "_")
.replace("\n", ""))
.replaceAll("");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2020 - 2023 - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.asciidoc.renderer.html;

import io.yupiik.asciidoc.model.Body;
import io.yupiik.asciidoc.model.Element;
import io.yupiik.asciidoc.model.Section;
import io.yupiik.asciidoc.model.Text;
import io.yupiik.asciidoc.renderer.Visitor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

import static java.util.Locale.ROOT;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.joining;

public class TocVisitor implements Visitor<StringBuilder> {
private final int maxLevel;
private final int currentLevel;
private final Collection<Section> sections = new ArrayList<>();

public TocVisitor(final int toclevels, final int currentLevel) {
this.maxLevel = toclevels;
this.currentLevel = currentLevel;
}

@Override
public void visitSection(final Section element) {
if (element.level() == currentLevel) {
sections.add(element);
}
}

@Override
public StringBuilder result() {
final var builder = new StringBuilder();
if (sections.isEmpty()) {
return builder;
}

builder.append(" <ul class=\"sectlevel").append(currentLevel).append("\">\n");
if (currentLevel == maxLevel) {
builder.append(sections.stream()
.map(it -> {
final var title = title(it.title());
return " <li><a href=\"#" + id(it, title) + "\">" + title + "</a></li>";
})
.collect(joining("\n", "", "\n")));
} else {
builder.append(sections.stream()
.map(it -> {
final var tocVisitor = new TocVisitor(maxLevel, currentLevel + 1);
tocVisitor.visitBody(new Body(it.children()));
final var children = tocVisitor.result().toString();
final var title = title(it.title());
return " <li><a href=\"#" + id(it, title) + "\">" + title + "</a>\n" + children + " </li>";
})
.collect(joining("\n", "", "\n")));
}
builder.append(" </ul>\n");
return builder;
}

private String id(final Section section, final String title) {
return ofNullable(section.options().get("id"))
// todo: better sanitization
.orElseGet(() -> IdGenerator.forTitle(title));
}

private String title(final Element title) {
final var titleRenderer = new AsciidoctorLikeHtmlRenderer(new AsciidoctorLikeHtmlRenderer.Configuration());
titleRenderer.visitElement(title instanceof Text t && t.options().isEmpty() && t.style().isEmpty() ?
new Text(t.style(), t.value(), Map.of("nowrap", "")) :
title);
return titleRenderer.result();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ void metaInPreamble() {
<blockquote>
Blabla. </blockquote>
</div> </div>
<div class="sect1">
<div class="sect1" id="_whats_new">
<h2>What's new?</h2>
<div class="sectionbody">
<div class="ulist">
Expand Down Expand Up @@ -124,7 +124,7 @@ public record Foo() {}
</p>
</div>
</div>
<div class="sect1">
<div class="sect1" id="_second_part">
<h2>Second part</h2>
<div class="sectionbody">
<div class="paragraph">
Expand Down Expand Up @@ -380,7 +380,7 @@ void stem() {
And inline stem:[[[a,b\\],[c,d\\]\\]((n),(k))] too.
""", """
<div class="sect0">
<div class="sect0" id="_some_formulas">
<h1>Some formulas</h1>
<div class="sectionbody">
<div class="stemblock">
Expand Down Expand Up @@ -415,7 +415,7 @@ void embeddedImage(@TempDir final Path work) throws IOException {
.setAttributes(Map.of("noheader", "true", "data-uri", "")));
renderer.visitBody(doc);
assertEquals("""
<div class="sect0">
<div class="sect0" id="_test">
<h1>Test</h1>
<div class="sectionbody">
<div class="imageblock">
Expand All @@ -442,7 +442,7 @@ public record UserId(String name) {}
.setAttributes(Map.of("noheader", "true", "data-uri", "false"/*true would mean we depend on the http url at test time, we don't want that*/)));
renderer.visitBody(doc);
assertEquals("""
<div class="sect0">
<div class="sect0" id="_test">
<h1>Test</h1>
<div class="sectionbody">
Expand Down Expand Up @@ -474,7 +474,7 @@ void ascii2svg() {
'-------------------------'
....
""", """
<div class="sect0">
<div class="sect0" id="_test">
<h1>Test</h1>
<div class="sectionbody">
<img src="data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB3aWR0aD0iMjUycHgiIGhlaWdodD0iMTYwcHgiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CjxkZWZzPgogICAgPGZpbHRlciBpZD0iZHNGaWx0ZXIiIHdpZHRoPSIxNTAlIiBoZWlnaHQ9IjE1MCUiPgogICAgICA8ZmVPZmZzZXQgcmVzdWx0PSJvZmZPdXQiIGluPSJTb3VyY2VHcmFwaGljIiBkeD0iMiIgZHk9IjIiLz4KICAgICAgPGZlQ29sb3JNYXRyaXggcmVzdWx0PSJtYXRyaXhPdXQiIGluPSJvZmZPdXQiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwLjIgMCAwIDAgMCAwIDAuMiAwIDAgMCAwIDAgMC4yIDAgMCAwIDAgMCAxIDAiLz4KICAgICAgPGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iYmx1ck91dCIgaW49Im1hdHJpeE91dCIgc3RkRGV2aWF0aW9uPSIzIi8+CiAgICAgIDxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImJsdXJPdXQiIG1vZGU9Im5vcm1hbCIvPgogICAgPC9maWx0ZXI+CiAgICA8bWFya2VyIGlkPSJpUG9pbnRlciIKICAgICAgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI1IiByZWZZPSI1IgogICAgICBtYXJrZXJVbml0cz0ic3Ryb2tlV2lkdGgiCiAgICAgIG1hcmtlcldpZHRoPSI4IiBtYXJrZXJIZWlnaHQ9IjE1IgogICAgb3JpZW50PSJhdXRvIj4KICAgIDxwYXRoIGQ9Ik0gMTAgMCBMIDEwIDEwIEwgMCA1IHoiIC8+CiAgPC9tYXJrZXI+CiAgPG1hcmtlciBpZD0iUG9pbnRlciIKICAgIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iNSIgcmVmWT0iNSIKICAgIG1hcmtlclVuaXRzPSJzdHJva2VXaWR0aCIKICAgIG1hcmtlcldpZHRoPSI4IiBtYXJrZXJIZWlnaHQ9IjE1IgogICAgb3JpZW50PSJhdXRvIj4KICAgIDxwYXRoIGQ9Ik0gMCAwIEwgMTAgNSBMIDAgMTAgeiIgLz4KICA8L21hcmtlcj4KPC9kZWZzPiAgPGcgaWQ9ImNsb3NlZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiPgogICAgPHBhdGggaWQ9ImNsb3NlZDAiIGZpbGw9IiNmZmYiIGQ9Ik0gNC41IDE4LjAgUSA0LjUgOC4wIDE0LjUgOC4wIEwgMTMuNSA4LjAgIEwgMjIuNSA4LjAgIEwgMzEuNSA4LjAgIEwgNDAuNSA4LjAgIEwgNDkuNSA4LjAgIEwgNTguNSA4LjAgIEwgNjcuNSA4LjAgIEwgNzYuNSA4LjAgIEwgODUuNSA4LjAgIEwgOTQuNSA4LjAgIEwgMTAzLjUgOC4wICBMIDExMi41IDguMCAgTCAxMjEuNSA4LjAgIEwgMTMwLjUgOC4wICBMIDEzOS41IDguMCAgTCAxNDguNSA4LjAgIEwgMTU3LjUgOC4wICBMIDE2Ni41IDguMCAgTCAxNzUuNSA4LjAgIEwgMTg0LjUgOC4wICBMIDE5My41IDguMCAgTCAyMDIuNSA4LjAgIEwgMjExLjUgOC4wICBMIDIyMC41IDguMCAgTCAyMjkuNSA4LjAgIEwgMjI4LjUgOC4wIFEgMjM4LjUgOC4wIDIzOC41IDE4LjAgIEwgMjM4LjUgMjQuMCAgTCAyMzguNSA0MC4wICBMIDIzOC41IDU2LjAgIEwgMjM4LjUgNzIuMCAgTCAyMzguNSA4OC4wICBMIDIzOC41IDEwNC4wICBMIDIzOC41IDEyMC4wICBMIDIzOC41IDEyNi4wIFEgMjM4LjUgMTM2LjAgMjI4LjUgMTM2LjAgIEwgMjI5LjUgMTM2LjAgIEwgMjIwLjUgMTM2LjAgIEwgMjExLjUgMTM2LjAgIEwgMjAyLjUgMTM2LjAgIEwgMTkzLjUgMTM2LjAgIEwgMTg0LjUgMTM2LjAgIEwgMTc1LjUgMTM2LjAgIEwgMTY2LjUgMTM2LjAgIEwgMTU3LjUgMTM2LjAgIEwgMTQ4LjUgMTM2LjAgIEwgMTM5LjUgMTM2LjAgIEwgMTMwLjUgMTM2LjAgIEwgMTIxLjUgMTM2LjAgIEwgMTEyLjUgMTM2LjAgIEwgMTAzLjUgMTM2LjAgIEwgOTQuNSAxMzYuMCAgTCA4NS41IDEzNi4wICBMIDc2LjUgMTM2LjAgIEwgNjcuNSAxMzYuMCAgTCA1OC41IDEzNi4wICBMIDQ5LjUgMTM2LjAgIEwgNDAuNSAxMzYuMCAgTCAzMS41IDEzNi4wICBMIDIyLjUgMTM2LjAgIEwgMTMuNSAxMzYuMCAgTCAxNC41IDEzNi4wIFEgNC41IDEzNi4wIDQuNSAxMjYuMCAgTCA0LjUgMTIwLjAgIEwgNC41IDEwNC4wICBMIDQuNSA4OC4wICBMIDQuNSA3Mi4wICBMIDQuNSA1Ni4wICBMIDQuNSA0MC4wICBMIDQuNSAyNC4wIFoiIC8+CiAgICA8cGF0aCBpZD0iY2xvc2VkMSIgZmlsbD0iI2ZmZiIgZD0iTSAyMi41IDUwLjAgUSAyMi41IDQwLjAgMzIuNSA0MC4wIEwgMzEuNSA0MC4wICBMIDQwLjUgNDAuMCAgTCA0OS41IDQwLjAgIEwgNTguNSA0MC4wICBMIDY3LjUgNDAuMCAgTCA2Ni41IDQwLjAgUSA3Ni41IDQwLjAgNzYuNSA1MC4wICBMIDc2LjUgNTYuMCAgTCA3Ni41IDcyLjAgIEwgNzYuNSA3OC4wIFEgNzYuNSA4OC4wIDY2LjUgODguMCAgTCA2Ny41IDg4LjAgIEwgNTguNSA4OC4wICBMIDQ5LjUgODguMCAgTCA0MC41IDg4LjAgIEwgMzEuNSA4OC4wICBMIDMyLjUgODguMCBRIDIyLjUgODguMCAyMi41IDc4LjAgIEwgMjIuNSA3Mi4wICBMIDIyLjUgNTYuMCBaIiAvPgogICAgPHBhdGggaWQ9ImNsb3NlZDMiIGZpbGw9IiNmZmYiIGQ9Ik0gOTQuNSA1MC4wIFEgOTQuNSA0MC4wIDEwNC41IDQwLjAgTCAxMDMuNSA0MC4wICBMIDExMi41IDQwLjAgIEwgMTIxLjUgNDAuMCAgTCAxMzAuNSA0MC4wICBMIDEzOS41IDQwLjAgIEwgMTM4LjUgNDAuMCBRIDE0OC41IDQwLjAgMTQ4LjUgNTAuMCAgTCAxNDguNSA1Ni4wICBMIDE0OC41IDcyLjAgIEwgMTQ4LjUgNzguMCBRIDE0OC41IDg4LjAgMTM4LjUgODguMCAgTCAxMzkuNSA4OC4wICBMIDEzMC41IDg4LjAgIEwgMTIxLjUgODguMCAgTCAxMTIuNSA4OC4wICBMIDEwMy41IDg4LjAgIEwgMTA0LjUgODguMCBRIDk0LjUgODguMCA5NC41IDc4LjAgIEwgOTQuNSA3Mi4wICBMIDk0LjUgNTYuMCBaIiAvPgogICAgPHBhdGggaWQ9ImNsb3NlZDUiIGZpbGw9IiNmZmYiIGQ9Ik0gMTY2LjUgNTAuMCBRIDE2Ni41IDQwLjAgMTc2LjUgNDAuMCBMIDE3NS41IDQwLjAgIEwgMTg0LjUgNDAuMCAgTCAxOTMuNSA0MC4wICBMIDIwMi41IDQwLjAgIEwgMjExLjUgNDAuMCAgTCAyMTAuNSA0MC4wIFEgMjIwLjUgNDAuMCAyMjAuNSA1MC4wICBMIDIyMC41IDU2LjAgIEwgMjIwLjUgNzIuMCAgTCAyMjAuNSA3OC4wIFEgMjIwLjUgODguMCAyMTAuNSA4OC4wICBMIDIxMS41IDg4LjAgIEwgMjAyLjUgODguMCAgTCAxOTMuNSA4OC4wICBMIDE4NC41IDg4LjAgIEwgMTc1LjUgODguMCAgTCAxNzYuNSA4OC4wIFEgMTY2LjUgODguMCAxNjYuNSA3OC4wICBMIDE2Ni41IDcyLjAgIEwgMTY2LjUgNTYuMCBaIiAvPgogIDwvZz4KICA8ZyBpZD0ibGluZXMiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIj4KICAgIDxwYXRoIGlkPSJvcGVuMiIgZD0iTSAyMi41IDUwLjAgUSAyMi41IDQwLjAgMzIuNSA0MC4wIEwgMzEuNSA0MC4wICBMIDQwLjUgNDAuMCAgTCA0OS41IDQwLjAgIEwgNTguNSA0MC4wICBMIDY3LjUgNDAuMCAgTCA2Ni41IDQwLjAgUSA3Ni41IDQwLjAgNzYuNSA1MC4wICBMIDc2LjUgNTYuMCAgTCA3Ni41IDcyLjAgIEwgNzYuNSA3OC4wIFEgNzYuNSA4OC4wIDY2LjUgODguMCAgTCA2Ny41IDg4LjAgIEwgNjguNSA4OC4wIFEgNTguNSA4OC4wIDU4LjUgNzguMCAgTCA1OC41IDgyLjAgUSA1OC41IDcyLjAgNDguNSA3Mi4wICBMIDQ5LjUgNzIuMCAgTCA1MC41IDcyLjAgUSA0MC41IDcyLjAgNDAuNSA2Mi4wICBMIDQwLjUgNjYuMCBRIDQwLjUgNTYuMCA1MC41IDU2LjAgIEwgNDkuNSA1Ni4wICBMIDQ4LjUgNTYuMCBRIDU4LjUgNTYuMCA1OC41IDQ2LjAgIiAvPgogICAgPHBhdGggaWQ9Im9wZW40IiBtYXJrZXItZW5kPSJ1cmwoI1BvaW50ZXIpIiAgZD0iTSAxNjYuNSA1MC4wIFEgMTY2LjUgNDAuMCAxNzYuNSA0MC4wIEwgMTc1LjUgNDAuMCAgTCAxODQuNSA0MC4wICBMIDE5My41IDQwLjAgIEwgMjAyLjUgNDAuMCAgTCAyMTEuNSA0MC4wICBMIDIxMC41IDQwLjAgUSAyMjAuNSA0MC4wIDIyMC41IDUwLjAgIEwgMjIwLjUgNTYuMCAgTCAyMjAuNSA3Mi4wICBMIDIyMC41IDc4LjAgUSAyMjAuNSA4OC4wIDIxMC41IDg4LjAgIEwgMjExLjUgODguMCAgTCAyMDIuNSA4OC4wICBMIDE5My41IDg4LjAgIEwgMTg0LjUgODguMCAgTCAxNzUuNSA4OC4wICBMIDE3Ni41IDg4LjAgUSAxNjYuNSA4OC4wIDE2Ni41IDc4LjAgIEwgMTY2LjUgNzIuMCAgTCAxNzUuNSA3Mi4wICBMIDE4NC41IDcyLjAgIEwgMTkzLjUgNzIuMCAiIC8+CiAgICA8cGF0aCBpZD0ib3BlbjYiIG1hcmtlci1lbmQ9InVybCgjUG9pbnRlcikiICBkPSJNIDk0LjUgNTYuMCBMIDEwMy41IDU2LjAgIEwgMTEyLjUgNTYuMCAgTCAxMjEuNSA1Ni4wICIgLz4KICAgIDxwYXRoIGlkPSJvcGVuNyIgbWFya2VyLXN0YXJ0PSJ1cmwoI2lQb2ludGVyKSIgIGQ9Ik0gMTkzLjUgNTYuMCBMIDIwMi41IDU2LjAgIEwgMjExLjUgNTYuMCAiIC8+CiAgICA8cGF0aCBpZD0ib3BlbjgiIG1hcmtlci1zdGFydD0idXJsKCNpUG9pbnRlcikiICBkPSJNIDEyMS41IDcyLjAgTCAxMzAuNSA3Mi4wICBMIDEzOS41IDcyLjAgIiAvPgogIDwvZz4KICA8ZyBpZD0idGV4dCIgc3Ryb2tlPSJub25lIiBzdHlsZT0iZm9udC1mYW1pbHk6Q29uc29sYXMsTW9uYWNvLEFub255bW91cyBQcm8sQW5vbnltb3VzLEJpdHN0cmVhbSBTYW5zIE1vbm8sbW9ub3NwYWNlO2ZvbnQtc2l6ZToxNS4ycHgiID4KICAgIDx0ZXh0IGlkPSJvYmo5IiB4PSIzMS41IiB5PSIxMDQuMCIgZmlsbD0iIzAwMCI+YXNjaWk8L3RleHQ+CiAgICA8dGV4dCBpZD0ib2JqMTAiIHg9IjEyMS41IiB5PSIxMDQuMCIgZmlsbD0iIzAwMCI+MjwvdGV4dD4KICAgIDx0ZXh0IGlkPSJvYmoxMSIgeD0iMTg0LjUiIHk9IjEwNC4wIiBmaWxsPSIjMDAwIj5zdmc8L3RleHQ+CiAgPC9nPgo8L3N2Zz4K" alt="a2s">
Expand Down

0 comments on commit e8ed220

Please sign in to comment.