forked from github/kensanata.oddmuse
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88799b3ebc | ||
|
|
4032aa563c | ||
|
|
2b9fd67dbd | ||
|
|
62056409de | ||
|
|
f45f4eb49f | ||
|
|
acd4d42561 | ||
|
|
8375c3a842 | ||
|
|
54138b7998 | ||
|
|
3816567543 | ||
|
|
c6954437ea | ||
|
|
f3725a4938 | ||
|
|
fc8f6b4a42 | ||
|
|
b6109e37ad | ||
|
|
b3b98e2b82 | ||
|
|
74f6a4b314 | ||
|
|
19a9ad3da0 | ||
|
|
2d99025024 | ||
|
|
63370f31d7 | ||
|
|
752daa81b5 | ||
|
|
429ead8c69 | ||
|
|
52f4aad356 | ||
|
|
4abd0a26cf | ||
|
|
c8173cac04 | ||
|
|
be4b141c43 | ||
|
|
f9d6258744 | ||
|
|
66234d7785 | ||
|
|
c67b4a7f12 | ||
|
|
4a94023be2 | ||
|
|
9535f45647 | ||
|
|
8f585bcd29 | ||
|
|
d95d7b0674 | ||
|
|
50fcd7eb0b | ||
|
|
e5b46fe1a4 | ||
|
|
28872646d0 | ||
|
|
d059e09104 | ||
|
|
545cd78805 | ||
|
|
1e7a7d2fa7 | ||
|
|
1b540fc294 | ||
|
|
493ddc233c | ||
|
|
07b3169c5b | ||
|
|
e73707a16f | ||
|
|
5323399bc8 | ||
|
|
13ac083542 | ||
|
|
3dee191328 | ||
|
|
e13524c1d3 | ||
|
|
dc82b7d64f | ||
|
|
d68163ee90 | ||
|
|
349ed2722c | ||
|
|
e01b39edf6 | ||
|
|
783325509a | ||
|
|
2db3736a70 | ||
|
|
22cf00c28f | ||
|
|
3fe2736ad4 | ||
|
|
03b38673f7 | ||
|
|
3590bb96dd | ||
|
|
5051b9602a | ||
|
|
af9da2be34 | ||
|
|
0c17454a0c | ||
|
|
d789bc40f0 | ||
|
|
3a97171320 | ||
|
|
110970f310 | ||
|
|
9986552ffb | ||
|
|
64277e26ed | ||
|
|
b35e867b55 | ||
|
|
67c6db4b03 | ||
|
|
eec5307bc3 | ||
|
|
6a3b9a9916 | ||
|
|
e6880ae469 | ||
|
|
40de3ea9a1 | ||
|
|
93d40b022f | ||
|
|
dec6acf354 | ||
|
|
062cd9b5b9 | ||
|
|
31574e3606 | ||
|
|
ca01d9d3d6 | ||
|
|
9c69322289 | ||
|
|
c13841e30a | ||
|
|
9ae1ff22c7 | ||
|
|
8db5a45dcd | ||
|
|
b3e2485cd0 | ||
|
|
6c135be248 | ||
|
|
b5a4af9656 | ||
|
|
b23f1d777b | ||
|
|
2f49adf605 | ||
|
|
76f9eb7945 | ||
|
|
0c2718ca8c | ||
|
|
7109c5be9c | ||
|
|
e9fad88a10 |
544
css/alex-2017.css
Normal file
544
css/alex-2017.css
Normal file
@@ -0,0 +1,544 @@
|
||||
/* This file is in the public domain. */
|
||||
html{ text-align: center; }
|
||||
|
||||
body, rss {
|
||||
font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif;
|
||||
font-style: normal;
|
||||
font-size: 14pt;
|
||||
padding: 1em 3em;
|
||||
max-width: 72ex;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@import url(file:///home/alex/alexschroeder.ch/css/alex-2017.css) print;
|
||||
|
||||
@media print {
|
||||
body {
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
/* hide all the crap */
|
||||
div.diff, div.diff+hr, div.refer, div.near, div.definition, div.sister,
|
||||
div.cal, div.footer, span.specialdays, span.gotobar, a.edit, a.number span,
|
||||
div.rc form, form.tiny, p.comment, p#plus1, div.g-plusone, div.content a.feed {
|
||||
display:none;
|
||||
}
|
||||
div.content a.book,
|
||||
div.content a.movie {
|
||||
text-decoration: none;
|
||||
}
|
||||
a cite {
|
||||
font-style: italic;
|
||||
}
|
||||
img[alt="RSS"] { display: none }
|
||||
a.rss { font-size: 8pt }
|
||||
}
|
||||
|
||||
/* headings: we can use larger sizes if we use a lighter color.
|
||||
we cannot inherit the font-family because header and footer use a narrow font. */
|
||||
|
||||
h1, h2, h3, title {
|
||||
font-family: inherit;
|
||||
font-weight: normal;
|
||||
}
|
||||
h1, channel title {
|
||||
font-size: 32pt;
|
||||
margin: 1em 0 0.5em 0;
|
||||
padding: 0.4em 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 18pt;
|
||||
margin: 2em 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
h3 {
|
||||
font-size: inherit;
|
||||
font-weight: bold;
|
||||
padding: 0;
|
||||
margin: 1em 0 0 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* headers in the journal are smaller */
|
||||
|
||||
div.journal h1, item title {
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
clear: both;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
div.journal h2 {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
div.journal h3 {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
font-style: italic;
|
||||
}
|
||||
div.journal hr {
|
||||
visibility: hidden;
|
||||
}
|
||||
p.more {
|
||||
margin-top: 3em;
|
||||
}
|
||||
/* Links in headings appear on journal pages. */
|
||||
|
||||
h1 a, h2 a, h3 a {
|
||||
color:inherit;
|
||||
text-decoration:none;
|
||||
font-weight: normal;
|
||||
}
|
||||
h1 a:visited, h2 a:visited, h3 a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* for download buttons and the like */
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
font-size: 120%;
|
||||
cursor: pointer;
|
||||
padding: 0.4em 0.6em;
|
||||
text-shadow: 0px -1px 0px #ccc;
|
||||
background-color: #cfa;
|
||||
border: 1px solid #9d8;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 1px 3px white inset, 0px 1px 3px black;
|
||||
}
|
||||
|
||||
.button .icon {
|
||||
color: #363;
|
||||
text-shadow: 0px -1px 1px white, 0px 1px 3px #666;
|
||||
}
|
||||
|
||||
.button a {
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* links */
|
||||
|
||||
a.pencil {
|
||||
padding-left: 1ex;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
visibility: hidden;
|
||||
transition: visibility 0s 1s, opacity 1s linear;
|
||||
opacity: 0;
|
||||
}
|
||||
*:hover > a.pencil {
|
||||
visibility: visible;
|
||||
transition: opacity .5s linear;
|
||||
opacity: 1;
|
||||
}
|
||||
@media print {
|
||||
a.pencil {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.number {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* stop floating content from flowing over the footer */
|
||||
|
||||
hr {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* the distance between links in the navigation bars */
|
||||
|
||||
span.bar a {
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* search box in the top bar */
|
||||
|
||||
.header form, .header p {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
label[for="searchlang"], #searchlang, .header input[type="submit"] {
|
||||
/* don't use display: none! http://stackoverflow.com/questions/5665203/getting-iphone-go-button-to-submit-form */
|
||||
visibility: hidden; position: absolute;
|
||||
}
|
||||
/* wrap on the iphone */
|
||||
@media only screen and (max-device-width: 480px) {
|
||||
}
|
||||
|
||||
.header input {
|
||||
width: 10ex;
|
||||
}
|
||||
|
||||
/* other form fields */
|
||||
|
||||
input[type="text"] {
|
||||
padding: 0;
|
||||
font-size: 80%;
|
||||
line-height: 125%;
|
||||
}
|
||||
|
||||
/* code */
|
||||
|
||||
textarea, pre, code, tt {
|
||||
font-family: "Andale Mono", Monaco, "Courier New", Courier, monospace, "Symbola";
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow:hidden;
|
||||
white-space: pre-wrap; /* CSS 3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
|
||||
/* styling for divs that will be invisible when printing
|
||||
when printing. */
|
||||
|
||||
div.header, div.footer, div.near, div.definition, p.comment, a.tag {
|
||||
|
||||
font-size: 14pt;
|
||||
}
|
||||
@media print {
|
||||
div.header, div.footer, div.near, div.definition, p.comment, a.tag {
|
||||
font-size: 8pt;
|
||||
}
|
||||
}
|
||||
|
||||
div.footer form.search {
|
||||
display: none;
|
||||
}
|
||||
div.rc li + li {
|
||||
margin-top: 1em;
|
||||
}
|
||||
div.rc li strong, table.history strong, strong.description {
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
div.diff {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
font-size: 12pt;
|
||||
color: #000;
|
||||
|
||||
}
|
||||
div.old {
|
||||
background-color: #ffffaf;
|
||||
}
|
||||
div.new {
|
||||
background-color: #cfffcf;
|
||||
}
|
||||
|
||||
div.refer {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
div.message {
|
||||
background-color:#fee;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
img.xml {
|
||||
border:none;
|
||||
padding:1px;
|
||||
}
|
||||
a.small img {
|
||||
max-width:300px;
|
||||
}
|
||||
a.large img {
|
||||
max-width:600px;
|
||||
}
|
||||
div.sister {
|
||||
margin-right:1ex;
|
||||
background-color:inherit;
|
||||
}
|
||||
div.sister p {
|
||||
margin-top:0;
|
||||
}
|
||||
div.sister hr {
|
||||
display:none;
|
||||
}
|
||||
div.sister img {
|
||||
border:none;
|
||||
}
|
||||
|
||||
div.near, div.definition {
|
||||
background-color:#efe;
|
||||
}
|
||||
|
||||
div.sidebar {
|
||||
float:right;
|
||||
border:1px dotted #000;
|
||||
padding:0 1em;
|
||||
}
|
||||
div.sidebar ul {
|
||||
padding-left:1em;
|
||||
}
|
||||
|
||||
/* replacements, features */
|
||||
|
||||
ins {
|
||||
font-style: italic;
|
||||
text-decoration: none;
|
||||
}
|
||||
acronym, abbr {
|
||||
letter-spacing:0.1em;
|
||||
font-variant:small-caps;
|
||||
}
|
||||
|
||||
/* Interlink prefix not shown */
|
||||
a .site, a .separator {
|
||||
display: none;
|
||||
}
|
||||
a cite { font:inherit; }
|
||||
/* browser borkage */
|
||||
textarea[name="text"] { width:97%; height:80%; }
|
||||
textarea[name="summary"] { width:97%; height:3em; }
|
||||
/* comments */
|
||||
textarea[name="aftertext"] { width:97%; height:10em; }
|
||||
div.commentshown {
|
||||
font-size: 12pt;
|
||||
padding: 2em 0;
|
||||
}
|
||||
div.commenthidden {
|
||||
display:none;
|
||||
}
|
||||
div.commentshown {
|
||||
display:block;
|
||||
}
|
||||
p.comment {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.comment {
|
||||
font-size: 14pt;
|
||||
}
|
||||
div.comment h2 {
|
||||
margin-top: 5em;
|
||||
}
|
||||
/* comment pages with username, homepage, and email subscription */
|
||||
.comment form span { display: block; }
|
||||
.comment form span label { display: inline-block; width: 10em; }
|
||||
/* IE sucks */
|
||||
.comment input#username,
|
||||
.comment input#homepage,
|
||||
.comment input#mail { width: 20em; }
|
||||
|
||||
/* cal */
|
||||
div.month { padding:0; margin:0 2ex; }
|
||||
body > div.month {
|
||||
float:right;
|
||||
background-color: inherit;
|
||||
border:solid thin;
|
||||
padding:0 1ex;
|
||||
}
|
||||
.year > .month {
|
||||
float:left;
|
||||
}
|
||||
.footer {
|
||||
clear:both;
|
||||
}
|
||||
.month .title a.local {
|
||||
background-color: inherit;
|
||||
}
|
||||
.month a.local {
|
||||
background-color: #ddf;
|
||||
}
|
||||
.month a.today {
|
||||
background-color: #fdd;
|
||||
}
|
||||
.month a {
|
||||
color:inherit;
|
||||
font-weight:inherit;
|
||||
text-decoration: none;
|
||||
background-color: #eee;
|
||||
}
|
||||
/* history tables and other tables */
|
||||
table.history {
|
||||
border: none;
|
||||
}
|
||||
td.history {
|
||||
border: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border: none;
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 1em;
|
||||
margin: 1em 2em;
|
||||
}
|
||||
table tr td, table tr th {
|
||||
border: none;
|
||||
padding: 0.2em 0.5em;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.arab tr th {
|
||||
font-weight:normal;
|
||||
text-align:left;
|
||||
vertical-align:top;
|
||||
}
|
||||
table.arab, table.arab tr th, table.arab tr td {
|
||||
border:none;
|
||||
}
|
||||
th.nobreak {
|
||||
white-space:nowrap;
|
||||
}
|
||||
table.full { width:99%; margin-left:1px; }
|
||||
table.j td, table.j th, table tr td.j, table tr th.j, .j { text-align:justify; }
|
||||
table.l td, table.l th, table tr td.l, table tr th.l, .l { text-align:left; }
|
||||
table.r td, table.r th, table tr td.r, table tr th.r, .r { text-align:right; }
|
||||
table.c td, table.c th, table tr td.c, table tr th.c, .c { text-align:center; }
|
||||
table.t td { vertical-align: top; }
|
||||
td.half { width:50%; }
|
||||
td.third { width:33%; }
|
||||
|
||||
form table td { padding:5px; }
|
||||
|
||||
/* lists */
|
||||
dd { padding-bottom:0.5ex; }
|
||||
dl.inside dt { float:left; }
|
||||
/* search */
|
||||
div.search span.result { font-size:larger; }
|
||||
div.search span.info { font-size:smaller; font-style:italic; }
|
||||
div.search p.result { display:none; }
|
||||
|
||||
img.logo {
|
||||
float: right;
|
||||
margin: 0 0 0 1ex;
|
||||
padding: 0;
|
||||
border: 1px solid #000;
|
||||
opacity: 0.3;
|
||||
background-color:#ffe;
|
||||
}
|
||||
|
||||
/* images */
|
||||
|
||||
div.content a.feed img, div.journal a.feed img,
|
||||
div.content a img.smiley, div.journal a img.smiley, img.smiley,
|
||||
div.content a.inline img, div.journal a.inline img,
|
||||
div.content li a.image img, div.journal li a.image img {
|
||||
margin: 0; padding: 0; border: none;
|
||||
}
|
||||
div.image a img {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.image span.caption {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.left { float:left; margin-right: 1em; }
|
||||
.right { float:right; margin-left: 1em; }
|
||||
.half img { height: 50%; width: 50%; }
|
||||
.face img { width: 200px; }
|
||||
div.left .left, div.right .right {
|
||||
float:none;
|
||||
}
|
||||
.center { text-align:center; }
|
||||
table.aside {
|
||||
float:right;
|
||||
width:40%;
|
||||
margin-left: 1em;
|
||||
padding: 1ex;
|
||||
border: 1px dotted #666;
|
||||
}
|
||||
table.aside td {
|
||||
text-align:left;
|
||||
}
|
||||
div.sidebar {
|
||||
float:right; width: 250px;
|
||||
text-align: right;
|
||||
border: none;
|
||||
margin: 1ex;
|
||||
}
|
||||
|
||||
.bigsidebar {
|
||||
float:right;
|
||||
width: 500px;
|
||||
border: none;
|
||||
margin-left: 1ex;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
dl.irc dt { width:20ex; float:left; text-align:right; clear:left; }
|
||||
dl.irc dt span.time { float:left; }
|
||||
dl.irc dd { margin-left:22ex; }
|
||||
|
||||
/* portrait */
|
||||
|
||||
div.footer, div.comment, hr { clear: both; }
|
||||
.portrait { float: left; font-size: small; margin-right: 1em; }
|
||||
.portrait a { color: #999; }
|
||||
|
||||
div.left { float:left; margin:1em; padding: 0.5em; }
|
||||
div.left p { display:table-cell; }
|
||||
div.left p + p { display:table-caption; caption-side:bottom; }
|
||||
|
||||
p.table a { float:left; width:20ex; }
|
||||
p.table + p { clear:both; }
|
||||
|
||||
/* mastodon */
|
||||
|
||||
div.mastodon { padding: 0 2em }
|
||||
div.mastodon .status {padding-top: 1ex; border-bottom: 1px solid grey;}
|
||||
div.mastodon .status:first-child {border-top: 1px solid grey;}
|
||||
|
||||
/* rss */
|
||||
|
||||
channel * { display: block; }
|
||||
|
||||
channel title {
|
||||
margin-top: 30pt;
|
||||
}
|
||||
copyright {
|
||||
font-size: 14pt;
|
||||
margin-top: 1em;
|
||||
}
|
||||
channel:before {
|
||||
font-size: 14pt;
|
||||
display: block;
|
||||
margin: 1em;
|
||||
padding: 0.5em;
|
||||
content: "This document is to be read in a feed reader. The item content is escaped HTML, which makes it hard to read for humans. Sorry!";
|
||||
color: red;
|
||||
border: 1px solid red;
|
||||
}
|
||||
license {
|
||||
font-size: 11pt;
|
||||
margin-bottom: 9pt;
|
||||
}
|
||||
contributor:before { content: "Last edited by "; }
|
||||
contributor:after { content: "."; }
|
||||
generator:before { content: "Feed generated by "; }
|
||||
generator:after { content: "."; }
|
||||
channel description {
|
||||
font-weight: bold;
|
||||
}
|
||||
item description {
|
||||
font-weight: normal;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
link, managingEditor, webMaster, license, url,
|
||||
docs, language,
|
||||
pubDate, lastBuildDate, ttl, guid, category, comments,
|
||||
docs, image title, image link,
|
||||
status, version, diff, history, importance {
|
||||
display: none;
|
||||
}
|
||||
562
css/alex-2018.css
Normal file
562
css/alex-2018.css
Normal file
@@ -0,0 +1,562 @@
|
||||
/* This file is in the public domain. */
|
||||
html{ text-align: center; }
|
||||
|
||||
body, rss {
|
||||
font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif;
|
||||
font-style: normal;
|
||||
font-size: 14pt;
|
||||
padding: 1em 3em;
|
||||
max-width: 72ex;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 480px) {
|
||||
body {
|
||||
padding: 1ex;
|
||||
}
|
||||
}
|
||||
|
||||
@import url(file:///home/alex/alexschroeder.ch/css/alex-2017.css) print;
|
||||
|
||||
@media print {
|
||||
body {
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
/* hide all the crap */
|
||||
div.diff, div.diff+hr, div.refer, div.near, div.definition, div.sister,
|
||||
div.cal, div.footer, span.specialdays, span.gotobar, a.edit, a.number span,
|
||||
div.rc form, form.tiny, p.comment, p#plus1, div.g-plusone, div.content a.feed {
|
||||
display:none;
|
||||
}
|
||||
div.content a.book,
|
||||
div.content a.movie {
|
||||
text-decoration: none;
|
||||
}
|
||||
a cite {
|
||||
font-style: italic;
|
||||
}
|
||||
img[alt="RSS"] { display: none }
|
||||
a.rss { font-size: 8pt }
|
||||
}
|
||||
|
||||
/* headings: we can use larger sizes if we use a lighter color.
|
||||
we cannot inherit the font-family because header and footer use a narrow font. */
|
||||
|
||||
h1, h2, h3, title {
|
||||
font-family: inherit;
|
||||
font-weight: normal;
|
||||
}
|
||||
h1, channel title {
|
||||
font-size: 32pt;
|
||||
margin: 1em 0 0.5em 0;
|
||||
padding: 0.4em 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 18pt;
|
||||
margin: 2em 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
h3 {
|
||||
font-size: inherit;
|
||||
font-weight: bold;
|
||||
padding: 0;
|
||||
margin: 1em 0 0 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* headers in the journal are smaller */
|
||||
|
||||
div.journal h1, item title {
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
clear: both;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
div.journal h2 {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
div.journal h3 {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
font-style: italic;
|
||||
}
|
||||
div.journal hr {
|
||||
visibility: hidden;
|
||||
}
|
||||
p.more {
|
||||
margin-top: 3em;
|
||||
}
|
||||
/* Links in headings appear on journal pages. */
|
||||
|
||||
h1 a, h2 a, h3 a {
|
||||
color:inherit;
|
||||
text-decoration:none;
|
||||
font-weight: normal;
|
||||
}
|
||||
h1 a:visited, h2 a:visited, h3 a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* for download buttons and the like */
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
font-size: 120%;
|
||||
cursor: pointer;
|
||||
padding: 0.4em 0.6em;
|
||||
text-shadow: 0px -1px 0px #ccc;
|
||||
background-color: #cfa;
|
||||
border: 1px solid #9d8;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 1px 3px white inset, 0px 1px 3px black;
|
||||
}
|
||||
|
||||
.button .icon {
|
||||
color: #363;
|
||||
text-shadow: 0px -1px 1px white, 0px 1px 3px #666;
|
||||
}
|
||||
|
||||
.button a {
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* links */
|
||||
|
||||
a.pencil {
|
||||
padding-left: 1ex;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
visibility: hidden;
|
||||
transition: visibility 0s 1s, opacity 1s linear;
|
||||
opacity: 0;
|
||||
}
|
||||
*:hover > a.pencil {
|
||||
visibility: visible;
|
||||
transition: opacity .5s linear;
|
||||
opacity: 1;
|
||||
}
|
||||
@media print {
|
||||
a.pencil {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.number {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* stop floating content from flowing over the footer */
|
||||
|
||||
hr {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* the distance between links in the navigation bars */
|
||||
|
||||
span.bar a {
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* search box in the top bar */
|
||||
|
||||
.header form, .header p {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
label[for="searchlang"], #searchlang, .header input[type="submit"] {
|
||||
/* don't use display: none! http://stackoverflow.com/questions/5665203/getting-iphone-go-button-to-submit-form */
|
||||
visibility: hidden; position: absolute;
|
||||
}
|
||||
|
||||
.header input {
|
||||
width: 10ex;
|
||||
}
|
||||
|
||||
/* other form fields */
|
||||
|
||||
input[type="text"] {
|
||||
padding: 0;
|
||||
font-size: 80%;
|
||||
line-height: 125%;
|
||||
}
|
||||
|
||||
/* code */
|
||||
|
||||
textarea, pre, code, tt {
|
||||
font-family: "Andale Mono", Monaco, "Courier New", Courier, monospace, "Symbola";
|
||||
font-size: 75%; /* fits 80ex */
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow:hidden;
|
||||
white-space: pre-wrap; /* CSS 3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
|
||||
/* styling for divs that will be invisible when printing
|
||||
when printing. */
|
||||
|
||||
div.header, div.footer, div.near, div.definition, p.comment, a.tag {
|
||||
|
||||
font-size: 14pt;
|
||||
}
|
||||
@media print {
|
||||
div.header, div.footer, div.near, div.definition, p.comment, a.tag {
|
||||
font-size: 8pt;
|
||||
}
|
||||
}
|
||||
|
||||
div.footer form.search {
|
||||
display: none;
|
||||
}
|
||||
div.rc {
|
||||
overflow: hidden;
|
||||
}
|
||||
div.rc li + li {
|
||||
margin-top: 1em;
|
||||
}
|
||||
div.rc li strong, table.history strong, strong.description {
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
div.diff {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
font-size: 12pt;
|
||||
color: #000;
|
||||
|
||||
}
|
||||
div.old {
|
||||
background-color: #ffffaf;
|
||||
}
|
||||
div.new {
|
||||
background-color: #cfffcf;
|
||||
}
|
||||
|
||||
div.refer {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
div.message {
|
||||
background-color:#fee;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
img.xml {
|
||||
border:none;
|
||||
padding:1px;
|
||||
}
|
||||
a.small img {
|
||||
max-width:300px;
|
||||
}
|
||||
a.large img {
|
||||
max-width:600px;
|
||||
}
|
||||
div.sister {
|
||||
margin-right:1ex;
|
||||
background-color:inherit;
|
||||
}
|
||||
div.sister p {
|
||||
margin-top:0;
|
||||
}
|
||||
div.sister hr {
|
||||
display:none;
|
||||
}
|
||||
div.sister img {
|
||||
border:none;
|
||||
}
|
||||
|
||||
div.near, div.definition {
|
||||
background-color:#efe;
|
||||
}
|
||||
|
||||
div.sidebar {
|
||||
float:right;
|
||||
border:1px dotted #000;
|
||||
padding:0 1em;
|
||||
}
|
||||
div.sidebar ul {
|
||||
padding-left:1em;
|
||||
}
|
||||
|
||||
/* replacements, features */
|
||||
|
||||
ins {
|
||||
font-style: italic;
|
||||
text-decoration: none;
|
||||
}
|
||||
acronym, abbr {
|
||||
letter-spacing:0.1em;
|
||||
font-variant:small-caps;
|
||||
}
|
||||
|
||||
/* Interlink prefix not shown */
|
||||
a .site, a .separator {
|
||||
display: none;
|
||||
}
|
||||
a cite { font:inherit; }
|
||||
/* browser borkage */
|
||||
textarea[name="text"] { width:97%; height:80%; }
|
||||
textarea[name="summary"] { width:97%; height:3em; }
|
||||
/* comments */
|
||||
textarea[name="aftertext"] { width:97%; height:10em; }
|
||||
div.commentshown {
|
||||
font-size: 12pt;
|
||||
padding: 2em 0;
|
||||
}
|
||||
div.commenthidden {
|
||||
display:none;
|
||||
}
|
||||
div.commentshown {
|
||||
display:block;
|
||||
}
|
||||
p.comment {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.comment {
|
||||
font-size: 14pt;
|
||||
}
|
||||
div.comment h2 {
|
||||
margin-top: 5em;
|
||||
}
|
||||
/* comment pages with username, homepage, and email subscription */
|
||||
.comment form span { display: block; }
|
||||
.comment form span label { display: inline-block; width: 10em; }
|
||||
/* IE sucks */
|
||||
.comment input#username,
|
||||
.comment input#homepage,
|
||||
.comment input#mail { width: 20em; }
|
||||
|
||||
/* cal */
|
||||
div.month { padding:0; margin:0 2ex; }
|
||||
body > div.month {
|
||||
float:right;
|
||||
background-color: inherit;
|
||||
border:solid thin;
|
||||
padding:0 1ex;
|
||||
}
|
||||
.year > .month {
|
||||
float:left;
|
||||
}
|
||||
.footer {
|
||||
clear:both;
|
||||
}
|
||||
.month .title a.local {
|
||||
background-color: inherit;
|
||||
}
|
||||
.month a.local {
|
||||
background-color: #ddf;
|
||||
}
|
||||
.month a.today {
|
||||
background-color: #fdd;
|
||||
}
|
||||
.month a {
|
||||
color:inherit;
|
||||
font-weight:inherit;
|
||||
text-decoration: none;
|
||||
background-color: #eee;
|
||||
}
|
||||
/* history tables and other tables */
|
||||
table.history {
|
||||
border: none;
|
||||
}
|
||||
td.history {
|
||||
border: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border: none;
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 1em;
|
||||
margin: 1em 2em;
|
||||
}
|
||||
table tr td, table tr th {
|
||||
border: none;
|
||||
padding: 0.2em 0.5em;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.arab tr th {
|
||||
font-weight:normal;
|
||||
text-align:left;
|
||||
vertical-align:top;
|
||||
}
|
||||
table.arab, table.arab tr th, table.arab tr td {
|
||||
border:none;
|
||||
}
|
||||
th.nobreak {
|
||||
white-space:nowrap;
|
||||
}
|
||||
table.full { width:99%; margin-left:1px; }
|
||||
table.j td, table.j th, table tr td.j, table tr th.j, .j { text-align:justify; }
|
||||
table.l td, table.l th, table tr td.l, table tr th.l, .l { text-align:left; }
|
||||
table.r td, table.r th, table tr td.r, table tr th.r, .r { text-align:right; }
|
||||
table.c td, table.c th, table tr td.c, table tr th.c, .c { text-align:center; }
|
||||
table.t td { vertical-align: top; }
|
||||
td.half { width:50%; }
|
||||
td.third { width:33%; }
|
||||
|
||||
form table td { padding:5px; }
|
||||
|
||||
/* lists */
|
||||
dd { padding-bottom:0.5ex; }
|
||||
dl.inside dt { float:left; }
|
||||
/* search */
|
||||
div.search span.result { font-size:larger; }
|
||||
div.search span.info { font-size:smaller; font-style:italic; }
|
||||
div.search p.result { display:none; }
|
||||
|
||||
img.logo {
|
||||
float: right;
|
||||
margin: 0 0 0 1ex;
|
||||
padding: 0;
|
||||
border: 1px solid #000;
|
||||
opacity: 0.3;
|
||||
background-color:#ffe;
|
||||
}
|
||||
|
||||
/* images */
|
||||
|
||||
div.content a.feed img, div.journal a.feed img,
|
||||
div.content a img.smiley, div.journal a img.smiley, img.smiley,
|
||||
div.content a.inline img, div.journal a.inline img,
|
||||
div.content li a.image img, div.journal li a.image img {
|
||||
margin: 0; padding: 0; border: none;
|
||||
}
|
||||
div.image a img {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.image span.caption {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.left { float:left; margin-right: 1em; }
|
||||
.right { float:right; margin-left: 1em; }
|
||||
.half img { height: 50%; width: 50%; }
|
||||
.face img { width: 200px; }
|
||||
div.left .left, div.right .right {
|
||||
float:none;
|
||||
}
|
||||
.center { text-align:center; }
|
||||
table.aside {
|
||||
float:right;
|
||||
width:40%;
|
||||
margin-left: 1em;
|
||||
padding: 1ex;
|
||||
border: 1px dotted #666;
|
||||
}
|
||||
table.aside td {
|
||||
text-align:left;
|
||||
}
|
||||
div.sidebar {
|
||||
float:right; width: 250px;
|
||||
text-align: right;
|
||||
border: none;
|
||||
margin: 1ex;
|
||||
}
|
||||
|
||||
.bigsidebar {
|
||||
float:right;
|
||||
width: 500px;
|
||||
border: none;
|
||||
margin-left: 1ex;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
dl.irc dt { width:20ex; float:left; text-align:right; clear:left; }
|
||||
dl.irc dt span.time { float:left; }
|
||||
dl.irc dd { margin-left:22ex; }
|
||||
|
||||
/* portrait */
|
||||
|
||||
div.footer, div.comment, hr { clear: both; }
|
||||
.portrait { float: left; font-size: small; margin-right: 1em; }
|
||||
.portrait a { color: #999; }
|
||||
|
||||
div.left { float:left; margin:1em; padding: 0.5em; }
|
||||
div.left p { display:table-cell; }
|
||||
div.left p + p { display:table-caption; caption-side:bottom; }
|
||||
|
||||
p.table a { float:left; width:20ex; }
|
||||
p.table + p { clear:both; }
|
||||
|
||||
/* mastodon */
|
||||
|
||||
div.mastodon { padding: 0 2em }
|
||||
div.mastodon .status {padding-top: 1ex; border-bottom: 1px solid grey;}
|
||||
div.mastodon .status:first-child {border-top: 1px solid grey;}
|
||||
|
||||
/* terminal "screenshots" */
|
||||
|
||||
.terminal {
|
||||
width: 80%;
|
||||
margin: 50px auto 100px auto;
|
||||
padding: 5px;
|
||||
font-size: 62%; /* fits 80ex */
|
||||
border: 1px solid #999;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 25px 50px #999;
|
||||
}
|
||||
|
||||
/* rss */
|
||||
|
||||
channel * { display: block; }
|
||||
|
||||
channel title {
|
||||
margin-top: 30pt;
|
||||
}
|
||||
copyright {
|
||||
font-size: 14pt;
|
||||
margin-top: 1em;
|
||||
}
|
||||
channel:before {
|
||||
font-size: 14pt;
|
||||
display: block;
|
||||
margin: 1em;
|
||||
padding: 0.5em;
|
||||
content: "This document is to be read in a feed reader. The item content is escaped HTML, which makes it hard to read for humans. Sorry!";
|
||||
color: red;
|
||||
border: 1px solid red;
|
||||
}
|
||||
license {
|
||||
font-size: 11pt;
|
||||
margin-bottom: 9pt;
|
||||
}
|
||||
contributor:before { content: "Last edited by "; }
|
||||
contributor:after { content: "."; }
|
||||
generator:before { content: "Feed generated by "; }
|
||||
generator:after { content: "."; }
|
||||
channel description {
|
||||
font-weight: bold;
|
||||
}
|
||||
item description {
|
||||
font-weight: normal;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
link, managingEditor, webMaster, license, url,
|
||||
docs, language,
|
||||
pubDate, lastBuildDate, ttl, guid, category, comments,
|
||||
docs, image title, image link,
|
||||
status, version, diff, history, importance {
|
||||
display: none;
|
||||
}
|
||||
@@ -48,12 +48,6 @@ sub MarkdownRule {
|
||||
return CloseHtmlEnvironments()
|
||||
. AddHtmlEnvironment("p");
|
||||
}
|
||||
# setext headers
|
||||
elsif ($bol and m/\G((\s*\n)*(.+?)[ \t]*\n(-+|=+)[ \t]*\n)/cg) {
|
||||
return CloseHtmlEnvironments()
|
||||
. (substr($4,0,1) eq '=' ? $q->h2($3) : $q->h3($3))
|
||||
. AddHtmlEnvironment('p');
|
||||
}
|
||||
# > blockquote
|
||||
# with continuation
|
||||
elsif ($bol and m/\G>/cg) {
|
||||
@@ -65,8 +59,8 @@ sub MarkdownRule {
|
||||
return CloseHtmlEnvironments() . $q->pre($1)
|
||||
. AddHtmlEnvironment("p");
|
||||
}
|
||||
# ` = code
|
||||
elsif (m/\G`([^`].*?)`/cg) {
|
||||
# ` = code may not start with a newline
|
||||
elsif (m/\G`([^\n`][^`]*)`/cg) {
|
||||
return $q->code($1);
|
||||
}
|
||||
# ***bold and italic***
|
||||
@@ -92,8 +86,8 @@ sub MarkdownRule {
|
||||
return AddOrCloseHtmlEnvironment('del');
|
||||
}
|
||||
# indented lists = nested lists
|
||||
elsif ($bol and m/\G(\s*\n)*()([*-]|\d+\.)[ \t]*/cg
|
||||
or InElement('li') && m/\G(\s*\n)+( *)([*-]|\d+\.)[ \t]*/cg) {
|
||||
elsif ($bol and m/\G(\s*\n)*()([*-]|\d+\.)[ \t]+/cg
|
||||
or InElement('li') && m/\G(\s*\n)+( *)([*-]|\d+\.)[ \t]+/cg) {
|
||||
my $nesting_goal = int(length($2)/4) + 1;
|
||||
my $tag = ($3 eq '*' or $3 eq '-') ? 'ul' : 'ol';
|
||||
my $nesting_current = 0;
|
||||
@@ -176,6 +170,12 @@ sub MarkdownRule {
|
||||
$params{-title} = $title if $title;
|
||||
return $q->a(\%params, $text);
|
||||
}
|
||||
# setext headers (must come after block quotes)
|
||||
elsif ($bol and m/\G((\s*\n)*(.+?)[ \t]*\n(-+|=+)[ \t]*\n)/cg) {
|
||||
return CloseHtmlEnvironments()
|
||||
. (substr($4,0,1) eq '=' ? $q->h2($3) : $q->h3($3))
|
||||
. AddHtmlEnvironment('p');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ sub MarkdownExtraRule {
|
||||
elsif (InElement('em', 'style="font-style: normal; text-decoration: underline"') and m/\G_/cg) {
|
||||
return CloseHtmlEnvironment('em');
|
||||
}
|
||||
elsif ($bol and m/\G_/cg or m/\G(?<=\P{Word})_/cg) {
|
||||
elsif ($bol and m/\G_/cg or m/\G(?<=\P{Word})_(?=\S)/cg) {
|
||||
return AddHtmlEnvironment('em', 'style="font-style: normal; text-decoration: underline"');
|
||||
}
|
||||
# //italic//
|
||||
@@ -201,7 +201,7 @@ sub MarkdownExtraRule {
|
||||
elsif (InElement('em') and m/\G\//cg) {
|
||||
return CloseHtmlEnvironment('em');
|
||||
}
|
||||
elsif ($bol and m/\G\//cg or m/\G(?<=\P{Word})\//cg) {
|
||||
elsif ($bol and m/\G\//cg or m/\G(?<=[|[:space:]])\/(?=\S)/cg) {
|
||||
return AddHtmlEnvironment('em');
|
||||
}
|
||||
return;
|
||||
|
||||
876
stuff/gopher-server.pl
Executable file
876
stuff/gopher-server.pl
Executable file
@@ -0,0 +1,876 @@
|
||||
#!/usr/bin/env perl
|
||||
# Copyright (C) 2017–2018 Alex Schroeder <alex@gnu.org>
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package OddMuse;
|
||||
use strict;
|
||||
use 5.10.0;
|
||||
use base qw(Net::Server::Fork); # any personality will do
|
||||
use MIME::Base64;
|
||||
use Text::Wrap;
|
||||
use List::Util qw(first);
|
||||
|
||||
our($RunCGI, $DataDir, %IndexHash, @IndexList, $IndexFile, $TagFile, $q,
|
||||
%Page, $OpenPageName, $MaxPost, $ShowEdits, %Locks, $CommentsPattern,
|
||||
$CommentsPrefix, $EditAllowed, $NoEditFile, $SiteName, $ScriptName);
|
||||
|
||||
my $external_image_path = '/home/alex/alexschroeder.ch/pics/';
|
||||
|
||||
# Sadly, we need this information before doing anything else
|
||||
my %args = (proto => 'ssl');
|
||||
for (grep(/--wiki_(key|cert)_file=/, @ARGV)) {
|
||||
$args{SSL_cert_file} = $1 if /--wiki_cert_file=(.*)/;
|
||||
$args{SSL_key_file} = $1 if /--wiki_key_file=(.*)/;
|
||||
}
|
||||
if ($args{SSL_cert_file} and not $args{SSL_key_file}
|
||||
or not $args{SSL_cert_file} and $args{SSL_key_file}) {
|
||||
die "I must have both --wiki_key_file and --wiki_cert_file\n";
|
||||
} elsif ($args{SSL_cert_file} and $args{SSL_key_file}) {
|
||||
OddMuse->run(%args);
|
||||
} else {
|
||||
OddMuse->run;
|
||||
}
|
||||
|
||||
sub options {
|
||||
my $self = shift;
|
||||
my $prop = $self->{'server'};
|
||||
my $template = shift;
|
||||
|
||||
# setup options in the parent classes
|
||||
$self->SUPER::options($template);
|
||||
|
||||
# add a single value option
|
||||
$prop->{wiki} ||= undef;
|
||||
$template->{wiki} = \$prop->{wiki};
|
||||
|
||||
$prop->{wiki_dir} ||= undef;
|
||||
$template->{wiki_dir} = \$prop->{wiki_dir};
|
||||
|
||||
$prop->{wiki_pages} ||= [];
|
||||
$template->{wiki_pages} = $prop->{wiki_pages};
|
||||
|
||||
$prop->{menu} ||= [];
|
||||
$template->{menu} = $prop->{menu};
|
||||
|
||||
$prop->{menu_file} ||= [];
|
||||
$template->{menu_file} = $prop->{menu_file};
|
||||
|
||||
# $prop->{wiki_pem_file} ||= undef;
|
||||
# $template->{wiki_pem_file} = $prop->{wiki_pem_file};
|
||||
}
|
||||
|
||||
sub post_configure_hook {
|
||||
my $self = shift;
|
||||
$self->write_help if $ARGV[0] eq '--help';
|
||||
|
||||
$DataDir = $self->{server}->{wiki_dir} || $ENV{WikiDataDir} || '/tmp/oddmuse';
|
||||
|
||||
$self->log(3, "PID $$");
|
||||
$self->log(3, "Host " . ("@{$self->{server}->{host}}" || "*"));
|
||||
$self->log(3, "Port @{$self->{server}->{port}}");
|
||||
$self->log(3, "Wiki data dir is $DataDir\n");
|
||||
|
||||
$RunCGI = 0;
|
||||
my $wiki = $self->{server}->{wiki} || "./wiki.pl";
|
||||
$self->log(1, "Running $wiki\n");
|
||||
unless (my $return = do $wiki) {
|
||||
$self->log(1, "couldn't parse wiki library $wiki: $@") if $@;
|
||||
$self->log(1, "couldn't do wiki library $wiki: $!") unless defined $return;
|
||||
$self->log(1, "couldn't run wiki library $wiki") unless $return;
|
||||
}
|
||||
|
||||
# make sure search is sorted newest first because NewTagFiltered resorts
|
||||
*OldGopherFiltered = \&Filtered;
|
||||
*Filtered = \&NewGopherFiltered;
|
||||
}
|
||||
|
||||
my $usage = << 'EOT';
|
||||
This server serves a wiki as a gopher site.
|
||||
|
||||
It implements Net::Server and thus all the options available to
|
||||
Net::Server are also available here. Additional options are available:
|
||||
|
||||
wiki - this is the path to the Oddmuse script
|
||||
wiki_dir - this is the path to the Oddmuse data directory
|
||||
wiki_pages - this is a page to show on the entry menu
|
||||
menu - this is the description of a gopher menu to prepend
|
||||
menu_file - this is the filename of the gopher menu to prepend
|
||||
wiki_cert_file - the filename containing a certificate in PEM format
|
||||
wiki_key_file - the filename containing a private key in PEM format
|
||||
|
||||
For many of the options, more information can be had in the Net::Server
|
||||
documentation. This is important if you want to daemonize the server. You'll
|
||||
need to use --pid_file so that you can stop it using a script, --setsid to
|
||||
daemonize it, --log_file to write keep logs, and you'll net to set the user or
|
||||
group using --user or --group such that the server has write access to the data
|
||||
directory.
|
||||
|
||||
For testing purposes, you can start with the following:
|
||||
|
||||
--port=7070
|
||||
The port to listen to, defaults to a random port.
|
||||
--log_level=4
|
||||
The log level to use, defaults to 2.
|
||||
--wiki_dir=/var/oddmuse
|
||||
The wiki directory, defaults to the value of the "WikiDataDir" environment
|
||||
variable or "/tmp/oddmuse".
|
||||
--wiki_lib=/home/alex/src/oddmuse/wiki.pl
|
||||
The Oddmuse main script, defaults to "./wiki.pl".
|
||||
--wiki_pages=SiteMap
|
||||
This adds a page to the main index. Can be used multiple times.
|
||||
--help
|
||||
Prints this message.
|
||||
|
||||
Example invocation:
|
||||
|
||||
/home/alex/src/oddmuse/stuff/gopher-server.pl \
|
||||
--port=7070 \
|
||||
--wiki=/home/alex/src/oddmuse/wiki.pl \
|
||||
--pid_file=/tmp/oddmuse/gopher.pid \
|
||||
--wiki_dir=/tmp/oddmuse \
|
||||
--wiki_pages=Homepage \
|
||||
--wiki_pages=Gopher
|
||||
|
||||
Run the script and test it:
|
||||
|
||||
echo | nc localhost 7070
|
||||
lynx gopher://localhost:7070
|
||||
|
||||
If you want to use SSL, you need to provide PEM files containing certificate and
|
||||
private key. To create self-signed files, for example:
|
||||
|
||||
openssl req -new -x509 -days 365 -nodes -out \
|
||||
gopher-server-cert.pem -keyout gopher-server-key.pem
|
||||
|
||||
Make sure the common name you provide matches your domain name!
|
||||
|
||||
Note that parameters should not contain spaces. Thus:
|
||||
|
||||
/home/alex/src/oddmuse/stuff/gopher-server.pl \
|
||||
--port=7070 \
|
||||
--log_level=3 \
|
||||
--wiki=/home/alex/src/oddmuse/wiki.pl \
|
||||
--wiki_dir=/home/alex/alexschroeder \
|
||||
--menu=Moku_Pona_Updates \
|
||||
--menu_file=~/.moku-pona/updates.txt \
|
||||
--menu=Moku_Pona_Sites \
|
||||
--menu_file=~/.moku-pona/sites.txt
|
||||
|
||||
EOT
|
||||
|
||||
run();
|
||||
|
||||
sub NewGopherFiltered {
|
||||
my @pages = OldGopherFiltered(@_);
|
||||
@pages = sort newest_first @pages;
|
||||
return @pages;
|
||||
}
|
||||
|
||||
sub print_text {
|
||||
my $self = shift;
|
||||
my $text = shift;
|
||||
print($text); # bytes
|
||||
}
|
||||
|
||||
sub print_menu {
|
||||
my $self = shift;
|
||||
my $display = shift;
|
||||
my $selector = shift;
|
||||
my $host = shift
|
||||
|| $self->{server}->{host}->[0]
|
||||
|| $self->{server}->{sockaddr};
|
||||
my $port = shift
|
||||
|| $self->{server}->{port}->[0]
|
||||
|| $self->{server}->{sockport};
|
||||
my $encoded = shift;
|
||||
|
||||
$selector = join('/', map { UrlEncode($_) } split(/\//, $selector)) unless $encoded;
|
||||
$self->print_text(join("\t", $display, $selector, $host, $port)
|
||||
. "\r\n");
|
||||
}
|
||||
|
||||
sub print_info {
|
||||
my $self = shift;
|
||||
my $info = shift;
|
||||
$self->print_menu("i$info", "");
|
||||
}
|
||||
|
||||
sub print_error {
|
||||
my $self = shift;
|
||||
my $error = shift;
|
||||
$self->print_menu("3$error", "");
|
||||
}
|
||||
|
||||
sub serve_main_menu {
|
||||
my $self = shift;
|
||||
my $more = shift;
|
||||
$self->log(3, "Serving main menu");
|
||||
$self->print_info("Welcome to the Gopher version of this wiki.");
|
||||
$self->print_info("");
|
||||
|
||||
$self->print_info("Phlog:");
|
||||
my @pages = sort { $b cmp $a } grep(/^\d\d\d\d-\d\d-\d\d/, @IndexList);
|
||||
for my $id (@pages[0..9]) {
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/menu");
|
||||
}
|
||||
$self->print_menu("1" . "More...", "do/more");
|
||||
$self->print_info("");
|
||||
|
||||
for my $id (@{$self->{server}->{wiki_pages}}) {
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/menu");
|
||||
}
|
||||
|
||||
for my $id (@{$self->{server}->{menu}}) {
|
||||
$self->print_menu("1" . NormalToFree($id), "map/$id");
|
||||
}
|
||||
|
||||
$self->print_menu("1" . "Recent Changes", "do/rc");
|
||||
$self->print_menu("0" . "Gopher RSS", "do/rss");
|
||||
$self->print_menu("7" . "Find matching page titles", "do/match");
|
||||
$self->print_menu("7" . "Full text search", "do/search");
|
||||
$self->print_menu("1" . "Index of all pages", "do/index");
|
||||
|
||||
if ($TagFile) {
|
||||
$self->print_menu("1" . "Index of all tags", "do/tags");
|
||||
}
|
||||
|
||||
if ($EditAllowed and not IsFile($NoEditFile)) {
|
||||
$self->print_menu("w" . "New page", "do/new");
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_phlog_archive {
|
||||
my $self = shift;
|
||||
$self->log(3, "Serving phlog archive");
|
||||
my @pages = sort { $b cmp $a } grep(/^\d\d\d\d-\d\d-\d\d/, @IndexList);
|
||||
for my $id (@pages) {
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/menu");
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_index {
|
||||
my $self = shift;
|
||||
$self->log(3, "Serving index of all pages");
|
||||
for my $id (sort newest_first @IndexList) {
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/menu");
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_match {
|
||||
my $self = shift;
|
||||
my $match = shift;
|
||||
$self->log(3, "Serving pages matching " . UrlEncode($match));
|
||||
$self->print_info("Use a regular expression to match page titles.");
|
||||
$self->print_info("Spaces in page titles are underlines, '_'.");
|
||||
for my $id (sort newest_first grep(/$match/i, @IndexList)) {
|
||||
$self->print_menu( "1" . NormalToFree($id), "$id/menu");
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_search {
|
||||
my $self = shift;
|
||||
my $str = shift;
|
||||
$self->log(3, "Serving search result for " . UrlEncode($str));
|
||||
$self->print_info("Use regular expressions separated by spaces.");
|
||||
SearchTitleAndBody($str, sub {
|
||||
my $id = shift;
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/menu");
|
||||
});
|
||||
}
|
||||
|
||||
sub serve_tags {
|
||||
my $self = shift;
|
||||
$self->log(3, "Serving tag cloud");
|
||||
# open the DB file
|
||||
my %h = TagReadHash();
|
||||
my %count = ();
|
||||
foreach my $tag (grep !/^_/, keys %h) {
|
||||
$count{$tag} = @{$h{$tag}};
|
||||
}
|
||||
foreach my $id (sort { $count{$b} <=> $count{$a} } keys %count) {
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/tag");
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_rc {
|
||||
my $self = shift;
|
||||
my $showedit = $ShowEdits = shift;
|
||||
$self->log(3, "Serving recent changes"
|
||||
. ($showedit ? " including minor changes" : ""));
|
||||
|
||||
$self->print_info("Recent Changes");
|
||||
if ($showedit) {
|
||||
$self->print_menu("1" . "Skip minor edits", "do/rc");
|
||||
} else {
|
||||
$self->print_menu("1" . "Show minor edits", "do/rc/showedits");
|
||||
}
|
||||
|
||||
ProcessRcLines(
|
||||
sub {
|
||||
my $date = shift;
|
||||
$self->print_info("");
|
||||
$self->print_info("$date");
|
||||
$self->print_info("");
|
||||
},
|
||||
sub {
|
||||
my($id, $ts, $author_host, $username, $summary, $minor, $revision,
|
||||
$languages, $cluster, $last) = @_;
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/menu");
|
||||
for my $line (split(/\n/, wrap(' ', ' ', $summary))) {
|
||||
$self->print_info($line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sub serve_rss {
|
||||
my $self = shift;
|
||||
$self->log(3, "Serving Gopher RSS");
|
||||
my $host = shift
|
||||
|| $self->{server}->{host}->[0]
|
||||
|| $self->{server}->{sockaddr};
|
||||
my $port = shift
|
||||
|| $self->{server}->{port}->[0]
|
||||
|| $self->{server}->{sockport};
|
||||
my $gopher = "gopher://$host:$port/"; # use gophers for TLS?
|
||||
local $ScriptName = $gopher;
|
||||
my $rss = GetRcRss();
|
||||
$rss =~ s!$ScriptName\?action=rss!${gopher}1do/rss!g;
|
||||
$rss =~ s!$ScriptName\?action=history;id=([^[:space:]<]*)!${gopher}1$1/history!g;
|
||||
$rss =~ s!$ScriptName/([^[:space:]<]*)!${gopher}0$1!g;
|
||||
$rss =~ s!<wiki:diff>.*</wiki:diff>\n!!g;
|
||||
print $rss;
|
||||
}
|
||||
|
||||
sub serve_map {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
$self->log(3, "Serving map " . UrlEncode($id));
|
||||
my @menu = @{$self->{server}->{menu}};
|
||||
my $i = first { $id eq $menu[$_] } 0..$#menu;
|
||||
my $file = $self->{server}->{menu_file}->[$i];
|
||||
if (-f $file and open(my $fh, '<:encoding(UTF-8)', $file)) {
|
||||
local $/ = undef;
|
||||
my $text = <$fh>;
|
||||
$self->log(4, "Map has " . length($text) . " characters");
|
||||
$self->print_text($text);
|
||||
} else {
|
||||
$self->log(1, "Error reading $file");
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_page_comment_link {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $revision = shift;
|
||||
if (not $revision and $CommentsPattern) {
|
||||
if ($id =~ /$CommentsPattern/) {
|
||||
my $original = $1;
|
||||
# sometimes we are on a comment page and cannot derive the original
|
||||
$self->print_menu("1" . "Back to the original page",
|
||||
"$original/menu") if $original;
|
||||
$self->print_menu("w" . "Add a comment", "$id/append/text");
|
||||
} else {
|
||||
my $comments = $CommentsPrefix . $id;
|
||||
$self->print_menu("1" . "Comments on this page", "$comments/menu");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_page_history_link {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $revision = shift;
|
||||
if (not $revision) {
|
||||
$self->print_menu("1" . "Page History", "$id/history");
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_file_page_menu {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $type = shift;
|
||||
my $revision = shift;
|
||||
my $code = substr($type, 0, 6) eq 'image/' ? 'I' : '9';
|
||||
$self->log(3, "Serving file page menu for " . UrlEncode($id));
|
||||
$self->print_menu($code . NormalToFree($id)
|
||||
. ($revision ? "/$revision" : ""), $id);
|
||||
$self->serve_page_comment_link($id, $revision);
|
||||
$self->serve_page_history_link($id, $revision);
|
||||
}
|
||||
|
||||
sub serve_text_page_menu {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $page = shift;
|
||||
my $revision = shift;
|
||||
$self->log(3, "Serving text page menu for " . UrlEncode($id)
|
||||
. ($revision ? "/$revision" : ""));
|
||||
|
||||
$self->print_info("The text of this page:");
|
||||
$self->print_menu("0" . NormalToFree($id),
|
||||
$id . ($revision ? "/$revision" : ""));
|
||||
$self->print_menu("h" . NormalToFree($id),
|
||||
$id . ($revision ? "/$revision" : "") . "/html");
|
||||
$self->print_menu("w" . "Replace " . NormalToFree($id),
|
||||
$id . "/write/text");
|
||||
|
||||
$self->serve_page_comment_link($id, $revision);
|
||||
$self->serve_page_history_link($id, $revision);
|
||||
|
||||
my $first = 1;
|
||||
while ($page->{text} =~ /\[\[([^\]|]*)(?:\|([^\]]*))?\]\]|\[(https?:\/\/\S+)\s+([^\]]*)\]|\[gopher:\/\/([^:\/]*)(?::(\d+))?(?:\/(\d)(\S+))?\s+([^\]]+)\]/g) {
|
||||
my ($title, $text, $url, $hostname, $port, $type, $selector)
|
||||
= ($1, $2||$4||$9, $3, $5, $6||70, $7||1, $8);
|
||||
if ($first) {
|
||||
$self->print_info("");
|
||||
$self->print_info("Links leaving " . NormalToFree($id) . ":");
|
||||
$first = 0;
|
||||
}
|
||||
if ($hostname) {
|
||||
$self->print_text(join("\t", $type . $text, $selector, $hostname, $port) . "\r\n");
|
||||
} elsif ($url) {
|
||||
$self->print_menu("h$text", "URL:" . $url, undef, undef, 1);
|
||||
} elsif ($title and substr($title, 0, 4) eq 'tag:') {
|
||||
$self->print_menu("1" . ($text||substr($title, 4)),
|
||||
substr($title, 4) . "/tag");
|
||||
} elsif ($title =~ s!^image[/a-z]* external:!pics/!) {
|
||||
$self->print_menu("I" . $text||$title, $title);
|
||||
} elsif ($title) {
|
||||
$title =~ s!^image[/[a-z]]*:!!;
|
||||
$self->print_menu("1" . $text||$title, $title . "/menu");
|
||||
}
|
||||
}
|
||||
|
||||
$first = 1;
|
||||
while ($page->{text} =~ /\[https?:\/\/gopher\.floodgap\.com\/gopher\/gw\?a=gopher%3a%2f%2f(.*?)(?:%3a(\d+))?%2f(.)(\S+)\s+([^\]]+)\]/gi) {
|
||||
my ($hostname, $port, $type, $selector, $text) = ($1, $2||"70", $3, $4, $5);
|
||||
if ($first) {
|
||||
$self->print_info("");
|
||||
$self->print_info("Gopher links (via Floodgap):");
|
||||
$first = 0;
|
||||
}
|
||||
$selector =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/eig; # url unescape
|
||||
$self->print_text(join("\t", $type . $text, $selector, $hostname, $port)
|
||||
. "\r\n");
|
||||
}
|
||||
|
||||
if ($page->{text} =~ m/<journal search tag:(\S+)>\s*/) {
|
||||
my $tag = $1;
|
||||
$self->print_info("");
|
||||
$self->serve_tag_list($tag);
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_page_history {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
$self->log(3, "Serving history of " . UrlEncode($id));
|
||||
OpenPage($id);
|
||||
|
||||
$self->print_menu("1" . NormalToFree($id) . " (current)", "$id/menu");
|
||||
$self->print_info(CalcTime($Page{ts})
|
||||
. " by " . GetAuthor($Page{host}, $Page{username})
|
||||
. ($Page{summary} ? ": $Page{summary}" : "")
|
||||
. ($Page{minor} ? " (minor)" : ""));
|
||||
|
||||
foreach my $revision (GetKeepRevisions($OpenPageName)) {
|
||||
my $keep = GetKeptRevision($revision);
|
||||
$self->print_menu("1" . NormalToFree($id) . " ($keep->{revision})",
|
||||
"$id/$keep->{revision}/menu");
|
||||
$self->print_info(CalcTime($keep->{ts})
|
||||
. " by " . GetAuthor($keep->{host}, $keep->{username})
|
||||
. ($keep->{summary} ? ": $keep->{summary}" : "")
|
||||
. ($keep->{minor} ? " (minor)" : ""));
|
||||
}
|
||||
}
|
||||
|
||||
sub get_page {
|
||||
my $id = shift;
|
||||
my $revision = shift;
|
||||
my $page;
|
||||
|
||||
if ($revision) {
|
||||
$OpenPageName = $id;
|
||||
$page = GetKeptRevision($revision);
|
||||
} else {
|
||||
OpenPage($id);
|
||||
$page = \%Page;
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
sub serve_page_menu {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $revision = shift;
|
||||
my $page = get_page($id, $revision);
|
||||
|
||||
if (my ($type) = TextIsFile($page->{text})) {
|
||||
$self->serve_file_page_menu($id, $type, $revision);
|
||||
} else {
|
||||
$self->serve_text_page_menu($id, $page, $revision);
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_file_page {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $page = shift;
|
||||
$self->log(3, "Serving " . UrlEncode($id) . " as file");
|
||||
my ($encoded) = $page->{text} =~ /^[^\n]*\n(.*)/s;
|
||||
$self->log(4, UrlEncode($id) . " has " . length($encoded)
|
||||
. " bytes of MIME encoded data");
|
||||
my $data = decode_base64($encoded);
|
||||
$self->log(4, UrlEncode($id) . " has " . length($data)
|
||||
. " bytes of binary data");
|
||||
binmode(STDOUT, ":raw");
|
||||
print($data);
|
||||
}
|
||||
|
||||
sub serve_text_page {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $page = shift;
|
||||
my $text = $page->{text};
|
||||
$self->log(3, "Serving " . UrlEncode($id) . " as " . length($text)
|
||||
. " bytes of text");
|
||||
$text =~ s/^\./../mg;
|
||||
$self->print_text($text);
|
||||
}
|
||||
|
||||
sub serve_page {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $revision = shift;
|
||||
my $page = get_page($id, $revision);
|
||||
if (my ($type) = TextIsFile($page->{text})) {
|
||||
$self->serve_file_page($id, $page);
|
||||
} else {
|
||||
$self->serve_text_page($id, $page);
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_page_html {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $revision = shift;
|
||||
my $page = get_page($id, $revision);
|
||||
|
||||
$self->log(3, "Serving " . UrlEncode($id) . " as HTML");
|
||||
|
||||
my $title = NormalToFree($id);
|
||||
print GetHtmlHeader(Ts('%s:', $SiteName) . ' ' . UnWiki($title), $id);
|
||||
print GetHeaderDiv($id, $title);
|
||||
print $q->start_div({-class=>'wrapper'});
|
||||
|
||||
if ($revision) {
|
||||
# no locking of the file, no updating of the cache
|
||||
PrintWikiToHTML($page->{text});
|
||||
} else {
|
||||
PrintPageHtml();
|
||||
}
|
||||
PrintFooter($id, $revision);
|
||||
}
|
||||
|
||||
sub serve_redirect {
|
||||
my $self = shift;
|
||||
my $url = shift;
|
||||
print qq{<!DOCTYPE HTML>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; url=$url">
|
||||
<title>Redirection</title>
|
||||
</head>
|
||||
<body>
|
||||
If you are not redirected automatically, follow this <a href='$url'>link</a>.
|
||||
</body>
|
||||
</html>
|
||||
};
|
||||
}
|
||||
|
||||
sub serve_image {
|
||||
my $self = shift;
|
||||
my $pic = shift;
|
||||
my $file = $external_image_path . $pic;
|
||||
# no tricks
|
||||
if ($file !~ /\.\./ and $file !~ /\/\//
|
||||
and -f $file and open(my $fh, "<", $file)) {
|
||||
local $/ = undef;
|
||||
my $data = <$fh>;
|
||||
$self->log(4, $pic . " has " . length($data)
|
||||
. " bytes of binary data");
|
||||
binmode(STDOUT, ":raw");
|
||||
print($data);
|
||||
} else {
|
||||
$self->log(1, "Error reading $file: $!");
|
||||
}
|
||||
}
|
||||
|
||||
sub newest_first {
|
||||
my ($A, $B) = ($a, $b);
|
||||
if ($A =~ /^\d\d\d\d-\d\d-\d\d/ and $B =~ /^\d\d\d\d-\d\d-\d\d/) {
|
||||
return $B cmp $A;
|
||||
}
|
||||
$A cmp $B;
|
||||
}
|
||||
|
||||
sub serve_tag_list {
|
||||
my $self = shift;
|
||||
my $tag = shift;
|
||||
$self->print_info("Search result for tag $tag:");
|
||||
for my $id (sort newest_first TagFind($tag)) {
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/menu");
|
||||
}
|
||||
}
|
||||
|
||||
sub serve_tag {
|
||||
my $self = shift;
|
||||
my $tag = shift;
|
||||
$self->log(3, "Serving tag " . UrlEncode($tag));
|
||||
if ($IndexHash{$tag}) {
|
||||
$self->print_info("This page is about the tag $tag.");
|
||||
$self->print_menu("1" . NormalToFree($tag), "$tag/menu");
|
||||
$self->print_info("");
|
||||
}
|
||||
$self->serve_tag_list($tag);
|
||||
}
|
||||
|
||||
sub serve_error {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $error = shift;
|
||||
$self->log(3, "Error ('" . UrlEncode($id) . "'): $error");
|
||||
$self->print_error("Error ('" . UrlEncode($id) . "'): $error");
|
||||
}
|
||||
|
||||
sub write_help {
|
||||
my $self = shift;
|
||||
my @lines = split(/\n/, <<"EOF");
|
||||
This is how your document should start:
|
||||
```
|
||||
username: Alex Schroeder
|
||||
summary: typo fixed
|
||||
```
|
||||
This is the text of your document.
|
||||
Just write whatever.
|
||||
|
||||
Note the space after the colon for metadata fields.
|
||||
More metadata fields are allowed:
|
||||
`minor` is 1 if this is a minor edit. The default is 0.
|
||||
EOF
|
||||
for my $line (@lines) {
|
||||
$self->print_info($line);
|
||||
}
|
||||
}
|
||||
|
||||
sub write_page_ok {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
$self->print_info("Page was saved.");
|
||||
$self->print_menu("1" . NormalToFree($id), "$id/menu");
|
||||
}
|
||||
|
||||
sub write_page_error {
|
||||
my $self = shift;
|
||||
my $error = shift;
|
||||
$self->log(4, "Not saved: $error");
|
||||
$self->print_error("Page was not saved: $error");
|
||||
map { ReleaseLockDir($_); } keys %Locks;
|
||||
}
|
||||
|
||||
sub write_data {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $data = shift;
|
||||
my $param = shift||'text';
|
||||
SetParam($param, $data);
|
||||
my $error;
|
||||
eval {
|
||||
local *ReBrowsePage = sub {};
|
||||
local *ReportError = sub { $error = shift };
|
||||
DoPost($id);
|
||||
};
|
||||
if ($error) {
|
||||
$self->write_page_error($error);
|
||||
} else {
|
||||
$self->write_page_ok($id);
|
||||
}
|
||||
}
|
||||
|
||||
sub write_file_page {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $data = shift;
|
||||
my $type = shift || 'application/octet-stream';
|
||||
$self->write_page_error("page title is missing") unless $id;
|
||||
$self->log(3, "Posting " . length($data) . " bytes of $type to page "
|
||||
. UrlEncode($id));
|
||||
# no metadata
|
||||
$self->write_data($id, "#FILE $type\n" . encode_base64($data));
|
||||
}
|
||||
|
||||
sub write_text {
|
||||
my $self = shift;
|
||||
my $id = shift;
|
||||
my $data = shift;
|
||||
my $param = shift;
|
||||
|
||||
utf8::decode($data);
|
||||
|
||||
my ($lead, $meta, $text) = split(/^```\s*(?:meta)?\n/m, $data, 3);
|
||||
|
||||
if (not $lead and $meta) {
|
||||
while ($meta =~ /^([a-z-]+): (.*)/mg) {
|
||||
if ($1 eq 'minor' and $2) {
|
||||
SetParam('recent_edit', 'on'); # legacy UseMod parameter name
|
||||
} else {
|
||||
SetParam($1, $2);
|
||||
if ($1 eq "title") {
|
||||
$id = $2;
|
||||
}
|
||||
}
|
||||
}
|
||||
$self->log(3, ($param eq 'text' ? "Posting" : "Appending")
|
||||
. " " . length($text) . " characters (with metadata) to page $id");
|
||||
$self->write_data($id, $text, $param);
|
||||
} else {
|
||||
# no meta data
|
||||
$self->log(3, ($param eq 'text' ? "Posting" : "Appending")
|
||||
. " " . length($data) . " characters to page $id") if $id;
|
||||
$self->write_data($id, $data, $param);
|
||||
}
|
||||
}
|
||||
|
||||
sub write_text_page {
|
||||
my $self = shift;
|
||||
$self->write_text(@_, 'text');
|
||||
}
|
||||
|
||||
sub append_text_page {
|
||||
my $self = shift;
|
||||
$self->write_text(@_, 'aftertext');
|
||||
}
|
||||
|
||||
sub read_file {
|
||||
my $self = shift;
|
||||
my $length = shift;
|
||||
$length = $MaxPost if $length > $MaxPost;
|
||||
local $/ = \$length;
|
||||
my $buf .= <STDIN>;
|
||||
$self->log(4, "Received " . length($buf) . " bytes (max is $MaxPost)");
|
||||
return $buf;
|
||||
}
|
||||
|
||||
sub read_text {
|
||||
my $self = shift;
|
||||
my $buf;
|
||||
while (1) {
|
||||
my $line = <STDIN>;
|
||||
if (length($line) == 0) {
|
||||
sleep(1); # wait for input
|
||||
next;
|
||||
}
|
||||
last if $line =~ /^.\r?\n/m;
|
||||
$buf .= $line;
|
||||
if (length($buf) > $MaxPost) {
|
||||
$buf = substr($buf, 0, $MaxPost);
|
||||
last;
|
||||
}
|
||||
}
|
||||
$self->log(4, "Received " . length($buf) . " bytes (max is $MaxPost)");
|
||||
utf8::decode($buf);
|
||||
$self->log(4, "Received " . length($buf) . " characters");
|
||||
return $buf;
|
||||
}
|
||||
|
||||
sub process_request {
|
||||
my $self = shift;
|
||||
|
||||
# clear cookie and all that
|
||||
$q = undef;
|
||||
Init();
|
||||
|
||||
# refresh list of pages
|
||||
if (IsFile($IndexFile) and ReadIndex()) {
|
||||
# we're good
|
||||
} else {
|
||||
RefreshIndex();
|
||||
}
|
||||
|
||||
eval {
|
||||
local $SIG{'ALRM'} = sub {
|
||||
$self->log(1, "Timeout!");
|
||||
die "Timed Out!\n";
|
||||
};
|
||||
alarm(10); # timeout
|
||||
my $selector = <STDIN>; # no loop
|
||||
$selector = UrlDecode($selector); # assuming URL-encoded UTF-8
|
||||
$selector =~ s/\s+$//g; # no trailing whitespace
|
||||
|
||||
if (not $selector or $selector eq "/") {
|
||||
$self->serve_main_menu();
|
||||
} elsif ($selector eq "do/more") {
|
||||
$self->serve_phlog_archive();
|
||||
} elsif ($selector eq "do/index") {
|
||||
$self->serve_index();
|
||||
} elsif (substr($selector, 0, 9) eq "do/match\t") {
|
||||
$self->serve_match(substr($selector, 9));
|
||||
} elsif (substr($selector, 0, 10) eq "do/search\t") {
|
||||
$self->serve_search(substr($selector, 10));
|
||||
} elsif ($selector eq "do/tags") {
|
||||
$self->serve_tags();
|
||||
} elsif ($selector eq "do/rc") {
|
||||
$self->serve_rc(0);
|
||||
} elsif ($selector eq "do/rss") {
|
||||
$self->serve_rss(0);
|
||||
} elsif ($selector eq "do/rc/showedits") {
|
||||
$self->serve_rc(1);
|
||||
} elsif ($selector eq "do/new") {
|
||||
my $data = $self->read_text();
|
||||
$self->write_text_page(undef, $data);
|
||||
} elsif ($selector =~ m!^([^/]*)/(\d+)/menu$!) {
|
||||
$self->serve_page_menu($1, $2);
|
||||
} elsif ($selector =~ m!^map/(.*)!) {
|
||||
$self->serve_map($1);
|
||||
} elsif (substr($selector, -5) eq '/menu') {
|
||||
$self->serve_page_menu(substr($selector, 0, -5));
|
||||
} elsif ($selector =~ m!^([^/]*)/tag$!) {
|
||||
$self->serve_tag($1);
|
||||
} elsif ($selector =~ m!^([^/]*)(?:/(\d+))?/html!) {
|
||||
$self->serve_page_html($1, $2);
|
||||
} elsif ($selector =~ m!^([^/]*)/history$!) {
|
||||
$self->serve_page_history($1);
|
||||
} elsif ($selector =~ m!^([^/]*)/write/text$!) {
|
||||
my $data = $self->read_text();
|
||||
$self->write_text_page($1, $data);
|
||||
} elsif ($selector =~ m!^([^/]*)/append/text$!) {
|
||||
my $data = $self->read_text();
|
||||
$self->append_text_page($1, $data);
|
||||
} elsif ($selector =~ m!^([^/]*)(?:/([a-z]+/[-a-z]+))?/write/file(?:\t(\d+))?$!) {
|
||||
my $data = $self->read_file($3);
|
||||
$self->write_file_page($1, $data, $2);
|
||||
} elsif ($selector =~ m!^([^/]*)(?:/(\d+))?(?:/text)?$!) {
|
||||
$self->serve_page($1, $2);
|
||||
} elsif ($selector =~ m!^URL:(.*)!i) {
|
||||
$self->serve_redirect(UrlDecode($1));
|
||||
} elsif ($selector =~ m!^pics/(.*)!i) {
|
||||
$self->serve_image(UrlDecode($1));
|
||||
} else {
|
||||
$self->serve_error($selector, ValidId($selector)||'Cause unknown');
|
||||
}
|
||||
|
||||
$self->log(4, "Done");
|
||||
}
|
||||
}
|
||||
309
t/gopher-server.t
Normal file
309
t/gopher-server.t
Normal file
@@ -0,0 +1,309 @@
|
||||
# Copyright (C) 2017–2018 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package OddMuse;
|
||||
use strict;
|
||||
use 5.10.0;
|
||||
use Test::More;
|
||||
use IO::Socket::IP;
|
||||
use utf8; # tests contain UTF-8 characters and it matters
|
||||
|
||||
require './t/test.pl';
|
||||
|
||||
add_module('tags.pl');
|
||||
|
||||
# enable uploads
|
||||
our($ConfigFile);
|
||||
AppendStringToFile($ConfigFile, "\$UploadAllowed = 1;\n");
|
||||
|
||||
my $port = random_port();
|
||||
my $pid = fork();
|
||||
|
||||
END {
|
||||
# kill server
|
||||
if ($pid) {
|
||||
kill 'KILL', $pid or warn "Could not kill server $pid";
|
||||
}
|
||||
}
|
||||
|
||||
our ($DataDir);
|
||||
if (!defined $pid) {
|
||||
die "Cannot fork: $!";
|
||||
} elsif ($pid == 0) {
|
||||
use Config;
|
||||
my $secure_perl_path = $Config{perlpath};
|
||||
exec($secure_perl_path,
|
||||
"stuff/gopher-server.pl",
|
||||
"--port=$port",
|
||||
"--log_level=0", # set to 4 for verbose logging
|
||||
"--wiki=./wiki.pl",
|
||||
"--wiki_dir=$DataDir",
|
||||
"--wiki_pages=Alex",
|
||||
"--wiki_pages=Berta",
|
||||
"--wiki_pages=Chris")
|
||||
or die "Cannot exec: $!";
|
||||
}
|
||||
|
||||
update_page('Alex', "My best friend is [[Berta]].\n\nTags: [[tag:Friends]]\n");
|
||||
update_page('Berta', "This is me.\n\nTags: [[tag:Friends]]\n");
|
||||
update_page('Chris', "I'm Chris.\n\nTags: [[tag:Friends]]\n");
|
||||
update_page('Friends', "Some friends.\n");
|
||||
update_page('2017-12-25', 'It was a Monday.\n\nTags: [[tag:Day]]');
|
||||
update_page('2017-12-26', 'It was a Tuesday.\n\nTags: [[tag:Day]]');
|
||||
update_page('2017-12-27', 'It was a Wednesday.\n\nTags: [[tag:Day]]');
|
||||
update_page('Friends', "News about friends.\n", 'rewrite', 1); # minor change
|
||||
update_page('Friends', "News about friends:\n\n<journal search tag:friends>\n",
|
||||
'add journal tag', 1); # minor change
|
||||
|
||||
# file created using convert NULL: test.png && base64 test.png
|
||||
update_page('Picture',
|
||||
"#FILE image/png\niVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bv"
|
||||
. "kkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg==");
|
||||
|
||||
sub query_gopher {
|
||||
my $query = shift;
|
||||
my $text = shift;
|
||||
|
||||
# create client
|
||||
my $socket = IO::Socket::IP->new(
|
||||
PeerHost => "localhost",
|
||||
PeerPort => $port,
|
||||
Type => SOCK_STREAM, )
|
||||
or die "Cannot construct client socket: $@";
|
||||
|
||||
$socket->print("$query\r\n");
|
||||
$socket->print($text);
|
||||
|
||||
undef $/; # slurp
|
||||
return <$socket>;
|
||||
}
|
||||
|
||||
# main menu
|
||||
my $page = query_gopher("");
|
||||
for my $item(qw(Alex Berta Chris 2017-12-25 2017-12-26 2017-12-27)) {
|
||||
like($page, qr/^1$item\t$item\/menu\t/m, "main menu contains $item");
|
||||
}
|
||||
|
||||
# page menu
|
||||
$page = query_gopher("Alex/menu");
|
||||
like($page, qr/^0Alex\tAlex\t/m,
|
||||
"Alex menu links to plain text");
|
||||
like($page, qr/^hAlex\tAlex\/html\t/m,
|
||||
"Alex menu links to HTML");
|
||||
like($page, qr/^1Page History\tAlex\/history\t/m,
|
||||
"Alex menu links to page history");
|
||||
like($page, qr/^1Berta\tBerta\/menu\t/m,
|
||||
"Alex menu links to Berta menu");
|
||||
like($page, qr/^1Friends\tFriends\/tag\t/m,
|
||||
"Alex menu links to Friends tag");
|
||||
|
||||
# plain text
|
||||
$page = query_gopher("Alex");
|
||||
like($page, qr/^My best friend is \[\[Berta\]\]/, "Alex plain text");
|
||||
|
||||
# HTML
|
||||
$page = query_gopher("Alex/html");
|
||||
like($page, qr/<p>My best friend is <a.*?>Berta<\/a>/, "Alex HTML");
|
||||
|
||||
# tags
|
||||
$page = query_gopher("Friends/tag");
|
||||
like($page, qr/iThis page is about the tag Friends/, "tag menu intro");
|
||||
for my $item(qw(Friends Alex Berta Chris)) {
|
||||
like($page, qr/^1$item\t$item\/menu\t/m, "tag menu contains $item");
|
||||
}
|
||||
|
||||
# tags
|
||||
$page = query_gopher("Day/tag");
|
||||
like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
|
||||
"tag menu sorted newest first");
|
||||
|
||||
# match
|
||||
$page = query_gopher("do/match\t2017");
|
||||
for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
|
||||
like($page, qr/^1$item\t$item\/menu\t/m, "match menu contains $item");
|
||||
}
|
||||
like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
|
||||
"match menu sorted newest first");
|
||||
|
||||
# search
|
||||
$page = query_gopher("do/search\ttag:day");
|
||||
for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
|
||||
like($page, qr/^1$item\t$item\/menu\t/m, "serch menu contains $item");
|
||||
}
|
||||
like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
|
||||
"search menu sorted newest first");
|
||||
|
||||
# rc
|
||||
$page = query_gopher("do/rc");
|
||||
my $re = join(".*", "Picture", "2017-12-27", "2017-12-26", "2017-12-25",
|
||||
"Friends", "Chris", "Berta", "Alex");
|
||||
like($page, qr/$re/s, "rc in the right order");
|
||||
|
||||
$page = query_gopher("do/rc/showedits");
|
||||
|
||||
$re = join(".*", "Friends", "2017-12-27", "2017-12-26", "2017-12-25");
|
||||
like($page, qr/$re/s, "rc in the right order");
|
||||
|
||||
# history
|
||||
$page = query_gopher("Friends/history");
|
||||
like($page, qr/^1Friends \(1\)\tFriends\/1\/menu\t/m,
|
||||
"Friends (1)");
|
||||
like($page, qr/^1Friends \(2\)\tFriends\/2\/menu\t/m,
|
||||
"Friends (2)");
|
||||
like($page, qr/^1Friends \(current\)\tFriends\/menu\t/m,
|
||||
"Friends (current)");
|
||||
like($page, qr/Friends\/menu.*Friends\/2\/menu.*Friends\/1\/menu/s,
|
||||
"history in the right order");
|
||||
|
||||
# revision menu
|
||||
$page = query_gopher("Friends/1/menu");
|
||||
like($page, qr/^0Friends\tFriends\/1\t/m,
|
||||
"Friends/1 menu links to plain text");
|
||||
like($page, qr/^hFriends\tFriends\/1\/html\t/m,
|
||||
"Friends/1 menu links to HTML");
|
||||
unlike($page, qr/Search result for tag/,
|
||||
"Friends/1 has no journal and thus no tag search");
|
||||
|
||||
# revision plain text
|
||||
$page = query_gopher("Friends/1");
|
||||
like($page, qr/^Some friends/m, "Friends/1 plain text");
|
||||
|
||||
# revision html
|
||||
$page = query_gopher("Friends/1/html");
|
||||
like($page, qr/<p>Some friends/m, "Friends/1 html");
|
||||
|
||||
# upload text
|
||||
my $haiku = <<EOT;
|
||||
Quiet disk ratling
|
||||
Keyboard clicking, then it stops.
|
||||
Rain falls and I think
|
||||
.
|
||||
EOT
|
||||
|
||||
$page = query_gopher("Haiku/write/text", "$haiku");
|
||||
like($page, qr/^iPage was saved./m, "Write Haiku");
|
||||
like($page, qr/^1Haiku\tHaiku\/menu/m, "Link back to Haiku");
|
||||
|
||||
my $haiku_re = quotemeta(substr($haiku, 0, -2)); # strip period and \n
|
||||
$page = query_gopher("Haiku");
|
||||
like($page, qr/^$haiku_re/, "Haiku saved");
|
||||
|
||||
$haiku = <<"EOT";
|
||||
```
|
||||
username: Alex
|
||||
minor: 1
|
||||
summary: typos
|
||||
```
|
||||
Quiet disk rattling
|
||||
Keyboard clicking, then it stops.
|
||||
Rain falls and I think.
|
||||
.
|
||||
EOT
|
||||
|
||||
$page = query_gopher("Haiku/write/text", "$haiku");
|
||||
like($page, qr/^iPage was saved./m, "Write haiku");
|
||||
|
||||
$haiku_re = quotemeta(<<"EOT");
|
||||
Quiet disk rattling
|
||||
Keyboard clicking, then it stops.
|
||||
Rain falls and I think.
|
||||
EOT
|
||||
|
||||
$page = query_gopher("Haiku");
|
||||
like($page, qr/^$haiku_re/, "Haiku updated");
|
||||
|
||||
$page = query_gopher("Haiku/history");
|
||||
like($page, qr/^1Haiku \(current\)\tHaiku\/menu\t/m, "Haiku (current)");
|
||||
like($page, qr/^i\d\d:\d\d UTC by Alex from \S+: typos \(minor\)/m,
|
||||
"Metadata recorded");
|
||||
like($page, qr/^1Haiku \(1\)\tHaiku\/1\/menu\t/m, "Haiku (1)");
|
||||
|
||||
# new page
|
||||
$page = query_gopher("do/new", <<"EOT");
|
||||
```
|
||||
username: Alex
|
||||
summary: copy
|
||||
title: Haiku_Copy
|
||||
```
|
||||
Quiet disk rattling
|
||||
Keyboard clicking, then it stops.
|
||||
Rain falls and I think.
|
||||
.
|
||||
EOT
|
||||
like($page, qr/^iPage was saved./m, "Write copy of haiku");
|
||||
$page = query_gopher("Haiku_Copy");
|
||||
like($page, qr/^$haiku_re/, "New copy of haiku created");
|
||||
|
||||
# append
|
||||
$page = query_gopher("Haiku_Copy/append/text", "This is a comment by me!\n.\n");
|
||||
like($page, qr/^iPage was saved./m, "Append to copy of haiku");
|
||||
$page = query_gopher("Haiku_Copy");
|
||||
like($page, qr/^$haiku_re/, "Copy of haiku still there");
|
||||
like($page, qr/\n\n----\n\nThis is a comment by me!\n\n-- Anonymous/,
|
||||
"Comment is also there");
|
||||
|
||||
# Image download
|
||||
my $image = query_gopher("Picture");
|
||||
like($image, qr/\211PNG\r\n/, "Image download");
|
||||
|
||||
# Image upload
|
||||
$page = query_gopher("PictureCopy/write/file\t" . length($image), "$image");
|
||||
like($page, qr/Files of type application\/octet-stream are not allowed/m,
|
||||
"MIME type check");
|
||||
|
||||
$page = query_gopher("PictureCopy/image/png/write/file\t" . length($image), "$image");
|
||||
like($page, qr/^iPage was saved./m, "Image upload");
|
||||
unlike($page, qr/^3Page was not saved/, "Messages are correct");
|
||||
|
||||
my $copy = query_gopher("PictureCopy");
|
||||
like($copy, qr/\211PNG\r\n/, "Image copy download");
|
||||
|
||||
is($copy, $image, "Image and copy are identical");
|
||||
|
||||
# image:link
|
||||
$page = query_gopher("Test/write/text", "[[image:Picture]]\n.\n");
|
||||
like($page, qr/^iPage was saved./m, "Saved test page containing image link");
|
||||
$page = query_gopher("Test/menu");
|
||||
like($page, qr/^1Picture\tPicture\/menu/m, "Link to image page looks good");
|
||||
$page = query_gopher("Picture/menu");
|
||||
like($page, qr/^IPicture\tPicture/, "Link to image file looks good");
|
||||
|
||||
# Test upload of large page (but note $MaxPost: 1024 * 210 > (10 * 8 + 1) * 2600)
|
||||
my $garbage = (("0123456789" x 8) . "\n") x 2600 . "Last Line\n";
|
||||
$page = query_gopher("Large/write/text", "$garbage.\n");
|
||||
like($page, qr/^iPage was saved./m, "Write page with "
|
||||
. length($garbage) . " bytes");
|
||||
$page = query_gopher("Large");
|
||||
like(substr($page, -20), qr/Last Line/, "All of large page was saved");
|
||||
|
||||
# Test of Umlauts in the selector
|
||||
test_page(update_page('Zürich♥', '[[Üetliberg♥]]'), 'Zürich♥', 'Üetliberg♥');
|
||||
$page = query_gopher("Z%c3%bcrich%e2%99%a5");
|
||||
utf8::decode($page);
|
||||
like($page, qr/Üetliberg♥/, "UTF-8 encoded page names");
|
||||
|
||||
$page = query_gopher("Z%c3%bcrich%e2%99%a5/menu");
|
||||
utf8::decode($page);
|
||||
like($page, qr/^0Zürich♥\tZ%c3%bcrich%e2%99%a5\t/m, "UTF-8 encoded text link");
|
||||
like($page, qr/^1Üetliberg♥\t%c3%9cetliberg%e2%99%a5\/menu\t/m,
|
||||
"UTF-8 encoded links");
|
||||
|
||||
# gopher links
|
||||
update_page('Gopher', '[http://gopher.floodgap.com/gopher/gw?a=gopher%3A%2F%2Fsdf.org%3A70%2F0%2Fusers%2Fsolderpunk%2Fphlog%2Fintroducing-vf1.txt VF-1], [gopher://sdf.org:70/1/phlogs/ Phlogs]');
|
||||
$page = query_gopher("Gopher/menu");
|
||||
like($page, qr/^1Phlogs\t\/phlogs\/\tsdf\.org\t70/m, "Direct Gopher link");
|
||||
like($page, qr/^0VF-1\t\/users\/solderpunk\/phlog\/introducing-vf1.txt\tsdf\.org\t70/m, "Floodgap proxy link");
|
||||
|
||||
done_testing();
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More tests => 52;
|
||||
use Test::More tests => 53;
|
||||
|
||||
add_module('markdown-rule.pl');
|
||||
add_module('bbcode.pl');
|
||||
@@ -38,7 +38,7 @@ run_tests(split(/\n/,<<'EOT'));
|
||||
- one
|
||||
<ul><li>one</li></ul>
|
||||
- one\n-- Alex
|
||||
<ul><li>one</li><li>- Alex</li></ul>
|
||||
<ul><li>one -- Alex</li></ul>
|
||||
- one\n\n- Alex
|
||||
<ul><li>one</li><li>Alex</li></ul>
|
||||
* one\n * two
|
||||
@@ -121,6 +121,8 @@ bar <h2>foo</h2><p>bar</p>
|
||||
<table><tr><th><em style="font-style: normal; text-decoration: underline">foo</em></th></tr></table>
|
||||
foo ~~bar~~
|
||||
foo <del>bar</del>
|
||||
pay 1.-/month
|
||||
pay 1.-/month
|
||||
EOT
|
||||
|
||||
xpath_run_tests(split('\n',<<'EOT'));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2006–2015 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2006–2017 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More tests => 47;
|
||||
use Test::More tests => 48;
|
||||
|
||||
add_module('usemod.pl');
|
||||
|
||||
@@ -73,6 +73,8 @@ This is <strong>strong text containing <em>emph</em> text</strong>.
|
||||
<table class="user"><tr class="odd first"><td align="center">one <em>two</em></td></tr></table>
|
||||
|| one two ||
|
||||
<table class="user"><tr class="odd first"><td align="center">one two </td></tr></table>
|
||||
||'''''foo''''' || '''''bar''''' ||\n||baz || quux ||
|
||||
<table class="user"><tr class="odd first"><td align="left"><strong><em>foo</em></strong></td><td align="center"><strong><em>bar</em></strong></td></tr><tr class="even"><td align="left">baz </td><td align="center">quux </td></tr></table>
|
||||
introduction\n\n||one||two||three||\n||||one two||three||
|
||||
introduction<table class="user"><tr class="odd first"><td>one</td><td>two</td><td>three</td></tr><tr class="even"><td colspan="2">one two</td><td>three</td></tr></table>
|
||||
||one||two||three||\n||||one two||three||\n\nfooter
|
||||
|
||||
10
wiki.pl
10
wiki.pl
@@ -687,10 +687,10 @@ sub CloseHtmlEnvironments { # close all -- remember to use AddHtmlEnvironment('p
|
||||
}
|
||||
|
||||
sub CloseHtmlEnvironment { # close environments up to and including $html_tag
|
||||
my $html = (@_ and InElement(@_)) ? CloseHtmlEnvironmentUntil(@_) : '';
|
||||
my $html = (@_ and InElement(@_)) ? CloseHtmlEnvironmentUntil(@_) : undef;
|
||||
if (@HtmlStack and (not(@_) or defined $html)) {
|
||||
shift(@HtmlAttrStack);
|
||||
return $html . '</' . shift(@HtmlStack) . '>';
|
||||
$html .= '</' . shift(@HtmlStack) . '>';
|
||||
}
|
||||
return $html || ''; # avoid returning undefined
|
||||
}
|
||||
@@ -2522,7 +2522,7 @@ sub GetFormStart {
|
||||
sub GetSearchForm {
|
||||
my $html = GetFormStart(undef, 'get', 'search') . $q->start_p;
|
||||
$html .= $q->label({-for=>'search'}, T('Search:')) . ' '
|
||||
. $q->textfield(-name=>'search', -id=>'search', -size=>20, -accesskey=>T('f')) . ' ';
|
||||
. $q->textfield(-name=>'search', -id=>'search', -size=>15, -accesskey=>T('f')) . ' ';
|
||||
if (GetParam('search') ne '' and UserIsAdmin()) { # see DoBrowseRequest
|
||||
$html .= $q->label({-for=>'replace'}, T('Replace:')) . ' '
|
||||
. $q->textfield(-name=>'replace', -id=>'replace', -size=>20) . ' '
|
||||
@@ -2532,11 +2532,11 @@ sub GetSearchForm {
|
||||
}
|
||||
if (GetParam('matchingpages', $MatchingPages)) {
|
||||
$html .= $q->label({-for=>'matchingpage'}, T('Filter:')) . ' '
|
||||
. $q->textfield(-name=>'match', -id=>'matchingpage', -size=>20) . ' ';
|
||||
. $q->textfield(-name=>'match', -id=>'matchingpage', -size=>15) . ' ';
|
||||
}
|
||||
if (%Languages) {
|
||||
$html .= $q->label({-for=>'searchlang'}, T('Language:')) . ' '
|
||||
. $q->textfield(-name=>'lang', -id=>'searchlang', -size=>10, -default=>GetParam('lang', '')) . ' ';
|
||||
. $q->textfield(-name=>'lang', -id=>'searchlang', -size=>5, -default=>GetParam('lang', '')) . ' ';
|
||||
}
|
||||
$html .= $q->submit('dosearch', T('Go!')) . $q->end_p . $q->end_form;
|
||||
return $html;
|
||||
|
||||
Reference in New Issue
Block a user