1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-07-16 00:14:34 +02:00

Doc: Added new website front page and moved doc under /help (#5169)

This commit is contained in:
Laurent
2021-07-10 11:16:13 +01:00
committed by GitHub
parent a73b749ddd
commit 5214da0a44
53 changed files with 2837 additions and 721 deletions

View File

@ -1646,9 +1646,6 @@ packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts packages/renderer/utils.d.ts
packages/renderer/utils.js packages/renderer/utils.js
packages/renderer/utils.js.map packages/renderer/utils.js.map
packages/tools/build-website.d.ts
packages/tools/build-website.js
packages/tools/build-website.js.map
packages/tools/buildServerDocker.d.ts packages/tools/buildServerDocker.d.ts
packages/tools/buildServerDocker.js packages/tools/buildServerDocker.js
packages/tools/buildServerDocker.js.map packages/tools/buildServerDocker.js.map
@ -1679,4 +1676,19 @@ packages/tools/tool-utils.js.map
packages/tools/update-readme-sponsors.d.ts packages/tools/update-readme-sponsors.d.ts
packages/tools/update-readme-sponsors.js packages/tools/update-readme-sponsors.js
packages/tools/update-readme-sponsors.js.map packages/tools/update-readme-sponsors.js.map
packages/tools/website/build.d.ts
packages/tools/website/build.js
packages/tools/website/build.js.map
packages/tools/website/utils/plans.d.ts
packages/tools/website/utils/plans.js
packages/tools/website/utils/plans.js.map
packages/tools/website/utils/pressCarousel.d.ts
packages/tools/website/utils/pressCarousel.js
packages/tools/website/utils/pressCarousel.js.map
packages/tools/website/utils/render.d.ts
packages/tools/website/utils/render.js
packages/tools/website/utils/render.js.map
packages/tools/website/utils/types.d.ts
packages/tools/website/utils/types.js
packages/tools/website/utils/types.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

18
.gitignore vendored
View File

@ -1632,9 +1632,6 @@ packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts packages/renderer/utils.d.ts
packages/renderer/utils.js packages/renderer/utils.js
packages/renderer/utils.js.map packages/renderer/utils.js.map
packages/tools/build-website.d.ts
packages/tools/build-website.js
packages/tools/build-website.js.map
packages/tools/buildServerDocker.d.ts packages/tools/buildServerDocker.d.ts
packages/tools/buildServerDocker.js packages/tools/buildServerDocker.js
packages/tools/buildServerDocker.js.map packages/tools/buildServerDocker.js.map
@ -1665,4 +1662,19 @@ packages/tools/tool-utils.js.map
packages/tools/update-readme-sponsors.d.ts packages/tools/update-readme-sponsors.d.ts
packages/tools/update-readme-sponsors.js packages/tools/update-readme-sponsors.js
packages/tools/update-readme-sponsors.js.map packages/tools/update-readme-sponsors.js.map
packages/tools/website/build.d.ts
packages/tools/website/build.js
packages/tools/website/build.js.map
packages/tools/website/utils/plans.d.ts
packages/tools/website/utils/plans.js
packages/tools/website/utils/plans.js.map
packages/tools/website/utils/pressCarousel.d.ts
packages/tools/website/utils/pressCarousel.js
packages/tools/website/utils/pressCarousel.js.map
packages/tools/website/utils/render.d.ts
packages/tools/website/utils/render.js
packages/tools/website/utils/render.js.map
packages/tools/website/utils/types.d.ts
packages/tools/website/utils/types.js
packages/tools/website/utils/types.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@ -24,6 +24,15 @@ a {
padding-bottom: 225px; padding-bottom: 225px;
} }
.press-carousel .photo {
width: 123px;
height: 136px;
}
.press-carousel a {
color: inherit;
}
.fw400 { .fw400 {
font-weight: 400; font-weight: 400;
} }
@ -94,6 +103,25 @@ a {
font-size: 16px !important; font-size: 16px !important;
} }
.donate-button i {
font-size: 0.8em;
}
.donate-button .heart-full {
position: absolute;
opacity: 0;
}
.donate-button:hover .heart-full {
position: static;
opacity: 1;
}
.donate-button:hover .heart-line {
position: absolute;
opacity: 0;
}
a.heading-anchor { a.heading-anchor {
display: inline-block; display: inline-block;
opacity: 0; opacity: 0;
@ -145,15 +173,27 @@ h2 {
margin-bottom: 0.6em; margin-bottom: 0.6em;
margin-top: 1.1em; margin-top: 1.1em;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
line-height: 2em; line-height: 1.3em;
/* line-height: 57.6px; /* line-height: 57.6px;
font-weight: 600; font-weight: 600;
margin-bottom: 20px; */ margin-bottom: 20px; */
} }
.front-page h1 {
font-size: 3em;
}
.front-page h2 {
font-size: 2.5em;
border-bottom: none;
}
.front-page p {
font-size: 1.3em;
}
p, p,
.button-link { .button-link {
/* font-size: 22px; */
line-height: 30.8px; line-height: 30.8px;
font-weight: 400; font-weight: 400;
text-decoration: none; text-decoration: none;
@ -161,13 +201,14 @@ p,
} }
.button-link { .button-link {
font-size: 18px;
text-align: center; text-align: center;
min-width: 300px; /* min-width: 300px; */
display: inline-block; display: inline-block;
font-weight: 500; font-weight: 500;
border: solid 1px #fff; border: solid 1px #fff;
border-radius: 40px; border-radius: 40px;
padding: 20px 30px; padding: 8px 20px;
color: #fff; color: #fff;
} }
@ -225,9 +266,10 @@ p,
width: 100%; width: 100%;
z-index: 9; z-index: 9;
} }
#nav-section a { #nav-section a {
display: inline-block; display: inline-block;
margin-left: 50px; margin-left: 30px;
text-decoration: none; text-decoration: none;
} }
@ -237,6 +279,17 @@ p,
text-decoration: underline; text-decoration: underline;
} }
#menu-mobile a:hover, #menu-mobile a:focus {
color: #0557ba;
}
.navbar-main .plans-button,
#menu-mobile .plans-button {
color: #0557ba;
border-color: #0557ba;
/* margin-bottom: 10px; */
}
#nav-section.white-bg a { #nav-section.white-bg a {
color: #0557ba; color: #0557ba;
} }
@ -247,11 +300,11 @@ p,
text-decoration: underline; text-decoration: underline;
} }
#nav-section .button-link { /* #nav-section .button-link {
padding: 15px; padding: 15px;
min-width: 200px; min-width: 200px;
text-decoration: none; text-decoration: none;
} } */
#nav-section { #nav-section {
box-shadow: 0 3px 11px 0 rgba(0,0,0,0.1); box-shadow: 0 3px 11px 0 rgba(0,0,0,0.1);
@ -266,6 +319,43 @@ p,
align-items: center; align-items: center;
} }
#sponsors-section .sponsor-github-item {
display: inline-flex;
flex-direction: column;
align-items: center;
}
#sponsors-section .sponsors-github,
#sponsors-section .sponsors-org {
margin-left: auto;
margin-right: auto;
max-width: 900px;
padding-bottom: 50px;
}
#sponsors-section .sponsors-org {
display: flex;
justify-content: center;
flex-flow: wrap;
}
#sponsors-section .sponsors-org .sponsor-org-item {
display: inline-flex;
align-items: center;
margin-left: 15px;
margin-bottom: 10px;
}
#sponsors-section .sponsors-org .sponsor-org-item img {
max-width: 256px;
max-height: 80px;
}
#sponsors-section .sponsors-github .sponsor-github-item {
margin-left: 10px;
margin-bottom: 10px;
}
/* top-section */ /* top-section */
#top-section { #top-section {
@ -274,7 +364,12 @@ p,
background-repeat: no-repeat, no-repeat; background-repeat: no-repeat, no-repeat;
background-position: left, right bottom; background-position: left, right bottom;
background-size: contain; background-size: contain;
padding-top: 150px; padding-top: 100px;
}
#top-section .download-button {
margin-right: 10px;
margin-bottom: 10px;
} }
#top-section .frame-bg { #top-section .frame-bg {
@ -292,7 +387,7 @@ p,
#top-section-img { #top-section-img {
margin-bottom: -280px; margin-bottom: -280px;
margin-top: 70px; margin-top: 40px;
} }
.main-content { .main-content {
@ -343,8 +438,8 @@ p,
/* customise-it-section */ /* customise-it-section */
#customise-it-section { #customise-it-section {
padding-top: 120px; padding-top: 40px;
padding-bottom: 150px; padding-bottom: 60px;
background-image: url("../images/customise-it-bg.png"); background-image: url("../images/customise-it-bg.png");
background-position: 100% 80%; background-position: 100% 80%;
background-repeat: no-repeat; background-repeat: no-repeat;
@ -355,16 +450,20 @@ p,
margin-top: 50px; margin-top: 50px;
} }
.website-env-prod .alert-env-dev {
display: none;
}
/* your-data-section */ /* your-data-section */
#your-data-section { #your-data-section {
padding-top: 180px; padding-top: 80px;
padding-bottom: 100px; padding-bottom: 80px;
} }
/* in-the-press-section */ /* in-the-press-section */
#in-the-press-section { #in-the-press-section {
padding-top: 180px; padding-top: 0;
padding-bottom: 100px; padding-bottom: 50px;
background-image: url("../images/in-the-web-bg.png"); background-image: url("../images/in-the-web-bg.png");
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
@ -372,12 +471,13 @@ p,
} }
#in-the-press-section .carousel-item { #in-the-press-section .carousel-item {
min-height: 500px; min-height: 550px;
} }
#in-the-press-section .carousel-caption { #in-the-press-section .carousel-caption {
left: 0; left: 0;
right: 0; right: 0;
top: 0;
} }
#in-the-press-section .carousel-indicators [data-bs-target] { #in-the-press-section .carousel-indicators [data-bs-target] {
@ -409,6 +509,10 @@ p,
display: none; display: none;
} }
.page-download {
text-align: center;
}
@media (min-width: 767px) { @media (min-width: 767px) {
.content-wrapper{ .content-wrapper{
display: flex; display: flex;
@ -444,60 +548,6 @@ p,
margin-bottom: 10px; margin-bottom: 10px;
} }
/* price page */
.page-container {
padding-top: 100px;
padding-bottom: 50px;
}
.price-container {
border: 1px solid #4f9cf9;
box-sizing: border-box;
border-radius: 20px;
padding: 30px 20px;
padding-bottom: 30px;
margin-bottom: 50px;
margin-top: 60px;
min-height: 645px;
}
.price-container p {
font-size: 16px;
}
.price-container p.plan-type {
font-size: 22px;
}
.price-container .plan-type img {
width: 65px;
}
.price-container p.price {
font-size: 30px;
margin-top: -10px;
}
.price-container p.unchecked-text {
color: #9db8d9;
}
.price-container p {
font-size: 16px;
}
.price-container-blue {
background: linear-gradient(251.85deg, #0b4f99 -11.85%, #002d61 104.73%);
box-shadow: 0px 4px 16px rgba(105, 132, 172, 0.13);
margin-top: 30px;
min-height: 710px;
padding-top: 60px;
}
.price-container-blue p {
color: #fff;
}
/* footer section */ /* footer section */
footer { footer {
padding-top: 50px; padding-top: 50px;
@ -535,6 +585,8 @@ footer .right-links a {
} }
/* responsive */ /* responsive */
/*
@media (min-width: 1200px) and (max-width: 1799px) { @media (min-width: 1200px) and (max-width: 1799px) {
#nav-section a { #nav-section a {
margin-left: 80px; margin-left: 80px;
@ -561,19 +613,22 @@ footer .right-links a {
text-decoration: none; text-decoration: none;
} }
/* h1 { .front-page h1 {
font-size: 44px; font-size: 2.7em;
}
.front-page h2 {
font-size: 2.3em;
border-bottom: none;
}
.front-page p {
font-size: 1.1em;
} }
h2 { .front-page .button-link {
font-size: 30px; font-size: 1.1em;
line-height: 40px; }
} */
/* p,
.button-link {
font-size: 16px;
} */
.button-link { .button-link {
min-width: 200px; min-width: 200px;
@ -593,9 +648,7 @@ footer .right-links a {
#work-together-section, #work-together-section,
#save-web-section, #save-web-section,
#customise-it-section,
#your-data-section, #your-data-section,
#in-the-press-section,
#your-note-section { #your-note-section {
padding-top: 50px; padding-top: 50px;
padding-bottom: 50px; padding-bottom: 50px;
@ -624,7 +677,7 @@ footer .right-links a {
} }
@media (min-width: 1700px) { @media (min-width: 1700px) {
#price-section { #plans-section {
background-image: url("../images/price-bg-left.png"), background-image: url("../images/price-bg-left.png"),
url("../images/price-bg-right.png"); url("../images/price-bg-right.png");
background-repeat: no-repeat, no-repeat; background-repeat: no-repeat, no-repeat;
@ -633,26 +686,6 @@ footer .right-links a {
} }
} }
@media (min-width: 1400px) {
.container {
max-width: 1250px;
}
.price-container .plan-type img {
width: 85px;
}
.price-container p.price {
margin-top: 0px;
}
/* #top-section-img,
#multimedia-notes-section-img,
#save-web-img {
max-width: none;
}*/
}
@media (min-width: 992px) and (max-width: 1200px) { @media (min-width: 992px) and (max-width: 1200px) {
.price-container-blue { .price-container-blue {
min-height: 785px; min-height: 785px;
@ -691,43 +724,97 @@ footer .right-links a {
} }
#in-the-press-section .carousel-item { #in-the-press-section .carousel-item {
min-height: 600px; min-height: 650px;
}
}
*/
/*****************************************************************
IN THE PRESS
The "In the press" section height needs to be adjusted as the
window is changed so that the content is fully visible.
*****************************************************************/
@media (max-width: 1200px) {
#in-the-press-section .carousel-item {
min-height: 670px;
} }
} }
@media (max-width: 767px) {
#in-the-press-section {
padding-top: 0px;
padding-bottom: 50px;
}
#in-the-press-section h2 {
padding-bottom: 20px;
}
#in-the-press-section .carousel {
margin-top: -50px;
}
#in-the-press-section .carousel-item {
min-height: 500px;
}
}
@media (max-width: 500px) {
#in-the-press-section .carousel-item {
min-height: 700px;
}
}
@media (max-width: 300px) {
#in-the-press-section .carousel-item {
min-height: 800px;
}
}
@media (max-width: 991px) {
#nav-section a {
margin-left: 10px;
}
.plans-page {
background:none;
}
}
/*****************************************************************
NARROW VIEW
- Top right menu is displayed
- Sections are changed: columns with text, then button, then image
*****************************************************************/
@media (max-width: 767px) { @media (max-width: 767px) {
#main-container { #main-container {
position: relative; position: relative;
min-height: 100vh; min-height: 100vh;
padding-bottom: 415px; padding-bottom: 260px;
} }
/* h1 { .front-page h1 {
font-size: 34px; font-size: 2.5em;
line-height: 37.4px; }
.front-page h2 {
font-size: 2.1em;
border-bottom: none;
}
.front-page p {
font-size: 1em;
}
.front-page .button-link {
font-size: 1em;
} }
h2 {
font-size: 28px;
line-height: 33.6px;
} */
/* p {
font-size: 16px;
line-height: 25.6px;
} */
.ml-mobile-0 { .ml-mobile-0 {
margin-left: 0px; margin-left: 0px;
} }
.button-link {
display: block;
min-width: inherit;
width: 100%;
max-width: 400px;
margin: auto;
padding: 12px 20px;
}
.img-fluid { .img-fluid {
margin: auto; margin: auto;
} }
@ -769,13 +856,26 @@ footer .right-links a {
box-shadow: -3px 0px 11px 0 rgba(0,0,0,0.1); box-shadow: -3px 0px 11px 0 rgba(0,0,0,0.1);
} }
#menu-mobile a {
margin-left: 0;
}
#toc-mobile { #toc-mobile {
overflow-y: scroll; overflow-y: scroll;
text-align: left; text-align: left;
} }
#toc-mobile ul li a { #menu-mobile .menu-mobile-buttons {
margin-left: 5px; display: flex;
justify-content: center;
}
#menu-mobile .menu-mobile-top {
margin-bottom: 1em;
}
#menu-mobile .menu-mobile-buttons .donate-button {
margin-left: .5em;
} }
#toc-mobile ul { #toc-mobile ul {
@ -785,7 +885,7 @@ footer .right-links a {
#toc-mobile ul > li > p { #toc-mobile ul > li > p {
font-weight: bold; font-weight: bold;
text-align: center; /* text-align: center; */
margin-top: 1em; margin-top: 1em;
} }
@ -811,7 +911,7 @@ footer .right-links a {
} }
#menu-mobile .button-link { #menu-mobile .button-link {
padding: 10px; padding: 10px 15px;
font-size: 16px; font-size: 16px;
margin-left: 0px; margin-left: 0px;
} }
@ -827,7 +927,7 @@ footer .right-links a {
#top-section, #top-section,
.page-container { .page-container {
padding-top: 120px; padding-top: 80px;
} }
#top-section-img { #top-section-img {
@ -836,7 +936,7 @@ footer .right-links a {
} }
#work-together-section { #work-together-section {
padding-top: 100px; padding-top: 10px;
padding-bottom: 0px; padding-bottom: 0px;
} }
@ -846,40 +946,169 @@ footer .right-links a {
#save-web-section, #save-web-section,
#customise-it-section, #customise-it-section,
#your-data-section { #your-data-section {
padding-top: 30px; padding-top: 10px;
padding-bottom: 50px; padding-bottom: 50px;
background-image: inherit; background-image: inherit;
} }
#your-note-section .button-link {
margin-bottom: 0px;
}
#in-the-press-section {
padding-top: 30px;
padding-bottom: 50px;
}
#in-the-press-section .carousel {
margin-top: -50px;
}
#in-the-press-section .carousel-item {
min-height: 480px;
}
footer { footer {
padding-top: 30px; padding-top: 30px;
padding-bottom: 30px; padding-bottom: 30px;
} }
} }
@media (max-width: 576px) { /*****************************************************************
#in-the-press-section .carousel { PLANS PAGE
margin-top: -20px; *****************************************************************/
/* .plans-page {
background-image: url("../images/price-bg-left.png"),
url("../images/price-bg-right.png");
background-repeat: no-repeat, no-repeat;
background-position: left, right;
background-size: contain;
} */
/* #plans-section.env-prod .alert-env-mode {
display: none;
} */
#plans-section .sub-title {
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.plans-page h1 {
font-size: 2.2em;
margin-top:0;
}
.plans-page .title-box p {
margin-bottom: 0;
}
.page-container {
padding-top: 100px;
padding-bottom: 50px;
}
.price-container {
border: 1px solid #4f9cf9;
background-color: rgba(255,255,255,0.9);
box-sizing: border-box;
border-radius: 20px;
padding: 30px 20px;
padding-bottom: 30px;
margin-bottom: 50px;
margin-top: 60px;
}
.price-container p {
font-size: 16px;
}
.price-container p.price {
font-size: 25px;
margin-top: -10px;
}
.price-container p.unchecked-text {
color: #9db8d9;
}
.price-container p {
font-size: 16px;
}
.price-container-blue {
background: linear-gradient(251.85deg, #0b4f99 -11.85%, #002d61 104.73%);
box-shadow: 0px 4px 16px rgba(105, 132, 172, 0.13);
margin-top: 40px;
padding-top: 50px;
color: white;
}
.price-container .subscribe-wrapper {
margin-top: 2em;
}
.price-row {
display: flex;
flex-direction: row;
margin-bottom: 1em;
}
.price-row .plan-type {
display: flex;
align-items: center;
font-size: 20px;
}
.price-row .plan-type img {
width: 65px;
}
.price-row .plan-price {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
font-size: 25px;
font-weight: bold;
}
.price-row .plan-price .per-month {
font-size: .5em;
font-weight: normal;
}
.price-container-blue p {
color: #fff;
}
.price-container .feature {
font-size: 0.9em;
margin-right: .5em;
color: #4F9CF9;
}
.price-container .feature-off {
opacity: 0.4;
}
@media (max-width: 1400px) {
.price-row .plan-type {
font-size: 16px;
}
.price-row .plan-type img {
width: 40px;
}
.price-row .plan-price {
font-size: 20px;
} }
} }
@media (max-width: 1000px) {
.price-row .plan-type {
font-size: 20px;
}
.price-row .plan-type img {
width: 65px;
}
.price-row .plan-price {
font-size: 25px;
}
}
/*****************************************************************
MOBILE VIEW
*****************************************************************/
@media (max-width: 400px) { @media (max-width: 400px) {
#top-section { #top-section {
background-image: url("../images/top-bg-mobile.png"); background-image: url("../images/top-bg-mobile.png");

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -1,8 +1,50 @@
function getOs() {
if (navigator.appVersion.indexOf("Win")!=-1) return "windows";
if (navigator.appVersion.indexOf("Mac")!=-1) return "macOs";
if (navigator.appVersion.indexOf("X11")!=-1) return "linux";
if (navigator.appVersion.indexOf("Linux")!=-1) return "linux";
return null;
}
function setupMobileMenu() {
$("#open-menu-mobile").click(function () {
$("#menu-mobile").animate({ "margin-right": "0px" }, 300);
});
$("#close-menu-mobile").click(function () {
$("#menu-mobile").animate({ "margin-right": "-300px" }, 300);
});
}
function setupDownloadPage() {
if (!$('.page-download').length) return;
const downloadLinks = {};
$('.page-download .get-it-desktop a').each(function() {
const href = $(this).attr('href');
if (href.indexOf('-Setup') > 0) downloadLinks['windows'] = href;
if (href.indexOf('.dmg') > 0) downloadLinks['macOs'] = href;
if (href.indexOf('.AppImage') > 0) downloadLinks['linux'] = href;
});
$('.page-download .get-it-desktop').hide();
$('.page-download .download-click-here').click((event) => {
event.preventDefault();
$('.page-download .get-it-desktop').show(500);
});
const os = getOs();
if (!os || !downloadLinks[os]) {
$('.page-download .get-it-desktop').show();
} else {
window.location = downloadLinks[os];
}
}
$(function () { $(function () {
$("#open-menu-mobile").click(function () { setupMobileMenu();
$("#menu-mobile").animate({ "margin-right": "0px" }, 300); setupDownloadPage();
});
$("#close-menu-mobile").click(function () {
$("#menu-mobile").animate({ "margin-right": "-300px" }, 300);
});
}); });

View File

@ -0,0 +1,409 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta
charset="utf-8"
http-equiv="X-UA-Compatible"
content="IE=edge,chrome=1"
/>
<link rel="icon" href="{{imageBaseUrl}}/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Jopli website" />
<link rel="stylesheet" href="{{cssBaseUrl}}/fontawesome-all.min.css">
<link
rel="stylesheet"
href="{{cssBaseUrl}}/bootstrap5.0.2.min.css"
as="style"
/>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap"
rel="stylesheet"
as="style"
media="all"
onload="this.media='all'; this.onload = null"
/>
<link rel="stylesheet" href="{{cssBaseUrl}}/site.css" as="style" />
<title>Joplin</title>
</head>
<body class="front-page website-env-{{env}}">
<div class="container-fluid" id="main-container">
{{#navbar}}
{{> navbar}}
{{/navbar}}
<div class="blue-bg" id="top-section">
<div class="container">
<div class="row">
<div class="col-12">
<div class="alert alert-danger alert-env-dev" role="alert">
Running in {{env}} mode!
</div>
<h1 class="text-center">
Free your <span class="frame-bg frame-bg-blue">notes</span>
</h1>
<p class="text-center" id="top-section-text">
Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device.
</p>
<br />
<br />
<p class="text-center">
<a href="{{baseUrl}}/download/" class="button-link btn-blue download-button">Download the app</a>
{{#showJoplinCloudLinks}}
<a href="{{baseUrl}}/plans/" class="button-link btn-trans plans-button">Sign up with Joplin Cloud</a>
{{/showJoplinCloudLinks}}
</p>
<img
src="{{imageBaseUrl}}/home-top-img.png"
alt=""
class="img-fluid img-center"
id="top-section-img"
/>
</div>
</div>
</div>
</div>
<div id="multimedia-notes-section">
<div class="container">
<div class="row">
<div class="col-12 col-md-5 col-xxl-6">
<div class="ml-30 ml-mobile-0">
<h2 id="multimedia-title">
Multimedia <span class="frame-bg frame-bg-yellow">notes</span>
</h2>
<p id="multimedia-text">
Images, videos, PDFs and audio files are supported. Create
math expressions and diagrams directly from the app. Take
photos with the mobile app and save them to a note.
</p>
<br />
<p>
<a href="{{baseUrl}}/download/" class="button-link btn-blue">Download the app</a>
</p>
</div>
</div>
<div class="col-12 col-md-7 col-xxl-6">
<br class="d-block d-lg-none" />
<br class="d-block d-lg-none" />
<img
src="{{imageBaseUrl}}/multimedia-notes-img.png"
alt=""
class="img-fluid"
id="multimedia-notes-section-img"
/>
</div>
</div>
</div>
</div>
{{#showJoplinCloudLinks}}
<div id="work-together-section" class="gray-bg">
<div class="container">
<div class="row">
<div class="col-6 d-none d-md-block"></div>
<div class="col-12 col-md-6">
<div class="ml-30 ml-mobile-0">
<h2>Work together</h2>
<p>
With Joplin Cloud, share your notes with your friends, family
or colleagues and collaborate on them.
</p>
<br />
<p>
<a href="{{baseUrl}}/plans/" class="button-link btn-blue">Try it now</a>
</p>
<br class="d-block d-md-none" />
<br class="d-block d-md-none" />
<div class="text-center">
<img
src="{{imageBaseUrl}}/work-together-img.png"
alt=""
class="img-fluid d-block-inline d-md-none"
/>
</div>
</div>
</div>
</div>
</div>
</div>
{{/showJoplinCloudLinks}}
<div id="save-web-section" class="blue-bg">
<div class="container">
<div class="row">
<div class="col-12 col-md-6">
<div class="ml-30 ml-mobile-0">
<h2 id="save-web-title">
Save web pages <br /><span class="frame-bg frame-bg-blue"
>as notes</span
>
</h2>
<p>
Use the web clipper extension, available on Chrome and
Firefox, to save web pages or take screenshots as notes.
</p>
<br />
<p>
<a href="{{baseUrl}}/clipper/" class="button-link btn-blue">Get the clipper</a>
</p>
</div>
</div>
<div class="col-12 col-md-6">
<br class="d-block d-md-none" />
<br class="d-block d-md-none" />
<img
src="{{imageBaseUrl}}/save-web-img.png"
alt=""
class="img-fluid"
id="save-web-img"
/>
</div>
</div>
</div>
</div>
<div id="customise-it-section">
<div class="container">
<div class="row">
<div class="d-none d-md-block col-md-6">
<img
src="{{imageBaseUrl}}/customise-it-img.png"
alt=""
class="img-fluid"
/>
</div>
<div class="col-12 col-md-6">
<div class="ml-30 ml-mobile-0">
<h2 id="customise-it-title">
<span class="frame-bg frame-bg-yellow-lg">Customise</span> it
<br />
to your needs
</h2>
<p>
Customise the app with plugins, custom themes and multiple
text editors (Rich Text or Markdown). Or create your own
scripts and plugins using the Extension API.
</p>
<br />
<p>
<a href="{{baseUrl}}/help/#plugins" class="button-link btn-blue">Find out more</a>
</p>
<br class="d-block d-lg-none" />
<br class="d-block d-lg-none" />
<img
src="{{imageBaseUrl}}/customise-it-img.png"
alt=""
class="img-fluid d-block d-md-none"
/>
</div>
</div>
</div>
</div>
</div>
<div id="your-data-section" class="gray-bg">
<div class="container">
<div class="row">
<div class="col-12 col-md-6">
<br class="d-block d-md-none" />
<div class="ml-30 ml-mobile-0">
<h2>100% your data</h2>
<p>
The app is open source and your notes are saved to an open
format, so you'll always have access to them. Uses EndTo-End Encryption (E2EE) to secure your notes and ensure no-one but
yourself can access them.
</p>
<br />
<p>
<a href="{{baseUrl}}/e2ee/" class="button-link btn-blue">More about E2EE</a>
</p>
</div>
</div>
<div class="col-12 col-md-6">
<br class="d-block d-md-none" />
<br class="d-block d-md-none" />
<br class="d-block d-md-none" />
<img
src="{{imageBaseUrl}}/your-data-img.png"
alt=""
class="img-fluid"
/>
</div>
</div>
</div>
</div>
<div id="in-the-press-section">
<div class="container">
<div class="row">
<div class="col-12">
<br />
<h2 class="text-center">
In the <span class="frame-bg frame-bg-yellow">Press</span>
</h2>
<br />
</div>
<div class="d-none d-md-block col-3">
<img
src="{{imageBaseUrl}}/in-the-press-img-left.png"
alt=""
class="img-fluid d-none d-md-block"
/>
</div>
<div class="d-none d-md-block col-6">
<div
id="{{pressCarouselRegular.id}}"
class="carousel slide d-none d-md-block"
data-bs-ride="carousel"
>
{{#pressCarouselRegular}}
{{> pressCarouselButtons}}
{{/pressCarouselRegular}}
<div class="carousel-inner">
{{#pressCarouselRegular.items}}
{{> pressCarouselItem}}
{{/pressCarouselRegular.items}}
</div>
</div>
</div>
<div class="d-none d-md-block col-3">
<div class="text-right">
<br />
<img
src="{{imageBaseUrl}}/in-the-press-img-right.png"
alt=""
class="img-fluid d-none d-md-inline-block"
/>
</div>
</div>
</div>
<div class="row d-block d-md-none">
<div class="col-12">
<div
id="{{pressCarouselMobile.id}}"
class="carousel slide"
data-bs-ride="carousel"
>
{{#pressCarouselMobile}}
{{> pressCarouselButtons}}
{{/pressCarouselMobile}}
<div class="carousel-inner">
{{#pressCarouselMobile.items}}
{{> pressCarouselItem}}
{{/pressCarouselMobile.items}}
</div>
</div>
</div>
</div>
</div>
</div>
<div id="your-note-section" class="blue-bg">
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="text-center">
<span class="frame-bg frame-bg-blue-lg">Your notes,</span> everywhere you are.
</h2>
<p class="text-center" id="your-note-text">
Access your notes from your computer, phone or tablet. All your
data is synced to all your devices. The app is available on Windows, macOS, Linux, Android and iOS. A terminal app is also available!
</p>
<br />
<br />
<p class="text-center">
<a href="{{baseUrl}}/download/" class="button-link btn-blue">Download it now</a>
</p>
<br />
</div>
</div>
</div>
</div>
<div id="sponsors-section" class="gray-bg">
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="text-center">
Our <span class="frame-bg frame-bg-blue-lg">sponsors</span>
</h2>
<p class="text-center" id="your-note-text">
Thank you for your support!
</p>
<br />
<div class="text-center sponsors-org">
{{#sponsors.orgs}}
<a class="sponsor-org-item" href="{{url}}"><img title="{{title}}" src="{{imageBaseUrl}}/sponsors/{{imageName}}"></a>
{{/sponsors.orgs}}
</div>
<div class="text-center sponsors-github">
{{#sponsors.github}}
<div class="sponsor-github-item">
<a href="https://github.com/{{name}}" title="{{name}}">
<img width="50" src="https://avatars2.githubusercontent.com/u/{{id}}?s=96&amp;v=4">
</a>
</div>
{{/sponsors.github}}
</div>
<br />
</div>
</div>
</div>
</div>
<footer class="darkblue-bg">
<div class="container">
<div class="row">
<div class="col-3 d-none d-md-block">
<img src="{{imageBaseUrl}}/logo-text.svg" alt="" width="150" />
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-12 col-md-6">
<a href="{{baseUrl}}">
<img
src="{{imageBaseUrl}}/logo-text.svg"
width="120"
class="img-center d-block d-md-none"
alt=""
/>
</a>
<br class="d-block d-md-none" />
<p class="text-center-sm">Copyright (C) 2016-{{yyyy}} Laurent Cozic</p>
</div>
<div class="col-12 col-md-6">
<p class="text-right text-center-sm right-links">
<a href="https://github.com/laurent22/joplin/" class="github-link"><i class="fab fa-github"></i> GitHub Repository</a>
<a href="{{baseUrl}}/privacy/">Privacy Policy</a>
</p>
</div>
</div>
</div>
</footer>
</div>
<script
src="{{jsBaseUrl}}/bootstrap5.0.2.min.js"
rel="preload"
as="script"
></script>
<script
src="{{jsBaseUrl}}/jquery-3.6.0.min.js"
rel="preload"
as="script"
></script>
<script src="{{jsBaseUrl}}/script.js"></script>
{{> analytics}}
</body>
</html>

View File

@ -39,121 +39,30 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
<link rel="stylesheet" href="{{cssBaseUrl}}/site.css" as="style" /> <link rel="stylesheet" href="{{cssBaseUrl}}/site.css" as="style" />
<title>{{pageTitle}}</title> <title>{{pageTitle}}</title>
</head> </head>
<body> <body class="website-env-{{env}}">
<div class="container-fluid" id="main-container"> <div class="container-fluid generic-template {{pageName}}-page" id="main-container">
<div class="with-profile white-bg" id="nav-section">
<div class="container">
<div class="row">
<div class="col-3">
<a href="https://joplinapp.org">
<img
src="{{imageBaseUrl}}/logo-text-blue.svg"
alt=""
id="top-logo"
width="180"
>
</a>
</div>
<div class="col-9 text-right d-none d-md-block">
<!-- <a href="#" class="fw500">Help</a> -->
<a href="https://joplinapp.org/gsoc2021/index/" class="fw500">GSoC 2021</a> {{#navbar}}
<a href="https://discourse.joplinapp.org/" class="fw500">Forum</a> {{> navbar}}
{{/navbar}}
<!--
<a href="#" class="fw500">Joplin Cloud</a>
<a class="button-link btn-blue ml-20" href="#">
<img src="{{imageBaseUrl}}/download-icon.svg" alt="" />&nbsp;
Download</a
>
-->
</div>
<div class="col-9 text-right d-block d-md-none">
<!--
<span class="pointer"
><img src="{{imageBaseUrl}}/profile-black-icon.png" alt=""
/></span>
&nbsp;&nbsp;
-->
<span class="pointer"
><img
src="{{imageBaseUrl}}/mobile-menu-black-open-icon.png"
id="open-menu-mobile"
alt=""
/></span>
&nbsp;&nbsp;
<div id="menu-mobile">
<div class="text-right">
<img
src="{{imageBaseUrl}}/close-icon.png"
alt=""
class="pointer"
id="close-menu-mobile"
/>
</div>
<!--
<div class="text-center">
<img src="{{imageBaseUrl}}/logo-text-blue.svg" alt="" />
<a href="#" class="fw500 mobile-menu-link">Contacts</a>
<a href="https://discourse.joplinapp.org/" class="fw500 mobile-menu-link">Forum</a>
<a href="#" class="fw500 mobile-menu-link">Help</a>
</div>
-->
<div id="toc-mobile">{{{tocHtml}}}</div>
<!--
<br />
<br />
<br />
<br />
<a class="button-link btn-blue" href="#">
<img src="{{imageBaseUrl}}/download-icon.svg" alt="" />
Download</a
>
<br />
<p class="light-blue mobile-menu-link-bottom text-center">
Joplin© 2021, All rights reserved
<br />
<a href="#" class="fw500">Terms & Conditions</a>
<br />
<a href="#" class="fw500">Privacy Policy</a>
</p>
-->
</div>
</div>
</div>
</div>
</div>
<div class="page-container page-{{sourceMarkdownName}}"> <div class="page-container page-{{sourceMarkdownName}}">
<div class="container"> <div class="container">
<!--
<div class="row">
<div class="col-12">
<h1 class="text-center">
Page <span class="frame-bg frame-bg-yellow">title</span>
</h1>
<p class="text-center">Hello word</p>
</div>
</div>
-->
<br />
<div class="row content-wrapper"> <div class="row content-wrapper">
<div id="toc">{{{tocHtml}}}</div> {{#showToc}}<div id="toc">{{{tocHtml}}}</div>{{/showToc}}
<div class="main-content"> <div class="main-content">
{{{contentHtml}}} <div class="alert alert-danger alert-env-dev" role="alert">
<div class="bottom-links"> Running in {{env}} mode!
<a href="https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}">
<i class="fab fa-github"></i> Improve this doc
</a>
</div> </div>
{{{contentHtml}}}
{{#showImproveThisDoc}}
<div class="bottom-links">
<a href="https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}">
<i class="fab fa-github"></i> Improve this doc
</a>
</div>
{{/showImproveThisDoc}}
</div> </div>
</div> </div>
</div> </div>
@ -165,26 +74,6 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
<div class="col-3 d-none d-md-block"> <div class="col-3 d-none d-md-block">
<img src="{{imageBaseUrl}}/logo-text.svg" alt="" width="150" /> <img src="{{imageBaseUrl}}/logo-text.svg" alt="" width="150" />
</div> </div>
<!--
<div class="col-12 col-md-6">
<p class="text-center">
<a href="#">Help</a>
&nbsp;&nbsp;&nbsp;
<a href="https://discourse.joplinapp.org/">Forum</a>
&nbsp;&nbsp;&nbsp;
<a href="#">Contacts</a>
</p>
</div>
<div class="col-12 col-md-3">
<br class="d-block d-md-none" />
<div class="text-right">
<a href="#" class="button-link btn-blue"
><img src="{{imageBaseUrl}}/download-icon.svg" alt="" />&nbsp;
Download</a
>
</div>
</div>
-->
</div> </div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
@ -200,16 +89,10 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
alt="" alt=""
/> />
<br class="d-block d-md-none" /> <br class="d-block d-md-none" />
<p class="text-center-sm">Copyright (C) 2016-{{yyyy}} Laurent Cozic, all rights reserved</p> <p class="text-center-sm">Copyright (C) 2016-{{yyyy}} Laurent Cozic</p>
</div> </div>
<div class="col-12 col-md-6 right-links"> <div class="col-12 col-md-6 right-links">
<p class="text-right text-center-sm"> <p class="text-right text-center-sm">
<!--
<a href="#">Terms & Conditions</a>
<span class="d-none d-md-inline">&nbsp;&nbsp;&nbsp;</span>
<br class="d-block d-md-none" />
<br class="d-block d-md-none" />
-->
<a href="https://github.com/laurent22/joplin/" class="github-link"><i class="fab fa-github"></i> GitHub Repository</a> <a href="https://github.com/laurent22/joplin/" class="github-link"><i class="fab fa-github"></i> GitHub Repository</a>
<a href="{{baseUrl}}/privacy/">Privacy Policy</a> <a href="{{baseUrl}}/privacy/">Privacy Policy</a>
</p> </p>
@ -226,27 +109,6 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
></script> ></script>
<script src="{{jsBaseUrl}}/script.js"></script> <script src="{{jsBaseUrl}}/script.js"></script>
<script> {{> analytics}}
function stickyHeader() {
return; // Disabled
if ($(window).scrollTop() > 179) {
$('.nav').addClass('sticky');
} else {
$('.nav').removeClass('sticky');
}
}
$(window).scroll(function() {
stickyHeader();
});
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-103586105-1', 'auto');
ga('send', 'pageview');
</script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,8 @@
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-103586105-1', 'auto');
ga('send', 'pageview');
</script>

View File

@ -0,0 +1 @@
<a href="{{baseUrl}}/plans/" class="button-link btn-trans plans-button">Joplin Cloud</a>

View File

@ -0,0 +1,68 @@
<div class="{{#isFrontPage}}navbar-frontpage blue-bg{{/isFrontPage}} {{^isFrontPage}}navbar-main white-bg{{/isFrontPage}}" id="nav-section">
<div class="container">
<div class="row">
<div class="col-3">
<a href="{{baseUrl}}/">
<img
src="{{#isFrontPage}}{{imageBaseUrl}}/logo-text.svg{{/isFrontPage}}{{^isFrontPage}}{{imageBaseUrl}}/logo-text-blue.svg{{/isFrontPage}}"
alt=""
id="top-logo"
width="180"
/>
</a>
</div>
<div class="col-9 text-right d-none d-md-block">
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
<a href="{{forumUrl}}" class="fw500">Forum</a>
{{#showJoplinCloudLinks}}
{{> joplinCloudButton}}
{{/showJoplinCloudLinks}}
{{> supportButton}}
</div>
<div class="col-9 text-right d-block d-md-none">
<span class="pointer"
><img
src="{{#isFrontPage}}{{imageBaseUrl}}/mobile-menu-open-icon.png{{/isFrontPage}}{{^isFrontPage}}{{imageBaseUrl}}/mobile-menu-black-open-icon.png{{/isFrontPage}}"
id="open-menu-mobile"
alt=""
/></span>
&nbsp;&nbsp;
<div id="menu-mobile">
<div>
<div class="text-right">
<img
src="{{imageBaseUrl}}/close-icon.png"
alt=""
class="pointer"
id="close-menu-mobile"
/>
</div>
<div class="text-center menu-mobile-top">
<a href="{{forumUrl}}" class="fw500 mobile-menu-link">Forum</a>
<a href="{{baseUrl}}/help/" class="fw500 mobile-menu-link">Help</a>
</div>
<div class="menu-mobile-buttons">
{{#showJoplinCloudLinks}}
{{> joplinCloudButton}}
{{/showJoplinCloudLinks}}
{{> supportButton}}
</div>
</div>
<div id="toc-mobile">{{{tocHtml}}}</div>
<div>
<p class="light-blue mobile-menu-link-bottom text-center">
Copyright (C) 2016-{{yyyy}} Laurent&nbsp;Cozic
<br/>
<a href="{{baseUrl}}/privacy/" class="fw500">Privacy Policy</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,52 @@
<div class="col-12 col-lg-4">
<div class="price-container {{#featured}}price-container-blue{{/featured}}">
<div class="price-row">
<div class="plan-type">
<img src="{{imageBaseUrl}}/{{iconName}}.png"/>&nbsp;{{title}}
</div>
<div class="plan-price">
{{price}}<sub class="per-month">&nbsp;/month</sub>
</div>
</div>
{{#featuresOn}}
<p><i class="fas fa-check feature feature-on"></i>{{.}}</p>
{{/featuresOn}}
{{#featuresOff}}
<p class="unchecked-text"><i class="fas fa-times feature feature-off"></i>{{.}}</p>
{{/featuresOff}}
<p class="text-center subscribe-wrapper">
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white">{{cfaLabel}}</a>
</p>
</div>
<script>
(function() {
const stripePriceId = '{{{stripePriceId}}}';
const planName = '{{{name}}}';
const buttonId = 'subscribeButton-' + planName;
const buttonElement = document.getElementById(buttonId);
if (stripePriceId) {
function handleResult() {
console.info('Redirected to checkout');
}
buttonElement.addEventListener("click", function(evt) {
evt.preventDefault();
const priceId = '{{{stripePriceId}}}';
createCheckoutSession(priceId).then(function(data) {
stripe.redirectToCheckout({
sessionId: data.sessionId
})
.then(handleResult);
});
});
}
})();
</script>
</div>

View File

@ -0,0 +1,22 @@
<div class="carousel-indicators">
<button
type="button"
data-bs-target="#{{id}}"
data-bs-slide-to="0"
class="active"
aria-current="true"
aria-label="Slide 1"
></button>
<button
type="button"
data-bs-target="#{{id}}"
data-bs-slide-to="1"
aria-label="Slide 2"
></button>
<button
type="button"
data-bs-target="#{{id}}"
data-bs-slide-to="2"
aria-label="Slide 3"
></button>
</div>

View File

@ -0,0 +1,23 @@
<div class="carousel-item {{active}} press-carousel" data-bs-interval="10000">
<img
src="{{imageBaseUrl}}/transparent-bg.png"
class="d-block w-100"
alt=""
/>
<div class="carousel-caption">
<img
src="{{imageBaseUrl}}/{{imageName}}"
alt=""
class="img-fluid img-center photo"
/>
<br />
<p class="text-center">
“{{body}}”
</p>
<br />
<p class="fw500 text-center">
<a href="{{url}}">{{source}}</a>
</p>
<p class="fw400 small text-center">By {{author}}</p>
</div>
</div>

View File

@ -0,0 +1,3 @@
<a class="button-link btn-blue donate-button" href="{{baseUrl}}/donate">
<i class="fas fa-heart heart-full"></i><i class="far fa-heart heart-line"></i>&nbsp;Support
</a>

View File

@ -0,0 +1,54 @@
<div id="plans-section" class="env-{{env}}">
<div class="container">
<div class="row">
<div class="col-12 title-box">
<h1 class="text-center">
Joplin Cloud <span class="frame-bg frame-bg-yellow">plans</span>
</h1>
<p class="text-center sub-title">
Joplin Cloud allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues.
</p>
</div>
</div>
<div class="row">
{{#plans.basic}}
{{> plan}}
{{/plans.basic}}
{{#plans.pro}}
{{> plan}}
{{/plans.pro}}
{{#plans.business}}
{{> plan}}
{{/plans.business}}
</div>
<div class="row">
{{{faqHtml}}}
</div>
</div>
<script src="https://js.stripe.com/v3/"></script>
<script>
var stripe = Stripe('{{{stripeConfig.publishableKey}}}');
var createCheckoutSession = function(priceId) {
console.info('Creating Stripe session for price:', priceId);
return fetch("{{{stripeConfig.webhookBaseUrl}}}/stripe/createCheckoutSession", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
priceId: priceId
})
}).then(function(result) {
return result.json();
});
};
</script>
</div>

View File

@ -25,7 +25,7 @@ Finally, when submitting a pull request, don't forget to [test your code](#autom
# Contributing to Joplin's translation # Contributing to Joplin's translation
Joplin is available in multiple languages thanks to the help of its users. You can help translate Joplin to your language or keep it up to date. Please read the documentation about [Localisation](https://joplinapp.org/#localisation). Joplin is available in multiple languages thanks to the help of its users. You can help translate Joplin to your language or keep it up to date. Please read the documentation about [Localisation](https://joplinapp.org/help/#localisation).
# Contributing to Joplin's code # Contributing to Joplin's code

View File

@ -12,7 +12,7 @@ The notes can be [synchronised](#synchronisation) with various cloud services in
The application is available for Windows, Linux, macOS, Android and iOS (the terminal app also works on FreeBSD). A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB). The application is available for Windows, Linux, macOS, Android and iOS (the terminal app also works on FreeBSD). A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
<div class="top-screenshot"><img src="https://joplinapp.org/images/AllClients.jpg" style="max-width: 100%; max-height: 35em;"></div> <div class="top-screenshot"><img src="https://joplinapp.org/images/home-top-img.png" style="max-width: 100%; max-height: 35em;"></div>
# Installation # Installation
@ -75,7 +75,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
| <img width="50" src="https://avatars2.githubusercontent.com/u/4862947?s=96&v=4"/></br>[chrootlogin](https://github.com/chrootlogin) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) | <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) | | <img width="50" src="https://avatars2.githubusercontent.com/u/4862947?s=96&v=4"/></br>[chrootlogin](https://github.com/chrootlogin) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) | <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | | <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) | | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | <img width="50" src="https://avatars2.githubusercontent.com/u/53228972?s=96&v=4"/></br>[wasteisobscene](https://github.com/wasteisobscene) | | | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | | |
<!-- SPONSORS --> <!-- SPONSORS -->
<!-- TOC --> <!-- TOC -->

955
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@
"buildSettingJsonSchema": "npm start --prefix=packages/app-cli -- settingschema ../../docs/schema/settings.json", "buildSettingJsonSchema": "npm start --prefix=packages/app-cli -- settingschema ../../docs/schema/settings.json",
"buildTranslations": "npm run tsc && node packages/tools/build-translation.js", "buildTranslations": "npm run tsc && node packages/tools/build-translation.js",
"buildTranslationsNoTsc": "node packages/tools/build-translation.js", "buildTranslationsNoTsc": "node packages/tools/build-translation.js",
"buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc && npm run buildSettingJsonSchema", "buildWebsite": "npm run buildApiDoc && node ./packages/tools/website/build.js && npm run buildPluginDoc && npm run buildSettingJsonSchema",
"circularDependencyCheck": "madge --warning --circular --extensions js ./", "circularDependencyCheck": "madge --warning --circular --extensions js ./",
"clean": "lerna clean -y && lerna run clean", "clean": "lerna clean -y && lerna run clean",
"dependencyTree": "madge", "dependencyTree": "madge",
@ -44,6 +44,7 @@
"updateIgnored": "gulp updateIgnoredTypeScriptBuild", "updateIgnored": "gulp updateIgnoredTypeScriptBuild",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh", "updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"watch": "lerna run watch --stream --parallel", "watch": "lerna run watch --stream --parallel",
"watch-website": "nodemon --verbose --watch Assets/WebsiteAssets --watch packages/tools/website/build.js --ext md,ts,js,mustache,css,tsx,gif,png,svg --exec \"node packages/tools/website/build.js && http-server docs -a localhost\"",
"i": "lerna add --no-bootstrap --scope" "i": "lerna add --no-bootstrap --scope"
}, },
"husky": { "husky": {
@ -68,5 +69,9 @@
"madge": "^4.0.2", "madge": "^4.0.2",
"typedoc": "^0.17.8", "typedoc": "^0.17.8",
"typescript": "^4.0.5" "typescript": "^4.0.5"
},
"dependencies": {
"http-server": "^0.12.3",
"nodemon": "^2.0.9"
} }
} }

View File

@ -180,7 +180,7 @@ async function fetchAllNotes() {
lines.push('# Searching'); lines.push('# Searching');
lines.push(''); lines.push('');
lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/#searching'); lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/help/#searching');
lines.push(''); lines.push('');
lines.push('To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-id). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title.'); lines.push('To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-id). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title.');
lines.push(''); lines.push('');

File diff suppressed because one or more lines are too long

View File

@ -106,6 +106,10 @@ async function main() {
'https://joplinapp.org', 'https://joplinapp.org',
]; ];
if (env === Env.Dev) {
corsAllowedDomains.push('http://localhost:8080');
}
function acceptOrigin(origin: string): boolean { function acceptOrigin(origin: string): boolean {
const hostname = (new URL(origin)).hostname; const hostname = (new URL(origin)).hostname;
const userContentDomain = envVariables.USER_CONTENT_BASE_URL ? (new URL(envVariables.USER_CONTENT_BASE_URL)).hostname : ''; const userContentDomain = envVariables.USER_CONTENT_BASE_URL ? (new URL(envVariables.USER_CONTENT_BASE_URL)).hostname : '';
@ -113,9 +117,12 @@ async function main() {
if (hostname === userContentDomain) return true; if (hostname === userContentDomain) return true;
const hostnameNoSub = hostname.split('.').slice(1).join('.'); const hostnameNoSub = hostname.split('.').slice(1).join('.');
// console.info('CORS check for origin', origin, 'Allowed domains', corsAllowedDomains);
if (hostnameNoSub === userContentDomain) return true; if (hostnameNoSub === userContentDomain) return true;
if (corsAllowedDomains.indexOf(origin) === 0) return true; if (corsAllowedDomains.includes(origin)) return true;
return false; return false;
} }

View File

@ -1,5 +1,5 @@
import { rtrimSlashes } from '@joplin/lib/path-utils'; import { rtrimSlashes } from '@joplin/lib/path-utils';
import { Config, DatabaseConfig, DatabaseConfigClient, Env, MailerConfig, RouteType, StripeConfig } from './utils/types'; import { Config, DatabaseConfig, DatabaseConfigClient, Env, MailerConfig, RouteType, StripeConfig, StripePublicConfig } from './utils/types';
import * as pathUtils from 'path'; import * as pathUtils from 'path';
import { readFile } from 'fs-extra'; import { readFile } from 'fs-extra';
@ -33,7 +33,6 @@ export interface EnvVariables {
SQLITE_DATABASE?: string; SQLITE_DATABASE?: string;
STRIPE_SECRET_KEY?: string; STRIPE_SECRET_KEY?: string;
STRIPE_PUBLISHABLE_KEY?: string;
STRIPE_WEBHOOK_SECRET?: string; STRIPE_WEBHOOK_SECRET?: string;
SIGNUP_ENABLED?: string; SIGNUP_ENABLED?: string;
@ -96,10 +95,10 @@ function mailerConfigFromEnv(env: EnvVariables): MailerConfig {
}; };
} }
function stripeConfigFromEnv(env: EnvVariables): StripeConfig { function stripeConfigFromEnv(publicConfig: StripePublicConfig, env: EnvVariables): StripeConfig {
return { return {
...publicConfig,
secretKey: env.STRIPE_SECRET_KEY || '', secretKey: env.STRIPE_SECRET_KEY || '',
publishableKey: env.STRIPE_PUBLISHABLE_KEY || '',
webhookSecret: env.STRIPE_WEBHOOK_SECRET || '', webhookSecret: env.STRIPE_WEBHOOK_SECRET || '',
}; };
} }
@ -129,6 +128,9 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
const rootDir = pathUtils.dirname(__dirname); const rootDir = pathUtils.dirname(__dirname);
const packageJson = await readPackageJson(`${rootDir}/package.json`); const packageJson = await readPackageJson(`${rootDir}/package.json`);
const stripePublicConfigs = JSON.parse(await readFile(`${rootDir}/stripeConfig.json`, 'utf8'));
const stripePublicConfig = stripePublicConfigs[envType];
if (!stripePublicConfig) throw new Error('Could not load Stripe config');
const viewDir = `${rootDir}/src/views`; const viewDir = `${rootDir}/src/views`;
const appPort = env.APP_PORT ? Number(env.APP_PORT) : 22300; const appPort = env.APP_PORT ? Number(env.APP_PORT) : 22300;
@ -145,7 +147,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
logDir: `${rootDir}/logs`, logDir: `${rootDir}/logs`,
database: databaseConfigFromEnv(runningInDocker_, env), database: databaseConfigFromEnv(runningInDocker_, env),
mailer: mailerConfigFromEnv(env), mailer: mailerConfigFromEnv(env),
stripe: stripeConfigFromEnv(env), stripe: stripeConfigFromEnv(stripePublicConfig, env),
port: appPort, port: appPort,
baseUrl, baseUrl,
showErrorStackTraces: (env.ERROR_STACK_TRACES === undefined && envType === Env.Dev) || env.ERROR_STACK_TRACES === '1', showErrorStackTraces: (env.ERROR_STACK_TRACES === undefined && envType === Env.Dev) || env.ERROR_STACK_TRACES === '1',

View File

@ -41,6 +41,12 @@ interface CreateCheckoutSessionFields {
priceId: string; priceId: string;
} }
function priceIdToAccountType(priceId: string): AccountType {
if (stripeConfig().basicPriceId === priceId) return AccountType.Basic;
if (stripeConfig().proPriceId === priceId) return AccountType.Pro;
throw new Error(`Unknown price ID: ${priceId}`);
}
type StripeRouteHandler = (stripe: Stripe, path: SubPath, ctx: AppContext)=> Promise<any>; type StripeRouteHandler = (stripe: Stripe, path: SubPath, ctx: AppContext)=> Promise<any>;
const postHandlers: Record<string, StripeRouteHandler> = { const postHandlers: Record<string, StripeRouteHandler> = {
@ -61,6 +67,9 @@ const postHandlers: Record<string, StripeRouteHandler> = {
quantity: 1, quantity: 1,
}, },
], ],
subscription_data: {
trial_period_days: 14,
},
// {CHECKOUT_SESSION_ID} is a string literal; do not change it! // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
// the actual Session ID is returned in the query parameter when your customer // the actual Session ID is returned in the query parameter when your customer
// is redirected to the success page. // is redirected to the success page.
@ -68,11 +77,32 @@ const postHandlers: Record<string, StripeRouteHandler> = {
cancel_url: `${globalConfig().baseUrl}/stripe/cancel`, cancel_url: `${globalConfig().baseUrl}/stripe/cancel`,
}); });
logger.info('Created checkout session', session.id);
// Somehow Stripe doesn't send back the price ID to the hook when the
// subscription is created, so we keep a map of sessions to price IDs so that we
// can create the right account, either Basic or Pro.
await ctx.joplin.models.keyValue().setValue(`stripeSessionToPriceId::${session.id}`, priceId);
return { return {
sessionId: session.id, sessionId: session.id,
}; };
}, },
// How to test the complete workflow locally:
//
// - In website/build.ts, set the env to "dev", then build the website - `npm run watch-website`
// - Start the Stripe CLI tool: `stripe listen --forward-to http://joplincloud.local:22300/stripe/webhook`
// - Copy the webhook secret, and paste it in joplin-credentials/server.env (under STRIPE_WEBHOOK_SECRET)
// - Start the local Joplin Server, `npm run start-dev`, running under http://joplincloud.local:22300
// - Start the workflow from http://localhost:8080/plans/
// - The local website often is not configured to send email, but you can see them in the database, in the "emails" table.
//
// Stripe config:
//
// - The public config is under packages/server/stripeConfig.json
// - The private config is in the server .env file
webhook: async (stripe: Stripe, _path: SubPath, ctx: AppContext) => { webhook: async (stripe: Stripe, _path: SubPath, ctx: AppContext) => {
const event = await stripeEvent(stripe, ctx.req); const event = await stripeEvent(stripe, ctx.req);
@ -130,6 +160,24 @@ const postHandlers: Record<string, StripeRouteHandler> = {
// } // }
const checkoutSession: Stripe.Checkout.Session = event.data.object as Stripe.Checkout.Session; const checkoutSession: Stripe.Checkout.Session = event.data.object as Stripe.Checkout.Session;
const userEmail = checkoutSession.customer_details.email || checkoutSession.customer_email;
logger.info('Checkout session completed:', checkoutSession.id);
logger.info('User email:', userEmail);
let accountType = AccountType.Basic;
try {
const priceId: string = await ctx.joplin.models.keyValue().value(`stripeSessionToPriceId::${checkoutSession.id}`);
accountType = priceIdToAccountType(priceId);
logger.info('Price ID:', priceId);
} catch (error) {
// We don't want this part to fail since the user has
// already paid at that point, so we just default to Basic
// in that case. Normally it shoud not happen anyway.
logger.error('Could not determine account type from price ID - defaulting to "Basic"', error);
}
logger.info('Account type:', accountType);
// The Stripe TypeScript object defines "customer" and // The Stripe TypeScript object defines "customer" and
// "subscription" as various types but they are actually // "subscription" as various types but they are actually
@ -138,8 +186,8 @@ const postHandlers: Record<string, StripeRouteHandler> = {
const stripeSubscriptionId = checkoutSession.subscription as string; const stripeSubscriptionId = checkoutSession.subscription as string;
await ctx.joplin.models.subscription().saveUserAndSubscription( await ctx.joplin.models.subscription().saveUserAndSubscription(
checkoutSession.customer_details.email || checkoutSession.customer_email, userEmail,
AccountType.Pro, accountType,
stripeUserId, stripeUserId,
stripeSubscriptionId stripeSubscriptionId
); );

View File

@ -75,9 +75,15 @@ export interface MailerConfig {
noReplyEmail: string; noReplyEmail: string;
} }
export interface StripeConfig { export interface StripePublicConfig {
secretKey: string;
publishableKey: string; publishableKey: string;
basicPriceId: string;
proPriceId: string;
webhookBaseUrl: string;
}
export interface StripeConfig extends StripePublicConfig {
secretKey: string;
webhookSecret: string; webhookSecret: string;
} }

View File

@ -0,0 +1,14 @@
{
"dev": {
"publishableKey": "pk_test_51IvkOPLx4fybOTqJetV23Y5S9YHU9KoOtE6Ftur0waWoWahkHdENjDKSVcl7v3y8Y0Euv7Uwd7O7W4UFasRwd0wE00MPcprz9Q",
"basicPriceId": "price_1JAx31Lx4fybOTqJRcGdsSfg",
"proPriceId": "price_1JAx1eLx4fybOTqJ5VhkxaKC",
"webhookBaseUrl": "http://joplincloud.local:22300"
},
"prod": {
"publishableKey": "pk_live_51IvkOPLx4fybOTqJow8RFsWs0eDznPeBlXMw6s8SIDQeCM8bAFNYlBdDsyonAwRcJgBCoSlvFzAbhJgLFxzzTu4r0006aw846C",
"basicPriceId": "price_1JAzWBLx4fybOTqJw64zxJRJ",
"proPriceId": "price_1JB1OVLx4fybOTqJOvp3NGM6",
"webhookBaseUrl": "https://joplincloud.com"
}
}

View File

@ -1,302 +0,0 @@
import * as fs from 'fs-extra';
const dirname = require('path').dirname;
const Mustache = require('mustache');
const glob = require('glob');
const MarkdownIt = require('markdown-it');
const path = require('path');
interface TemplateParams {
baseUrl?: string;
imageBaseUrl?: string;
cssBaseUrl?: string;
jsBaseUrl?: string;
tocHtml?: string;
sourceMarkdownFile?: string;
title?: string;
donateLinksMd?: string;
pageTitle?: string;
yyyy? : string;
}
const rootDir = dirname(dirname(__dirname));
const websiteAssetDir = `${rootDir}/Assets/WebsiteAssets`;
const mainTemplateHtml = fs.readFileSync(`${websiteAssetDir}/templates/main-new.mustache`, 'utf8');
function markdownToHtml(md: string, templateParams: TemplateParams): string {
const markdownIt = new MarkdownIt({
breaks: true,
linkify: true,
html: true,
});
markdownIt.core.ruler.push('tableClass', (state: any) => {
const tokens = state.tokens;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'table_open') {
token.attrs = [
['class', 'table'],
];
}
// if (token.type === 'heading_open') {
// insideHeading = true;
// continue;
// }
// if (token.type === 'heading_close') {
// insideHeading = false;
// continue;
// }
// if (insideHeading && token.type === 'inline') {
// const anchorName = headingTextToAnchorName(token.content, doneNames);
// doneNames.push(anchorName);
// const anchorTokens = createAnchorTokens(anchorName);
// // token.children = anchorTokens.concat(token.children);
// token.children = token.children.concat(anchorTokens);
// }
}
});
// console.info('iiiiiiiiiiiiiiiiii');
// markdownIt.renderer.rules.image = function (tokens:any[], idx:number, options:any, env:any, self:any) {
// const defaultRender = markdownIt.renderer.rules.image;
// const token = tokens[idx];
// console.info('AAAAAAAAAAA', tokens);
// return defaultRender(tokens, idx, options, env, self);
// }
markdownIt.core.ruler.push('checkbox', (state: any) => {
const tokens = state.tokens;
const Token = state.Token;
const doneNames = [];
const headingTextToAnchorName = (text: string, doneNames: string[]) => {
const allowed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let lastWasDash = true;
let output = '';
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (allowed.indexOf(c) < 0) {
if (lastWasDash) continue;
lastWasDash = true;
output += '-';
} else {
lastWasDash = false;
output += c;
}
}
output = output.toLowerCase();
while (output.length && output[output.length - 1] === '-') {
output = output.substr(0, output.length - 1);
}
let temp = output;
let index = 1;
while (doneNames.indexOf(temp) >= 0) {
temp = `${output}-${index}`;
index++;
}
output = temp;
return output;
};
const createAnchorTokens = (anchorName: string) => {
const output = [];
{
const token = new Token('heading_anchor_open', 'a', 1);
token.attrs = [
['name', anchorName],
['href', `#${anchorName}`],
['class', 'heading-anchor'],
];
output.push(token);
}
{
const token = new Token('text', '', 0);
token.content = '🔗';
output.push(token);
}
{
const token = new Token('heading_anchor_close', 'a', -1);
output.push(token);
}
return output;
};
let insideHeading = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'heading_open') {
insideHeading = true;
continue;
}
if (token.type === 'heading_close') {
insideHeading = false;
continue;
}
if (insideHeading && token.type === 'inline') {
const anchorName = headingTextToAnchorName(token.content, doneNames);
doneNames.push(anchorName);
const anchorTokens = createAnchorTokens(anchorName);
// token.children = anchorTokens.concat(token.children);
token.children = token.children.concat(anchorTokens);
}
}
});
return Mustache.render(mainTemplateHtml, {
...templateParams,
contentHtml: markdownIt.render(md),
});
}
let tocMd_: string = null;
let tocHtml_: string = null;
const tocRegex_ = /<!-- TOC -->([^]*)<!-- TOC -->/;
function tocMd() {
if (tocMd_) return tocMd_;
const md = fs.readFileSync(`${rootDir}/README.md`, 'utf8');
const toc = md.match(tocRegex_);
tocMd_ = toc[1];
return tocMd_;
}
const donateLinksRegex_ = /<!-- DONATELINKS -->([^]*)<!-- DONATELINKS -->/;
async function getDonateLinks() {
const md = await fs.readFile(`${rootDir}/README.md`, 'utf8');
const matches = md.match(donateLinksRegex_);
if (!matches) throw new Error('Cannot fetch donate links');
return matches[1].trim();
}
function replaceGitHubByJoplinAppLinks(md: string) {
// let output = md.replace(/https:\/\/github.com\/laurent22\/joplin\/blob\/master\/readme\/(.*?)\/index\.md(#[^\s)]+|)/g, 'https://joplinapp.org/$1');
return md.replace(/https:\/\/github.com\/laurent22\/joplin\/blob\/dev\/readme\/(.*?)\.md(#[^\s)]+|)/g, 'https://joplinapp.org/$1/$2');
}
function tocHtml() {
if (tocHtml_) return tocHtml_;
const markdownIt = new MarkdownIt();
let md = tocMd();
md = md.replace(/# Table of contents/, '');
md = replaceGitHubByJoplinAppLinks(md);
tocHtml_ = markdownIt.render(md);
tocHtml_ = `<div>${tocHtml_}</div>`;
return tocHtml_;
}
function renderMdToHtml(md: string, targetPath: string, templateParams: TemplateParams) {
// Remove the header because it's going to be added back as HTML
md = md.replace(/# Joplin\n/, '');
templateParams.baseUrl = '';// 'https://joplinapp.org';
templateParams.imageBaseUrl = `${templateParams.baseUrl}/images`;
templateParams.cssBaseUrl = `${templateParams.baseUrl}/css`;
templateParams.jsBaseUrl = `${templateParams.baseUrl}/js`;
templateParams.tocHtml = tocHtml();
templateParams.yyyy = (new Date()).getFullYear().toString();
const title = [];
if (!templateParams.title) {
title.push('Joplin - an open source note taking and to-do application with synchronisation capabilities');
} else {
title.push(templateParams.title);
title.push('Joplin');
}
md = replaceGitHubByJoplinAppLinks(md);
if (templateParams.donateLinksMd) {
md = `${templateParams.donateLinksMd}\n\n* * *\n\n${md}`;
}
templateParams.pageTitle = title.join(' | ');
const html = markdownToHtml(md, templateParams);
const folderPath = dirname(targetPath);
fs.mkdirpSync(folderPath);
fs.writeFileSync(targetPath, html);
}
async function readmeFileTitle(sourcePath: string) {
const md = await fs.readFile(sourcePath, 'utf8');
const r = md.match(/(^|\n)# (.*)/);
if (!r) {
throw new Error(`Could not determine title for Markdown file: ${sourcePath}`);
} else {
return r[2];
}
}
function renderFileToHtml(sourcePath: string, targetPath: string, templateParams: TemplateParams) {
const md = fs.readFileSync(sourcePath, 'utf8');
return renderMdToHtml(md, targetPath, templateParams);
}
function makeHomePageMd() {
let md = fs.readFileSync(`${rootDir}/README.md`, 'utf8');
md = md.replace(tocRegex_, '');
// HACK: GitHub needs the \| or the inline code won't be displayed correctly inside the table,
// while MarkdownIt doesn't and will in fact display the \. So we remove it here.
md = md.replace(/\\\| bash/g, '| bash');
return md;
}
async function main() {
await fs.remove(`${rootDir}/docs`);
await fs.copy(websiteAssetDir, `${rootDir}/docs`);
renderMdToHtml(makeHomePageMd(), `${rootDir}/docs/index.html`, { sourceMarkdownFile: 'README.md' });
const mdFiles = glob.sync(`${rootDir}/readme/**/*.md`, {
ignore: [
// '**/node_modules/**',
],
}).map((f: string) => f.substr(rootDir.length + 1));
const sources = [];
const donateLinksMd = await getDonateLinks();
for (const mdFile of mdFiles) {
const title = await readmeFileTitle(`${rootDir}/${mdFile}`);
const targetFilePath = `${mdFile.replace(/\.md/, '').replace(/readme\//, 'docs/')}/index.html`;
sources.push([mdFile, targetFilePath, {
title: title,
donateLinksMd: donateLinksMd,
}]);
}
for (const source of sources) {
source[2].sourceMarkdownFile = source[0];
source[2].sourceMarkdownName = path.basename(source[0], path.extname(source[0]));
renderFileToHtml(`${rootDir}/${source[0]}`, `${rootDir}/${source[1]}`, source[2]);
}
}
main().catch((error) => {
console.error(error);
});

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/tools", "name": "@joplin/tools",
"version": "2.1.1", "version": "2.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -101,6 +101,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/mustache": {
"version": "0.8.32",
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-0.8.32.tgz",
"integrity": "sha512-RTVWV485OOf4+nO2+feurk0chzHkSjkjALiejpHltyuMf/13fGymbbNNFrSKdSSUg1TIwzszXdWsVirxgqYiFA==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "14.14.6", "version": "14.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",

View File

@ -39,6 +39,7 @@
"@rmp135/sql-ts": "^1.6.0", "@rmp135/sql-ts": "^1.6.0",
"@types/fs-extra": "^9.0.6", "@types/fs-extra": "^9.0.6",
"@types/node": "^14.14.6", "@types/node": "^14.14.6",
"@types/mustache": "^0.8.32",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"sqlite3": "^5.0.0", "sqlite3": "^5.0.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"

View File

@ -24,10 +24,6 @@
"name": "c-nagy", "name": "c-nagy",
"id": "3061769" "id": "3061769"
}, },
{
"name": "wasteisobscene",
"id": "53228972"
},
{ {
"name": "mcejp", "name": "mcejp",
"id": "29300939" "id": "29300939"
@ -76,5 +72,22 @@
"name": "konishi-t", "name": "konishi-t",
"id": "24908652" "id": "24908652"
} }
],
"orgs": [
{
"url": "https://seirei.ne.jp",
"title": "Serei Network",
"imageName": "SeireiNetwork.png"
},
{
"url": "https://usrigging.com/",
"title": "U.S. Ringing Supply",
"imageName": "RingingSupply.svg"
},
{
"url": "https://tranio.com/italy/",
"title": "Tranio",
"imageName": "Tranio.png"
}
] ]
} }

View File

@ -0,0 +1,285 @@
import * as fs from 'fs-extra';
import { insertContentIntoFile, rootDir } from '../tool-utils';
import { getPlans } from './utils/plans';
import { pressCarouselItems } from './utils/pressCarousel';
import { getMarkdownIt, loadMustachePartials, markdownToPageHtml, renderMustache } from './utils/render';
import { Env, PlanPageParams, Sponsors, StripePublicConfig, TemplateParams } from './utils/types';
const dirname = require('path').dirname;
const glob = require('glob');
const path = require('path');
const env = Env.Prod;
const websiteAssetDir = `${rootDir}/Assets/WebsiteAssets`;
const mainTemplateHtml = fs.readFileSync(`${websiteAssetDir}/templates/main-new.mustache`, 'utf8');
const frontTemplateHtml = fs.readFileSync(`${websiteAssetDir}/templates/front.mustache`, 'utf8');
const plansTemplateHtml = fs.readFileSync(`${websiteAssetDir}/templates/plans.mustache`, 'utf8');
const stripeConfigs: Record<Env, StripePublicConfig> = JSON.parse(fs.readFileSync(`${rootDir}/packages/server/stripeConfig.json`, 'utf8'));
const partialDir = `${websiteAssetDir}/templates/partials`;
const stripeConfig = stripeConfigs[env];
let tocMd_: string = null;
let tocHtml_: string = null;
const tocRegex_ = /<!-- TOC -->([^]*)<!-- TOC -->/;
function tocMd() {
if (tocMd_) return tocMd_;
const md = fs.readFileSync(`${rootDir}/README.md`, 'utf8');
const toc = md.match(tocRegex_);
tocMd_ = toc[1];
return tocMd_;
}
const donateLinksRegex_ = /<!-- DONATELINKS -->([^]*)<!-- DONATELINKS -->/;
async function getDonateLinks() {
const md = await fs.readFile(`${rootDir}/README.md`, 'utf8');
const matches = md.match(donateLinksRegex_);
if (!matches) throw new Error('Cannot fetch donate links');
return `<div class="donate-links">\n\n${matches[1].trim()}\n\n</div>`;
}
function replaceGitHubByWebsiteLinks(md: string) {
// let output = md.replace(/https:\/\/github.com\/laurent22\/joplin\/blob\/master\/readme\/(.*?)\/index\.md(#[^\s)]+|)/g, 'https://joplinapp.org/$1');
return md
.replace(/https:\/\/github.com\/laurent22\/joplin\/blob\/dev\/readme\/(.*?)\.md(#[^\s)]+|)/g, '/$1/$2')
.replace(/https:\/\/github.com\/laurent22\/joplin\/blob\/dev\/README\.md(#[^\s)]+|)/g, '/help/$1');
}
function tocHtml() {
if (tocHtml_) return tocHtml_;
const markdownIt = getMarkdownIt();
let md = tocMd();
md = md.replace(/# Table of contents/, '');
md = replaceGitHubByWebsiteLinks(md);
tocHtml_ = markdownIt.render(md);
tocHtml_ = `<div>${tocHtml_}</div>`;
return tocHtml_;
}
function defaultTemplateParams(): TemplateParams {
const baseUrl = '';
return {
env,
baseUrl: baseUrl,
imageBaseUrl: `${baseUrl}/images`,
cssBaseUrl: `${baseUrl}/css`,
jsBaseUrl: `${baseUrl}/js`,
tocHtml: tocHtml(),
yyyy: (new Date()).getFullYear().toString(),
templateHtml: mainTemplateHtml,
forumUrl: 'https://discourse.joplinapp.org/',
showToc: true,
showImproveThisDoc: true,
showJoplinCloudLinks: false,
navbar: {
isFrontPage: false,
},
};
}
function renderPageToHtml(md: string, targetPath: string, templateParams: TemplateParams) {
// Remove the header because it's going to be added back as HTML
md = md.replace(/# Joplin\n/, '');
templateParams = {
...defaultTemplateParams(),
...templateParams,
};
const title = [];
if (!templateParams.title) {
title.push('Joplin - an open source note taking and to-do application with synchronisation capabilities');
} else {
title.push(templateParams.title);
title.push('Joplin');
}
md = replaceGitHubByWebsiteLinks(md);
if (templateParams.donateLinksMd) {
md = `${templateParams.donateLinksMd}\n\n${md}`;
}
templateParams.pageTitle = title.join(' | ');
const html = templateParams.contentHtml ? renderMustache(templateParams.contentHtml, templateParams) : markdownToPageHtml(md, templateParams);
const folderPath = dirname(targetPath);
fs.mkdirpSync(folderPath);
fs.writeFileSync(targetPath, html);
}
async function readmeFileTitle(sourcePath: string) {
const md = await fs.readFile(sourcePath, 'utf8');
const r = md.match(/(^|\n)# (.*)/);
if (!r) {
throw new Error(`Could not determine title for Markdown file: ${sourcePath}`);
} else {
return r[2];
}
}
function renderFileToHtml(sourcePath: string, targetPath: string, templateParams: TemplateParams) {
const md = fs.readFileSync(sourcePath, 'utf8');
return renderPageToHtml(md, targetPath, templateParams);
}
function makeHomePageMd() {
let md = fs.readFileSync(`${rootDir}/README.md`, 'utf8');
md = md.replace(tocRegex_, '');
// HACK: GitHub needs the \| or the inline code won't be displayed correctly inside the table,
// while MarkdownIt doesn't and will in fact display the \. So we remove it here.
md = md.replace(/\\\| bash/g, '| bash');
return md;
}
async function createDownloadButtonsHtml(readmeMd: string): Promise<Record<string, string>> {
const output: Record<string, string> = {};
output['windows'] = readmeMd.match(/(<a href=.*?Joplin-Setup-.*?<\/a>)/)[0];
output['macOs'] = readmeMd.match(/(<a href=.*?Joplin-.*\.dmg.*?<\/a>)/)[0];
output['linux'] = readmeMd.match(/(<a href=.*?Joplin-.*\.AppImage.*?<\/a>)/)[0];
output['android'] = readmeMd.match(/(<a href='https:\/\/play.google.com\/store\/apps\/details\?id=net\.cozic\.joplin.*?<\/a>)/)[0];
output['ios'] = readmeMd.match(/(<a href='https:\/\/itunes\.apple\.com\/us\/app\/joplin\/id1315599797.*?<\/a>)/)[0];
for (const [k, v] of Object.entries(output)) {
if (!v) throw new Error(`Could not get download element for: ${k}`);
}
return output;
// <a href='https://github.com/laurent22/joplin/releases/download/v2.1.8/Joplin-Setup-2.1.8.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a>
// <a href='https://github.com/laurent22/joplin/releases/download/v2.1.8/Joplin-2.1.8.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a>
// <a href='https://github.com/laurent22/joplin/releases/download/v2.1.8/Joplin-2.1.8.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a>
// <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a>
// <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a>
}
async function updateDownloadPage(downloadButtonsHtml: Record<string, string>) {
const desktopButtonsHtml = [
downloadButtonsHtml['windows'],
downloadButtonsHtml['macOs'],
downloadButtonsHtml['linux'],
];
const mobileButtonsHtml = [
downloadButtonsHtml['android'],
downloadButtonsHtml['ios'],
];
await insertContentIntoFile(`${rootDir}/readme/download.md`, '<!-- DESKTOP-DOWNLOAD-LINKS -->', '<!-- DESKTOP-DOWNLOAD-LINKS -->', desktopButtonsHtml.join(' '));
await insertContentIntoFile(`${rootDir}/readme/download.md`, '<!-- MOBILE-DOWNLOAD-LINKS -->', '<!-- MOBILE-DOWNLOAD-LINKS -->', mobileButtonsHtml.join(' '));
}
async function loadSponsors(): Promise<Sponsors> {
const sponsorsPath = `${rootDir}/packages/tools/sponsors.json`;
return JSON.parse(await fs.readFile(sponsorsPath, 'utf8'));
}
async function main() {
await fs.remove(`${rootDir}/docs`);
await fs.copy(websiteAssetDir, `${rootDir}/docs`);
const sponsors = await loadSponsors();
const partials = await loadMustachePartials(partialDir);
const readmeMd = makeHomePageMd();
const downloadButtonsHtml = await createDownloadButtonsHtml(readmeMd);
await updateDownloadPage(downloadButtonsHtml);
// =============================================================
// HELP PAGE
// =============================================================
renderPageToHtml(readmeMd, `${rootDir}/docs/help/index.html`, { sourceMarkdownFile: 'README.md', partials, sponsors });
// =============================================================
// FRONT PAGE
// =============================================================
renderPageToHtml('', `${rootDir}/docs/index.html`, {
templateHtml: frontTemplateHtml,
partials,
pressCarouselRegular: {
id: 'carouselRegular',
items: pressCarouselItems(),
},
pressCarouselMobile: {
id: 'carouselMobile',
items: pressCarouselItems(),
},
sponsors,
navbar: {
isFrontPage: true,
},
});
// =============================================================
// PLANS PAGE
// =============================================================
const planPageFaqMd = await fs.readFile(`${rootDir}/readme/faq_joplin_cloud.md`, 'utf8');
const planPageFaqHtml = getMarkdownIt().render(planPageFaqMd, {});
const planPageParams: PlanPageParams = {
...defaultTemplateParams(),
partials,
templateHtml: plansTemplateHtml,
plans: getPlans(stripeConfig),
faqHtml: planPageFaqHtml,
stripeConfig,
};
const planPageContentHtml = renderMustache('', planPageParams);
renderPageToHtml('', `${rootDir}/docs/plans/index.html`, {
...defaultTemplateParams(),
pageName: 'plans',
partials,
showToc: false,
showImproveThisDoc: false,
contentHtml: planPageContentHtml,
title: 'Joplin Cloud Plans',
});
// =============================================================
// All other pages are generated dynamically from the
// Markdown files under /readme
// =============================================================
const mdFiles = glob.sync(`${rootDir}/readme/**/*.md`).map((f: string) => f.substr(rootDir.length + 1));
const sources = [];
const donateLinksMd = await getDonateLinks();
for (const mdFile of mdFiles) {
const title = await readmeFileTitle(`${rootDir}/${mdFile}`);
const targetFilePath = `${mdFile.replace(/\.md/, '').replace(/readme\//, 'docs/')}/index.html`;
sources.push([mdFile, targetFilePath, {
title: title,
donateLinksMd: mdFile === 'readme/donate.md' ? '' : donateLinksMd,
showToc: mdFile !== 'readme/download.md',
}]);
}
for (const source of sources) {
source[2].sourceMarkdownFile = source[0];
source[2].sourceMarkdownName = path.basename(source[0], path.extname(source[0]));
renderFileToHtml(`${rootDir}/${source[0]}`, `${rootDir}/${source[1]}`, {
...source[2],
templateHtml: mainTemplateHtml,
partials,
});
}
}
main().catch((error) => {
console.error(error);
});

View File

@ -0,0 +1,96 @@
/* eslint-disable import/prefer-default-export */
import { Plan, StripePublicConfig } from './types';
const businessAccountEmailBody = `Hello,
This is an automatically generated email. The Business feature is coming soon, and in the meantime we offer a business discount if you would like to register multiple users.
If so please let us know the following details and we will get back to you as soon as possible:
- Name:
- Email:
- Number of users: `;
export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan> {
const features = {
publishNote: 'Publish a note to the internet',
sync: 'Sync as many devices as you want',
clipper: 'Web Clipper',
collaborate: 'Share and collaborate on a notebook',
multiUsers: 'Up to 10 users',
prioritySupport: 'Priority support',
};
return {
basic: {
name: 'basic',
title: 'Basic',
price: '1.99€',
stripePriceId: stripeConfig.basicPriceId,
featured: false,
iconName: 'basic-icon',
featuresOn: [
'Max 10 MB per note or attachment',
features.publishNote,
features.sync,
features.clipper,
'1 GB storage space',
],
featuresOff: [
features.collaborate,
features.multiUsers,
features.prioritySupport,
],
cfaLabel: 'Try it now',
cfaUrl: '',
},
pro: {
name: 'pro',
title: 'Pro',
price: '5.99€',
stripePriceId: stripeConfig.proPriceId,
featured: true,
iconName: 'pro-icon',
featuresOn: [
'Max 200 MB per note or attachment',
features.publishNote,
features.sync,
features.clipper,
'10 GB storage space',
features.collaborate,
],
featuresOff: [
features.multiUsers,
features.prioritySupport,
],
cfaLabel: 'Try it now',
cfaUrl: '',
},
business: {
name: 'business',
title: 'Business',
price: '49.99€',
stripePriceId: '',
featured: false,
iconName: 'business-icon',
featuresOn: [
'Max 200 MB per note or attachment',
features.publishNote,
features.sync,
features.clipper,
'10 GB storage space',
features.collaborate,
features.multiUsers,
features.prioritySupport,
],
featuresOff: [],
cfaLabel: 'Contact us',
cfaUrl: `mailto:business@joplincloud.com?subject=${encodeURIComponent('Joplin Cloud Business Account Order')}&body=${encodeURIComponent(businessAccountEmailBody)}`,
},
};
}

View File

@ -0,0 +1,30 @@
/* eslint-disable import/prefer-default-export */
export function pressCarouselItems() {
return [
{
active: 'active',
body: 'It lets you create multiple types of notes, reminders, and alarms, all of which can be synced. The app also includes a web clipper too, but in our opinion, Joplin’s best feature is the built-in end-to-end encryption for keeping your notes private.',
author: 'Brendan Hesse',
source: 'Life Hacker, "The Best Note-Taking Apps"',
imageName: 'in-the-press-life-hacker.png',
url: 'https://lifehacker.com/the-best-note-taking-apps-1837842880',
},
{
active: '',
body: 'Joplin is single handedly the best pick for an open-source note-taking app, making it an Editors\' Choice winner for that category. Unlike some open-source tools, which are incredibly difficult to use, Joplin is surprisingly user friendly, even in setting up storage and syncing.',
author: 'Jill Duffy',
source: 'PCMag, "The Best Open-Source Note-Taking App"',
imageName: 'in-the-press-life-pcmag.png',
url: 'https://www.pcmag.com/reviews/joplin',
},
{
active: '',
body: 'Joplin is an excellent open source note taking application with plenty of features. You can take notes, make to-do list and sync your notes across devices by linking it with cloud services. The synchronization is protected with end to end encryption.',
author: 'Abhishek Prakash',
source: 'It\'s FOSS, "Joplin: Open source note organizer"',
imageName: 'in-the-press-its-foss.png',
url: 'https://itsfoss.com/joplin/',
},
];
}

View File

@ -0,0 +1,139 @@
import * as Mustache from 'mustache';
import { filename } from '@joplin/lib/path-utils';
import * as fs from 'fs-extra';
import { TemplateParams } from './types';
const MarkdownIt = require('markdown-it');
export async function loadMustachePartials(partialDir: string) {
const output: Record<string, string> = {};
const files = await fs.readdir(partialDir);
for (const f of files) {
const name = filename(f);
const templateContent = await fs.readFile(`${partialDir}/${f}`, 'utf8');
output[name] = templateContent;
}
return output;
}
export function renderMustache(contentHtml: string, templateParams: TemplateParams) {
return Mustache.render(templateParams.templateHtml, {
...templateParams,
contentHtml,
}, templateParams.partials);
}
export function getMarkdownIt() {
return new MarkdownIt({
breaks: true,
linkify: true,
html: true,
});
}
export function markdownToPageHtml(md: string, templateParams: TemplateParams): string {
const markdownIt = getMarkdownIt();
markdownIt.core.ruler.push('tableClass', (state: any) => {
const tokens = state.tokens;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'table_open') {
token.attrs = [
['class', 'table'],
];
}
}
});
markdownIt.core.ruler.push('checkbox', (state: any) => {
const tokens = state.tokens;
const Token = state.Token;
const doneNames = [];
const headingTextToAnchorName = (text: string, doneNames: string[]) => {
const allowed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let lastWasDash = true;
let output = '';
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (allowed.indexOf(c) < 0) {
if (lastWasDash) continue;
lastWasDash = true;
output += '-';
} else {
lastWasDash = false;
output += c;
}
}
output = output.toLowerCase();
while (output.length && output[output.length - 1] === '-') {
output = output.substr(0, output.length - 1);
}
let temp = output;
let index = 1;
while (doneNames.indexOf(temp) >= 0) {
temp = `${output}-${index}`;
index++;
}
output = temp;
return output;
};
const createAnchorTokens = (anchorName: string) => {
const output = [];
{
const token = new Token('heading_anchor_open', 'a', 1);
token.attrs = [
['name', anchorName],
['href', `#${anchorName}`],
['class', 'heading-anchor'],
];
output.push(token);
}
{
const token = new Token('text', '', 0);
token.content = '🔗';
output.push(token);
}
{
const token = new Token('heading_anchor_close', 'a', -1);
output.push(token);
}
return output;
};
let insideHeading = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'heading_open') {
insideHeading = true;
continue;
}
if (token.type === 'heading_close') {
insideHeading = false;
continue;
}
if (insideHeading && token.type === 'inline') {
const anchorName = headingTextToAnchorName(token.content, doneNames);
doneNames.push(anchorName);
const anchorTokens = createAnchorTokens(anchorName);
// token.children = anchorTokens.concat(token.children);
token.children = token.children.concat(anchorTokens);
}
}
});
return renderMustache(markdownIt.render(md), templateParams);
}

View File

@ -0,0 +1,90 @@
export enum Env {
Dev = 'dev',
Prod = 'prod',
}
interface GithubSponsor {
name: string;
id: string;
}
interface OrgSponsor {
url: string;
title: string;
imageName: string;
}
export interface Sponsors {
github: GithubSponsor[];
orgs: OrgSponsor[];
}
interface PressCarouselItem {
active: string;
body: string;
author: string;
source: string;
imageName: string;
url: string;
}
interface PressCarousel {
id: string;
items: PressCarouselItem[];
}
interface NavBar {
isFrontPage: boolean;
}
export interface TemplateParams {
env?: Env;
baseUrl?: string;
pageName?: string;
imageBaseUrl?: string;
cssBaseUrl?: string;
jsBaseUrl?: string;
tocHtml?: string;
sourceMarkdownFile?: string;
title?: string;
donateLinksMd?: string;
pageTitle?: string;
yyyy? : string;
templateHtml?: string;
partials?: Record<string, string>;
forumUrl?: string;
showToc?: boolean;
pressCarouselRegular?: PressCarousel;
pressCarouselMobile?: PressCarousel;
sponsors?: Sponsors;
showImproveThisDoc?: boolean;
contentHtml?: string;
navbar?: NavBar;
showJoplinCloudLinks?: boolean;
}
export interface Plan {
name: string;
title: string;
price: string;
stripePriceId: string;
featured: boolean;
iconName: string;
featuresOn: string[];
featuresOff: string[];
cfaLabel: string;
cfaUrl: string;
}
export interface PlanPageParams extends TemplateParams {
plans: Record<string, Plan>;
faqHtml: string;
stripeConfig: StripePublicConfig;
}
export interface StripePublicConfig {
publishableKey: string;
basicPriceId: string;
proPriceId: string;
webhookBaseUrl: string;
}

View File

@ -116,7 +116,7 @@ Call **GET /ping** to check if the service is available. It should return "Jopli
# Searching # Searching
Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/#searching Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/help/#searching
To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-id). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title. To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-id). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title.

View File

@ -4,7 +4,7 @@ The original search engine in Joplin was pretty limited - it would search for yo
The last versions of Joplin include a new search engine that provides much better results, and also allow better specifying search queries. The last versions of Joplin include a new search engine that provides much better results, and also allow better specifying search queries.
The search engine indexes in real time the content of the notes, thus it can give back results very fast. It is also built on top of SQLite FTS and thus support [all its queries](https://joplinapp.org/#searching). Unlike the previous search engine, the new one also sorts the results by relevance. The search engine indexes in real time the content of the notes, thus it can give back results very fast. It is also built on top of SQLite FTS and thus support [all its queries](https://joplinapp.org/help/#searching). Unlike the previous search engine, the new one also sorts the results by relevance.
The first iteration of this new search engine was a bit limited when it comes to non-English text. For example, for searching text that contains accents or non-alphabetical characters. So in the last update, better support for this was also added - accentuated and non-accentuated characters are treated in the same way, and languages like Russian, Chinese, Japanese or Korean can be searched easily. The first iteration of this new search engine was a bit limited when it comes to non-English text. For example, for searching text that contains accents or non-alphabetical characters. So in the last update, better support for this was also added - accentuated and non-accentuated characters are treated in the same way, and languages like Russian, Chinese, Japanese or Korean can be searched easily.

View File

@ -1,6 +1,6 @@
# Customising your notes with the help of the development tools and CSS # Customising your notes with the help of the development tools and CSS
In Joplin desktop, it has been possible [to customise the appearance of your notes](https://joplinapp.org/#custom-css) using CSS for quite some time. In Joplin desktop, it has been possible [to customise the appearance of your notes](https://joplinapp.org/help/#custom-css) using CSS for quite some time.
An issue however is that it is difficult to know what CSS to write and how to select specific elements with CSS. The development tools that were just added allow figuring this out. They are available under the menu **Help > Toggle development tools.** An issue however is that it is difficult to know what CSS to write and how to select specific elements with CSS. The development tools that were just added allow figuring this out. They are available under the menu **Help > Toggle development tools.**

View File

@ -1,6 +1,8 @@
# Support Joplin development # Support Joplin development
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standards. Joplin is an open source project, and your donations support the developer. Developing quality applications mostly takes time, but there are also expenses, such as digital certificates to sign the applications, app store fees, hosting, as well as the hardware to develop and test a cross-plaform app.
Most of all, your donation will make it possible to keep up the current development standards, and will bring new features and improvements to the app.
## Donations ## Donations
@ -17,4 +19,4 @@ Finally, there are other ways to support the development of Joplin:
- Consider rating the app on [Google Play](https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) or [App Store](https://itunes.apple.com/us/app/joplin/id1315599797). - Consider rating the app on [Google Play](https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) or [App Store](https://itunes.apple.com/us/app/joplin/id1315599797).
- Vote for or review the app on [alternativeTo](https://alternativeto.net/software/joplin/about/) or [Product Hunt](https://www.producthunt.com/posts/joplin). - Vote for or review the app on [alternativeTo](https://alternativeto.net/software/joplin/about/) or [Product Hunt](https://www.producthunt.com/posts/joplin).
- [Create or update a translation](https://joplinapp.org/#localisation). - [Create or update a translation](https://joplinapp.org/help/#localisation).

25
readme/download.md Normal file
View File

@ -0,0 +1,25 @@
# Download Joplin
Thank you for downloading the Joplin desktop app!
If you download didn't start, <a href="#" class="download-click-here">click here</a>.
<div class="get-it-desktop">
## Get it on desktop
Access your notes on Windows, macOS or Linux.
<!-- DESKTOP-DOWNLOAD-LINKS --><a href='https://github.com/laurent22/joplin/releases/download/v2.1.8/Joplin-Setup-2.1.8.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> <a href='https://github.com/laurent22/joplin/releases/download/v2.1.8/Joplin-2.1.8.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> <a href='https://github.com/laurent22/joplin/releases/download/v2.1.8/Joplin-2.1.8.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a><!-- DESKTOP-DOWNLOAD-LINKS -->
</div>
## Get it on mobile
To access your notes on your mobile or tablet, get the Android or iOS apps!
<!-- MOBILE-DOWNLOAD-LINKS --><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a><!-- MOBILE-DOWNLOAD-LINKS -->
## More download options
For other download options, such as the portable application, terminal application or Android APK file, please [follow this link](https://github.com/laurent22/joplin/blob/dev/README.md#installation).

View File

@ -142,4 +142,4 @@ Additionally the Windows Task Manager can be used to verify whether Joplin is st
## Why is it named Joplin? ## Why is it named Joplin?
The name comes from the composer and pianist [Scott Joplin](https://en.wikipedia.org/wiki/Scott_Joplin), which I often listen to. His name is also easy to remember and type so it felt like a good choice. And, to quote a user on Hacker News, "though Scott Joplin's ragtime musical style has a lot in common with some very informal music, his own approach was more educated, sophisticated, and precise. Every note was in its place for a reason, and he was known to prefer his pieces to be performed exactly as written. So you could say that compared to the people who came before him, his notes were more organized". The name comes from the composer and pianist [Scott Joplin](https://en.wikipedia.org/wiki/Scott_Joplin), which I often listen to. His name is also easy to remember and type so it felt like a good choice.

View File

@ -0,0 +1,17 @@
# FAQ
## What is Joplin Cloud?
Joplin supports various ways to synchronise data with cloud file hosting services. It does that in a generic way, which allows it to support many providers. However there are limits to what can be done in such a generic way - for example, certain performance optimisations are not possible, and more advanced features such as the ability to publish notes or share notebooks cannot be implemented.
This is where Joplin Cloud comes into place. It is a solution developed specifically for Joplin and as such offers various improvements, such as improved synchronisation performances, support for publishing notes, and sharing and collaborating on notebooks.
Moreover, by getting a subscription you are supporting the development of the project as a whole, including the open source applications. Such support helps in the long term to provide bug and security fixes, add new features, and provide support.
## What if I exceed the storage space?
If you exceed the storage space, you will not be able to upload new notes. You may however delete notes and attachments so as to free up space. If you are on a Basic plan, you may also upgrade to Pro. If you are on a Pro or Business plan please contact us and let us know that you need more space and we will increase your storage space.
## Do you offer discounts?
We can offer discounts for students, schools and universities, or nonprofit organisations. To get this discount please contact us.

View File

@ -84,7 +84,7 @@ More info: [Mobile - Add share menu #876](https://github.com/laurent22/joplin/is
## 5. Web client for Nextcloud ## 5. Web client for Nextcloud
There is the community's wish to have the notes integrated Nextcloud, so that Notes can be sought by Nextcloud itself. Although this idea focuses on Nextcloud it shall allow to extend it to other collaboration applications going beyond the current scope of [Synchronisation](https://joplinapp.org/#synchronisation). There is already the [web application](https://github.com/foxmask/joplin-web) what may used as a starting point, but it is also fine to start from scratch. There is the community's wish to have the notes integrated Nextcloud, so that Notes can be sought by Nextcloud itself. Although this idea focuses on Nextcloud it shall allow to extend it to other collaboration applications going beyond the current scope of [Synchronisation](https://joplinapp.org/help/#synchronisation). There is already the [web application](https://github.com/foxmask/joplin-web) what may used as a starting point, but it is also fine to start from scratch.
Feature parity with the desktop client is not needed and would be out of scope. These are the features that would be needed to create a minimal web client: Feature parity with the desktop client is not needed and would be out of scope. These are the features that would be needed to create a minimal web client:

View File

@ -15,7 +15,7 @@ All participants will need a Google account in order to join the program. So, sa
All applications share the same back-end written in JavaScript (Node.js), with Redux for state management. The back-end runs locally. All applications share the same back-end written in JavaScript (Node.js), with Redux for state management. The back-end runs locally.
The GUI's, as listed on the [Joplin's website](https://joplinapp.org/#installation) are: The GUI's, as listed on the [Joplin's website](https://joplinapp.org/help/#installation) are:
- CLI: terminal-kit - CLI: terminal-kit
- Desktop: Electron - Desktop: Electron

View File

@ -31,7 +31,7 @@ We suggest you read carefully these important documents and bookmark the links a
In general, all applications share the same back-end written in TypeScript or JavaScript (Node.js), with Redux for state management. The back-end runs locally. In general, all applications share the same back-end written in TypeScript or JavaScript (Node.js), with Redux for state management. The back-end runs locally.
The desktop GUI, as listed on the [Joplin's website](https://joplinapp.org/#installation) is done using Electron and React. The desktop GUI, as listed on the [Joplin's website](https://joplinapp.org/help/#installation) is done using Electron and React.
The mobile app is done using React Native. The mobile app is done using React Native.

View File

@ -2,7 +2,7 @@
Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified with your own text editor. Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified with your own text editor.
Notes exported from Evernote via .enex files [can be imported](https://joplinapp.org/#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported. Notes exported from Evernote via .enex files [can be imported](https://joplinapp.org/help/#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
The notes can be [synchronised](#synchronisation) with various targets including the file system (for example with a network directory), Nextcloud, Dropbox, OneDrive or WebDAV. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around. The notes can be [synchronised](#synchronisation) with various targets including the file system (for example with a network directory), Nextcloud, Dropbox, OneDrive or WebDAV. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
@ -347,7 +347,7 @@ The following commands are available in [command-line mode](#command-line-mode):
locale Language. locale Language.
Please see localisation section on Please see localisation section on
https://joplinapp.org/#localisation https://joplinapp.org/help/#localisation
for info on translation completion progress for info on translation completion progress
Type: Enum. Type: Enum.
Possible values: ar (Arabic), eu (Basque), Possible values: ar (Arabic), eu (Basque),

View File

@ -1,6 +1,6 @@
# Welcome to Joplin! 🗒️ # Welcome to Joplin! 🗒️
Joplin is a free, open source note taking and to-do application, which helps you write and organise your notes, and synchronise them between your devices. The notes are searchable, can be copied, tagged and modified either from the application directly or from your own text editor. The notes are in [Markdown format](https://joplinapp.org/#markdown). Joplin is available as a **💻 desktop**, **📱 mobile** and **🔡 terminal** application. Joplin is a free, open source note taking and to-do application, which helps you write and organise your notes, and synchronise them between your devices. The notes are searchable, can be copied, tagged and modified either from the application directly or from your own text editor. The notes are in [Markdown format](https://joplinapp.org/help/#markdown). Joplin is available as a **💻 desktop**, **📱 mobile** and **🔡 terminal** application.
The notes in this notebook give an overview of what Joplin can do and how to use it. In general, the three applications share roughly the same functionalities; any differences will be clearly indicated. The notes in this notebook give an overview of what Joplin can do and how to use it. In general, the three applications share roughly the same functionalities; any differences will be clearly indicated.
@ -12,7 +12,7 @@ Joplin has three main columns:
- **Sidebar** contains the list of your notebooks and tags, as well as the synchronisation status. - **Sidebar** contains the list of your notebooks and tags, as well as the synchronisation status.
- **Note List** contains the current list of notes - either the notes in the currently selected notebook, the notes in the currently selected tag, or search results. - **Note List** contains the current list of notes - either the notes in the currently selected notebook, the notes in the currently selected tag, or search results.
- **Note Editor** is the place where you write your notes. There is a **WYSIWYG editor** and a **Markdown editor** - click on **Code View** to switch between both! You may also use an [external editor](https://joplinapp.org/#external-text-editor) to edit notes. For example you can use something like Typora as an external editor and it will display the note as well as any embedded images. - **Note Editor** is the place where you write your notes. There is a **WYSIWYG editor** and a **Markdown editor** - click on **Code View** to switch between both! You may also use an [external editor](https://joplinapp.org/help/#external-text-editor) to edit notes. For example you can use something like Typora as an external editor and it will display the note as well as any embedded images.
## Writing notes in Markdown ## Writing notes in Markdown
@ -42,7 +42,7 @@ This is a [link](https://joplinapp.org) and, finally, below is a horizontal rule
* * * * * *
A lot more is possible including adding code samples, math formulae or checkbox lists - see the [Markdown documentation](https://joplinapp.org/#markdown) for more information. A lot more is possible including adding code samples, math formulae or checkbox lists - see the [Markdown documentation](https://joplinapp.org/help/#markdown) for more information.
## Organising your notes ## Organising your notes

View File

@ -4,7 +4,7 @@
Joplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, images, attached files and note metadata (such as author, geo-location, etc.) via ENEX files. Joplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, images, attached files and note metadata (such as author, geo-location, etc.) via ENEX files.
To import Evernote data, first export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks). Then, on **desktop**, do the following: Open File > Import > ENEX and select your file. The notes will be imported into a new separate notebook. If needed they can then be moved to a different notebook, or the notebook can be renamed, etc. Read [more about Evernote import](https://joplinapp.org/#importing-from-evernote). To import Evernote data, first export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks). Then, on **desktop**, do the following: Open File > Import > ENEX and select your file. The notes will be imported into a new separate notebook. If needed they can then be moved to a different notebook, or the notebook can be renamed, etc. Read [more about Evernote import](https://joplinapp.org/help/#importing-from-evernote).
# Importing from other apps # Importing from other apps

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 438 KiB