New features:

* Discount 1.6.1
      * Lines beginning with spaces are considered code. I don't know why markdown.py didn't trigger this.
      * tables in mark down: why not?
      * validation of resulting HTML vial libxml to prevent hax.
    * private RSS and JSON feeds
    * optional whitelists for subreddits
    * Moderator messaging

    Additions:
    * destination sanitization to cut down on XSRF
    * cosmetic fix to spam and reported listing
    * make the rss feeds on messages useful
    * /admin/errors
    * Of the types of listings hitting the precomputers (top/controversy by hour/day/week/month/year), the ones over long periods of time don't change often. So we can try to run them at most once per day, and then merge in the day's listings.
    * google analytics
    * logging queue
    * Created empty commentspanel.xml, errorpage.xml, login.xml
    * add subreddit rules/info box to submit page
    * add 'via' link on messages in moderator inbox
    * add a show=all get parameter to link listings to optionally ignore hiding preferences.
    * Raise edited timelimit to three mins
    * Remove UI that makes it look like you can edit deleted selftexts
    * Make it clearer to admins when a link is deleted
    * Fix [S] leak on deleted comments
    * Fix /user/[deleted] misrendering
    * New house ads system
    * updated so that minimalcontrollers actually can use the page cache.
    * Added /admin/usage

    Bugfixes:
    * Reduce the number of results that we request from Solr and simplify that caching a bit
    * Require a secret key to shut down app-servers
    * Make get_title a little more resilient to malformed documents and slow remote servers
    * Cause the SearchBuilder to only byID the results that it's going to render instead of all 1000
    * Remove ability for an author to XSS himself
    * fix spam listings and an xsrf
    * More verbose VDestination
    * Fixing the famous ?limit=0.1 error, and one last password-validation one
    * distinguish deleted comments' and deleted links' error messages
    * Don't allow ridiculously long log lines to widen the page
    * Bug with HardCache.add() when existing key is expired
    * Add adminbox next to domain
This commit is contained in:
Mike
2010-05-03 17:15:44 -07:00
committed by KeyserSosa
parent 2869eaf8b9
commit a402d48de3
204 changed files with 18566 additions and 1420 deletions

1
.gitignore vendored
View File

@@ -30,3 +30,4 @@ r2/srcount.pickle
r2/myproduction.ini
.DS_Store
r2/r2.egg-info/**
r2/r2/public/static/sprite.png

View File

@@ -9,7 +9,7 @@ template_debug = true
uncompressedJS = true
translator = true
sqlprinting = false
exception_logging = false
log_start = true
proxy_addr =
@@ -30,6 +30,9 @@ adframetracker_url =
clicktracker_url =
traffic_url =
# Just a list of words. Used by errlog.py to make up names for new errors.
words_file = /usr/dict/words
# for sponsored links:
payment_domain = https://pay.localhost/
authorizenetname =
@@ -93,6 +96,9 @@ db_table_report_account_subreddit = relation, account, subreddit, main
db_table_award = thing, award
db_table_trophy = relation, account, award, award
db_table_ad = thing, main
db_table_adsr = relation, ad, subreddit, main
disallow_db_writes = False
###
@@ -103,6 +109,8 @@ timezone = UTC
lang = en
monitored_servers = localhost
enable_usage_stats = false
#query cache settings
num_query_queue_workers = 0
query_queue_worker =
@@ -174,6 +182,10 @@ sr_dropdown_threshold = 15
smtp_server = localhost
new_link_share_delay = 5 minutes
# email address of the person / people running your site
nerds_email = root@localhost
share_reply = noreply@yourdomain.com
#user-agents to limit

View File

@@ -33,7 +33,7 @@ from pylons.wsgiapp import PylonsApp, PylonsBaseWSGIApp
from r2.config.environment import load_environment
from r2.config.rewrites import rewrites
from r2.lib.utils import rstrips
from r2.lib.utils import rstrips, is_authorized_cname
from r2.lib.jsontemplates import api_type
#middleware stuff
@@ -245,11 +245,10 @@ class DomainMiddleware(object):
auth_cnames = [x.strip() for x in auth_cnames.split(',')]
# we are going to be matching with endswith, so make sure there
# are no empty strings that have snuck in
self.auth_cnames = [x for x in auth_cnames if x]
self.auth_cnames = filter(None, auth_cnames)
def is_auth_cname(self, domain):
return any((domain == cname or domain.endswith('.' + cname))
for cname in self.auth_cnames)
return is_authorized_cname(domain, self.auth_cnames)
def __call__(self, environ, start_response):
# get base domain as defined in INI file

View File

@@ -31,7 +31,7 @@ def make_map(global_conf={}, app_conf={}):
mc = map.connect
admin_routes.add(mc)
mc('/login', controller='front', action='login')
mc('/logout', controller='front', action='logout')
mc('/verify', controller='front', action='verify')
@@ -41,15 +41,16 @@ def make_map(global_conf={}, app_conf={}):
mc('/validuser', controller='front', action='validuser')
mc('/over18', controller='post', action='over18')
mc('/search', controller='front', action='search')
mc('/sup', controller='front', action='sup')
mc('/traffic', controller='front', action='site_traffic')
mc('/about/message/:where', controller='message', action='listing')
mc('/about/:location', controller='front',
action='editreddit', location = 'about')
mc('/reddits/create', controller='front', action='newreddit')
mc('/reddits/search', controller='front', action='search_reddits')
mc('/reddits/login', controller='front', action='login')
@@ -60,42 +61,51 @@ def make_map(global_conf={}, app_conf={}):
mc('/reddits/mine/:where', controller='myreddits', action='listing',
where='subscriber',
requirements=dict(where='subscriber|contributor|moderator'))
mc('/buttons', controller='buttons', action='button_demo_page')
#the frame
mc('/button_content', controller='buttons', action='button_content')
#/button.js and buttonlite.js - the embeds
mc('/button', controller='buttons', action='button_embed')
mc('/button', controller='buttonjs', action='button_embed')
mc('/buttonlite', controller='buttons', action='button_lite')
mc('/widget', controller='buttons', action='widget_demo_page')
mc('/bookmarklets', controller='buttons', action='bookmarklets')
mc('/awards', controller='front', action='awards')
mc('/i18n', controller='feedback', action='i18n')
mc('/feedback', controller='feedback', action='feedback')
mc('/ad_inq', controller='feedback', action='ad_inq')
mc('/admin/i18n', controller='i18n', action='list')
mc('/admin/i18n/:action', controller='i18n')
mc('/admin/i18n/:action/:lang', controller='i18n')
mc('/admin/usage', controller='usage')
# Used for editing ads
mc('/admin/ads', controller='ads')
mc('/admin/ads/:adcn/:action', controller='ads',
requirements=dict(action="assign|srs"))
mc('/admin/awards', controller='awards')
mc('/admin/awards/:awardcn/:action', controller='awards',
requirements=dict(action="give|winners"))
mc('/admin/errors', controller='errorlog')
mc('/admin/:action', controller='admin')
mc('/user/:username/about', controller='user', action='about',
where='overview')
mc('/user/:username/:where', controller='user', action='listing',
where='overview')
mc('/prefs/:location', controller='front',
action='prefs', location='options')
mc('/info/0:article/*rest', controller = 'front',
action='oldinfo', dest='comments', type='ancient')
mc('/info/:article/:dest/:comment', controller='front',
@@ -113,7 +123,7 @@ def make_map(global_conf={}, app_conf={}):
action = 'comments', title=None, comment = None)
mc('/duplicates/:article/:title', controller = 'front',
action = 'duplicates', title=None)
mc('/mail/optout', controller='front', action = 'optout')
mc('/mail/optin', controller='front', action = 'optin')
mc('/stylesheet', controller = 'front', action = 'stylesheet')
@@ -138,8 +148,8 @@ def make_map(global_conf={}, app_conf={}):
mc('/shutdown', controller='health', action='shutdown')
mc('/', controller='hot', action='listing')
listing_controllers = "hot|saved|toplinks|new|recommended|randomrising|comments"
listing_controllers = "hot|saved|new|recommended|randomrising|comments"
mc('/:controller', action='listing',
requirements=dict(controller=listing_controllers))
@@ -148,18 +158,20 @@ def make_map(global_conf={}, app_conf={}):
mc('/:sort', controller='browse', sort='top', action = 'listing',
requirements = dict(sort = 'top|controversial'))
mc('/message/compose', controller='message', action='compose')
mc('/message/messages/:mid', controller='message', action='listing',
where = "messages")
mc('/message/:where', controller='message', action='listing')
mc('/message/moderator/:subwhere', controller='message', action='listing',
where = 'moderator')
mc('/:action', controller='front',
requirements=dict(action="password|random|framebuster"))
mc('/:action', controller='embed',
requirements=dict(action="help|blog"))
mc('/help/*anything', controller='embed', action='help')
mc('/goto', controller='toolbar', action='goto')
mc('/tb/:id', controller='toolbar', action='tb')
mc('/toolbar/:action', controller='toolbar',
@@ -172,7 +184,7 @@ def make_map(global_conf={}, app_conf={}):
# additional toolbar-related rules just above the catchall
mc('/d/:what', controller='api', action='bookmarklet')
mc('/resetpassword/:key', controller='front',
action='resetpassword')
mc('/verification/:key', controller='front',
@@ -184,7 +196,7 @@ def make_map(global_conf={}, app_conf={}):
requirements=dict(action="login|reg"))
mc('/post/:action', controller='post',
requirements=dict(action="options|over18|unlogged_options|optout|optin|login|reg"))
mc('/api/distinguish/:how', controller='api', action="distinguish")
mc('/api/:action/:url_user', controller='api',
requirements=dict(action="login|register"))
@@ -193,7 +205,7 @@ def make_map(global_conf={}, app_conf={}):
mc('/api/:action', controller='promote',
requirements=dict(action="promote|unpromote|new_promo|link_thumb|freebie|promote_note|update_pay|refund|traffic_viewer|rm_traffic_viewer"))
mc('/api/:action', controller='api')
mc('/captcha/:iden', controller='captcha', action='captchaimg')
mc('/mediaembed/:link', controller="mediaembed", action="mediaembed")
@@ -202,17 +214,23 @@ def make_map(global_conf={}, app_conf={}):
mc('/store', controller='redirect', action='redirect',
dest='http://store.reddit.com/index.html')
mc('/code', controller='redirect', action='redirect',
dest='http://code.reddit.com/')
mc('/mobile', controller='redirect', action='redirect',
dest='http://m.reddit.com/')
mc('/authorize_embed', controller = 'front', action = 'authorize_embed')
mc("/ads/", controller = "front", action = "ad")
mc("/ads/:reddit", controller = "front", action = "ad")
# Used for showing ads
mc("/ads/", controller = "mediaembed", action = "ad")
mc("/ads/r/:reddit_name", controller = "mediaembed", action = "ad")
mc("/ads/:codename", controller = "mediaembed", action = "ad_by_codename")
mc('/comscore-iframe/', controller='mediaembed', action='comscore')
mc('/comscore-iframe/*url', controller='mediaembed', action='comscore')
# This route handles displaying the error page and
# graphics used in the 404/500
# error pages. It should likely stay at the top

View File

@@ -22,7 +22,6 @@
from listingcontroller import ListingController
from listingcontroller import HotController
from listingcontroller import SavedController
from listingcontroller import ToplinksController
from listingcontroller import NewController
from listingcontroller import BrowseController
from listingcontroller import RecommendedController
@@ -39,6 +38,7 @@ from feedback import FeedbackController
from front import FrontController
from health import HealthController
from buttons import ButtonsController
from buttons import ButtonjsController
from captcha import CaptchaController
from embed import EmbedController
from error import ErrorController
@@ -46,6 +46,9 @@ from post import PostController
from toolbar import ToolbarController
from i18n import I18nController
from awards import AwardsController
from ads import AdsController
from usage import UsageController
from errorlog import ErrorlogController
from promotecontroller import PromoteController
from mediaembed import MediaembedController

56
r2/r2/controllers/ads.py Normal file
View File

@@ -0,0 +1,56 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from pylons import request, g
from reddit_base import RedditController
from r2.lib.pages import AdminPage, AdminAds, AdminAdAssign, AdminAdSRs
from validator import *
class AdsController(RedditController):
@validate(VSponsor())
def GET_index(self):
res = AdminPage(content = AdminAds(),
show_sidebar = False,
title = 'ads').render()
return res
@validate(VSponsor(),
ad = VAdByCodename('adcn'))
def GET_assign(self, ad):
if ad is None:
abort(404, 'page not found')
res = AdminPage(content = AdminAdAssign(ad),
show_sidebar = False,
title='assign an ad to a community').render()
return res
@validate(VSponsor(),
ad = VAdByCodename('adcn'))
def GET_srs(self, ad):
if ad is None:
abort(404, 'page not found')
res = AdminPage(content = AdminAdSRs(ad),
show_sidebar = False,
title='ad srs').render()
return res

View File

@@ -50,10 +50,25 @@ from r2.lib.media import force_thumbnail, thumbnail_url
from r2.lib.comment_tree import add_comment, delete_comment
from r2.lib import tracking, cssfilter, emailer
from r2.lib.subreddit_search import search_reddits
from r2.lib.log import log_text
from datetime import datetime, timedelta
from md5 import md5
def reject_vote(thing):
voteword = request.params.get('dir')
if voteword == '1':
voteword = 'upvote'
elif voteword == '0':
voteword = '0-vote'
elif voteword == '-1':
voteword = 'downvote'
log_text ("rejected vote", "Rejected %s from %s (%s) on %s %s via %s" %
(voteword, c.user.name, request.ip, thing.__class__.__name__,
thing._id36, request.referer), "info")
class ApiController(RedditController):
"""
Controller which deals with almost all AJAX site interaction.
@@ -73,6 +88,9 @@ class ApiController(RedditController):
return abort(404, 'not found')
links = link_from_url(request.params.get('url'), filter_spam = False)
if not links:
return abort(404, 'not found')
listing = wrap_links(links, num = count)
return BoringPage(_("API"), content = listing).render()
@@ -107,19 +125,19 @@ class ApiController(RedditController):
VUser(),
VModhash(),
ip = ValidIP(),
to = VExistingUname('to'),
to = VMessageRecipent('to'),
subject = VRequired('subject', errors.NO_SUBJECT),
body = VMessage(['text', 'message']))
body = VMarkdown(['text', 'message']))
def POST_compose(self, form, jquery, to, subject, body, ip):
"""
handles message composition under /message/compose.
"""
if not (form.has_errors("to", errors.USER_DOESNT_EXIST,
errors.NO_USER) or
errors.NO_USER, errors.SUBREDDIT_NOEXIST) or
form.has_errors("subject", errors.NO_SUBJECT) or
form.has_errors("text", errors.NO_TEXT, errors.TOO_LONG) or
form.has_errors("captcha", errors.BAD_CAPTCHA)):
m, inbox_rel = Message._new(c.user, to, subject, body, ip)
form.set_html(".status", _("your message has been delivered"))
form.set_inputs(to = "", subject = "", text = "", captcha="")
@@ -128,20 +146,20 @@ class ApiController(RedditController):
@validatedForm(VUser(),
VCaptcha(),
ValidDomain('url'),
VRatelimit(rate_user = True, rate_ip = True,
prefix = "rate_submit_"),
ip = ValidIP(),
sr = VSubmitSR('sr'),
url = VUrl(['url', 'sr']),
banmsg = VOkayDomain('url'),
title = VTitle('title'),
save = VBoolean('save'),
selftext = VSelfText('text'),
selftext = VMarkdown('text'),
kind = VOneOf('kind', ['link', 'self', 'poll']),
then = VOneOf('then', ('tb', 'comments'),
default='comments'))
def POST_submit(self, form, jquery, url, selftext, kind, title, save,
sr, ip, then):
def POST_submit(self, form, jquery, url, banmsg, selftext, kind, title,
save, sr, ip, then):
#backwards compatability
if url == 'self':
kind = 'self'
@@ -178,6 +196,11 @@ class ApiController(RedditController):
elif form.has_errors("title", errors.NO_TEXT):
pass
# Uncomment if we want to let spammers know we're on to them
# if banmsg:
# form.set_html(".field-url.BAD_URL", banmsg)
# return
elif kind == 'self' and form.has_errors('text', errors.TOO_LONG):
pass
@@ -194,6 +217,9 @@ class ApiController(RedditController):
l = Link._submit(request.post.title, url if kind == 'link' else 'self',
c.user, sr, ip)
if banmsg:
admintools.spam(l, banner = "domain (%s)" % banmsg)
if kind == 'self':
l.url = l.make_permalink_slow()
l.is_self = True
@@ -264,31 +290,18 @@ class ApiController(RedditController):
rem = VBoolean('rem'),
reason = VReason('reason'))
def POST_login(self, form, jquery, user, username, dest, rem, reason):
if reason and reason[0] == 'redirect':
dest = reason[1]
hc_key = "login_attempts-%s" % request.ip
# TODO: You-know-what (not mentioning it, just in case
# we accidentally release code with this comment in it)
# Cache lifetime for login_attmempts
la_expire_time = 3600 * 8
recent_attempts = g.hardcache.add(hc_key, 0, time=la_expire_time)
fake_failure = False
if recent_attempts >= 25:
g.log.error ("%s failed to login as %s (attempt #%d)"
% (request.ip, username, recent_attempts))
fake_failure = True
if fake_failure or form.has_errors("passwd", errors.WRONG_PASSWORD):
if login_throttle(username, wrong_password = form.has_errors("passwd",
errors.WRONG_PASSWORD)):
VRatelimit.ratelimit(rate_ip = True, prefix = 'login_', seconds=1)
g.hardcache.incr(hc_key, time = la_expire_time)
else:
self._login(form, user, dest, rem)
c.errors.add(errors.WRONG_PASSWORD, field = "passwd")
if not form.has_errors("passwd", errors.WRONG_PASSWORD):
self._login(form, user, dest, rem)
@validatedForm(VCaptcha(),
VRatelimit(rate_ip = True, prefix = "rate_register_"),
@@ -310,11 +323,11 @@ class ApiController(RedditController):
user = register(name, password)
VRatelimit.ratelimit(rate_ip = True, prefix = "rate_register_")
#anything else we know (email, languages)?
if email:
user.email = email
user.pref_lang = c.lang
if c.content_langs == 'all':
user.pref_content_langs = 'all'
@@ -322,10 +335,10 @@ class ApiController(RedditController):
langs = list(c.content_langs)
langs.sort()
user.pref_content_langs = tuple(langs)
d = c.user._dirties.copy()
user._commit()
c.user = user
if reason:
if reason[0] == 'redirect':
@@ -333,7 +346,7 @@ class ApiController(RedditController):
elif reason[0] == 'subscribe':
for sr, sub in reason[1].iteritems():
self._subscribe(sr, sub)
self._login(form, user, dest, rem)
@noresponse(VUser(),
@@ -530,7 +543,7 @@ class ApiController(RedditController):
if isinstance(thing, Link):
sr = thing.subreddit_slow
expire_hot(sr)
queries.new_link(thing)
queries.delete_links(thing)
#comments have special delete tasks
elif isinstance(thing, Comment):
@@ -567,11 +580,11 @@ class ApiController(RedditController):
@validatedForm(VUser(),
VModhash(),
item = VByNameIfAuthor('thing_id'),
text = VComment('text'))
text = VMarkdown('text'))
def POST_editusertext(self, form, jquery, item, text):
if not form.has_errors("text",
errors.NO_TEXT, errors.TOO_LONG,
errors.NOT_AUTHOR):
if (not form.has_errors("text",
errors.NO_TEXT, errors.TOO_LONG) and
not form.has_errors("thing_id", errors.NOT_AUTHOR)):
if isinstance(item, Comment):
kind = 'comment'
@@ -580,7 +593,10 @@ class ApiController(RedditController):
kind = 'link'
item.selftext = text
if (item._date < timeago('60 seconds')
if item._deleted:
return abort(403, "forbidden")
if (item._date < timeago('3 minutes')
or (item._ups + item._downs > 2)):
item.editted = True
@@ -601,7 +617,7 @@ class ApiController(RedditController):
prefix = "rate_comment_"),
ip = ValidIP(),
parent = VSubmitParent(['thing_id', 'parent']),
comment = VComment(['text', 'comment']))
comment = VMarkdown(['text', 'comment']))
def POST_comment(self, commentform, jquery, parent, comment, ip):
should_ratelimit = True
#check the parent type here cause we need that for the
@@ -633,7 +649,8 @@ class ApiController(RedditController):
not commentform.has_errors("ratelimit",
errors.RATELIMIT) and
not commentform.has_errors("parent",
errors.DELETED_COMMENT)):
errors.DELETED_COMMENT,
errors.DELETED_LINK)):
if is_message:
to = Account._byID(parent.author_id)
@@ -650,11 +667,9 @@ class ApiController(RedditController):
queries.queue_vote(c.user, item, True, ip,
cheater = (errors.CHEATER, None) in c.errors)
#update last modified
set_last_modified(link, 'comments')
#update the comment cache
add_comment(item)
# adding to comments-tree is done as part of
# newcomments_q, so if they refresh immediately they
# won't see their comment
# clean up the submission form and remove it from the DOM (if reply)
t = commentform.find("textarea")
@@ -666,7 +681,7 @@ class ApiController(RedditController):
# insert the new comment
jquery.insert_things(item)
# remove any null listings that may be present
jquery("#noresults").hide()
@@ -751,14 +766,12 @@ class ApiController(RedditController):
return
if vote_type == "rejected":
g.log.error("POST_vote: rejected vote (%s) from '%s' on %s (%s)"%
(request.params.get('dir'), c.user.name,
thing._fullname, request.ip))
reject_vote(thing)
store = False
# TODO: temporary hack until we migrate the rest of the vote data
if thing._date < datetime(2009, 4, 17, 0, 0, 0, 0, g.tz):
g.log.error("POST_vote: ignoring old vote on %s" % thing._fullname)
g.log.debug("POST_vote: ignoring old vote on %s" % thing._fullname)
store = False
# in a lock to prevent duplicate votes from people
@@ -982,21 +995,20 @@ class ApiController(RedditController):
name = VSubredditName("name"),
title = VLength("title", max_length = 100),
domain = VCnameDomain("domain"),
description = VLength("description", max_length = 1000),
description = VMarkdown("description", max_length = 1000),
lang = VLang("lang"),
over_18 = VBoolean('over_18'),
allow_top = VBoolean('allow_top'),
show_media = VBoolean('show_media'),
use_whitelist = VBoolean('use_whitelist'),
type = VOneOf('type', ('public', 'private', 'restricted')),
ip = ValidIP(),
ad_type = VOneOf('ad', ('default', 'basic', 'custom')),
ad_file = VLength('ad-location', max_length = 500),
sponsor_text =VLength('sponsorship-text', max_length = 500),
sponsor_name =VLength('sponsorship-name', max_length = 500),
sponsor_url = VLength('sponsorship-url', max_length = 500),
css_on_cname = VBoolean("css_on_cname"),
)
def POST_site_admin(self, form, jquery, name, ip, sr, ad_type, ad_file,
def POST_site_admin(self, form, jquery, name, ip, sr,
sponsor_text, sponsor_url, sponsor_name, **kw):
# the status button is outside the form -- have to reset by hand
form.parent().set_html('.status', "")
@@ -1005,7 +1017,7 @@ class ApiController(RedditController):
kw = dict((k, v) for k, v in kw.iteritems()
if k in ('name', 'title', 'domain', 'description', 'over_18',
'show_media', 'type', 'lang', "css_on_cname",
'allow_top'))
'allow_top', 'use_whitelist'))
#if a user is banned, return rate-limit errors
if c.user._spam:
@@ -1054,10 +1066,6 @@ class ApiController(RedditController):
elif sr.is_moderator(c.user) or c.user_is_admin:
if c.user_is_admin:
sr.ad_type = ad_type
if ad_type != "custom":
ad_file = Subreddit._defaults['ad_file']
sr.ad_file = ad_file
sr.sponsorship_text = sponsor_text or ""
sr.sponsorship_url = sponsor_url or None
sr.sponsorship_name = sponsor_name or None
@@ -1137,50 +1145,58 @@ class ApiController(RedditController):
if r:
queries.new_savehide(r)
@noresponse(VUser(),
VModhash(),
thing = VByName('id', multiple = True))
def POST_collapse_message(self, thing):
if not thing:
def collapse_handler(self, things, collapse):
if not things:
return
for t in tup(thing):
things = tup(things)
srs = Subreddit._byID([t.sr_id for t in things if t.sr_id],
return_dict = True)
for t in things:
if hasattr(t, "to_id") and c.user._id == t.to_id:
t.to_collapse = True
t.to_collapse = collapse
elif hasattr(t, "author_id") and c.user._id == t.author_id:
t.author_collapse = True
t.author_collapse = collapse
elif isinstance(t, Message) and t.sr_id:
if srs[t.sr_id].is_moderator(c.user):
t.to_collapse = collapse
t._commit()
@noresponse(VUser(),
VModhash(),
thing = VByName('id', multiple = True))
def POST_uncollapse_message(self, thing):
things = VByName('id', multiple = True))
def POST_collapse_message(self, things):
self.collapse_handler(things, True)
@noresponse(VUser(),
VModhash(),
things = VByName('id', multiple = True))
def POST_uncollapse_message(self, things):
self.collapse_handler(things, False)
def unread_handler(self, thing, unread):
if not thing:
return
for t in tup(thing):
if hasattr(t, "to_id") and c.user._id == t.to_id:
t.to_collapse = False
elif hasattr(t, "author_id") and c.user._id == t.author_id:
t.author_collapse = False
t._commit()
# if the message has a recipient, try validating that
# desitination first (as it is cheaper and more common)
if not hasattr(thing, "to_id") or c.user._id == thing.to_id:
queries.set_unread(thing, c.user, unread)
# if the message is for a subreddit, check that next
if hasattr(thing, "sr_id"):
sr = thing.subreddit_slow
if sr and sr.is_moderator(c.user):
queries.set_unread(thing, sr, unread)
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
def POST_unread_message(self, thing):
if not thing:
return
if hasattr(thing, "to_id") and c.user._id != thing.to_id:
return
queries.set_unread(thing, True)
self.unread_handler(thing, True)
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
def POST_read_message(self, thing):
if not thing: return
if hasattr(thing, "to_id") and c.user._id != thing.to_id:
return
queries.set_unread(thing, False)
self.unread_handler(thing, False)
@noresponse(VUser(),
VModhash(),
@@ -1203,10 +1219,14 @@ class ApiController(RedditController):
@validatedForm(VUser(),
parent = VByName('parent_id'))
def POST_moremessages(self, form, jquery, parent):
if not parent.can_view():
if not parent.can_view_slow():
return self.abort(403,'forbidden')
builder = MessageBuilder(c.user, parent = parent, skip = False)
if parent.sr_id:
builder = SrMessageBuilder(parent.subreddit_slow,
parent = parent, skip = False)
else:
builder = UserMessageBuilder(c.user, parent = parent, skip = False)
listing = Listing(builder).listing()
a = []
for item in listing.things:
@@ -1221,19 +1241,18 @@ class ApiController(RedditController):
@validatedForm(link = VByName('link_id'),
sort = VMenu('where', CommentSortMenu),
children = VCommentIDs('children'),
depth = VInt('depth', min = 0, max = 8),
mc_id = nop('id'))
def POST_morechildren(self, form, jquery,
link, sort, children, depth, mc_id):
link, sort, children, mc_id):
user = c.user if c.user_is_loggedin else None
if not link or not link.subreddit_slow.can_view(user):
return self.abort(403,'forbidden')
return abort(403,'forbidden')
if children:
builder = CommentBuilder(link, CommentSortMenu.operator(sort),
children)
listing = Listing(builder, nextprev = False)
items = listing.get_items(starting_depth = depth, num = 20)
items = listing.get_items(num = 20)
def _children(cur_items):
items = []
for cm in cur_items:
@@ -1399,6 +1418,107 @@ class ApiController(RedditController):
tr._is_enabled = True
@validatedForm(VAdmin(),
hexkey=VLength("hexkey", max_length=32),
nickname=VLength("nickname", max_length = 1000),
status = VOneOf("status",
("new", "severe", "interesting", "normal", "fixed")))
def POST_edit_error(self, form, jquery, hexkey, nickname, status):
if form.has_errors(("hexkey", "nickname", "status"),
errors.NO_TEXT, errors.INVALID_OPTION):
pass
if form.has_error():
return
key = "error_nickname-%s" % str(hexkey)
g.hardcache.set(key, nickname, 86400 * 365)
key = "error_status-%s" % str(hexkey)
g.hardcache.set(key, status, 86400 * 365)
form.set_html(".status", _('saved'))
@validatedForm(VSponsor(),
ad = VByName("fullname"),
colliding_ad=VAdByCodename(("codename", "fullname")),
codename = VLength("codename", max_length = 100),
imgurl = VLength("imgurl", max_length = 1000),
linkurl = VLength("linkurl", max_length = 1000))
def POST_editad(self, form, jquery, ad, colliding_ad, codename,
imgurl, linkurl):
if form.has_errors(("codename", "imgurl", "linkurl"),
errors.NO_TEXT):
pass
if form.has_errors(("codename"), errors.INVALID_OPTION):
form.set_html(".status", "some other ad has that codename")
pass
if form.has_error():
return
if ad is None:
Ad._new(codename, imgurl, linkurl)
form.set_html(".status", "saved. reload to see it.")
return
ad.codename = codename
ad.imgurl = imgurl
ad.linkurl = linkurl
ad._commit()
form.set_html(".status", _('saved'))
@validatedForm(VSponsor(),
ad = VByName("fullname"),
sr = VSubmitSR("community"),
weight = VInt("weight",
coerce=False, min=0, max=100000),
)
def POST_assignad(self, form, jquery, ad, sr, weight):
if form.has_errors("ad", errors.NO_TEXT):
pass
if form.has_errors("community", errors.SUBREDDIT_REQUIRED,
errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED):
pass
if form.has_errors("fullname", errors.NO_TEXT):
pass
if form.has_errors("weight", errors.BAD_NUMBER):
pass
if form.has_error():
return
if ad.codename == "DART" and sr.name == g.default_sr and weight != 100:
log_text("Bad default DART weight",
"The default DART weight can only be 100, not %s."
% weight,
"error")
abort(403, 'forbidden')
existing = AdSR.by_ad_and_sr(ad, sr)
if weight is not None:
if existing:
existing.weight = weight
existing._commit()
else:
AdSR._new(ad, sr, weight)
form.set_html(".status", _('saved'))
else:
if existing:
existing._delete()
AdSR.by_ad(ad, _update=True)
AdSR.by_sr(sr, _update=True)
form.set_html(".status", _('deleted'))
@validatedForm(VAdmin(),
award = VByName("fullname"),
colliding_award=VAwardByCodename(("codename", "fullname")),
@@ -1448,10 +1568,8 @@ class ApiController(RedditController):
if form.has_errors("award", errors.NO_TEXT):
pass
if form.has_errors("recipient", errors.USER_DOESNT_EXIST):
pass
if form.has_errors("recipient", errors.NO_USER):
if form.has_errors("recipient", errors.USER_DOESNT_EXIST,
errors.NO_USER):
pass
if form.has_errors("fullname", errors.NO_TEXT):
@@ -1488,6 +1606,7 @@ class ApiController(RedditController):
return self.abort404()
recipient = trophy._thing1
award = trophy._thing2
trophy._delete()
Trophy.by_account(recipient, _update=True)
Trophy.by_award(award, _update=True)
@@ -1566,7 +1685,7 @@ class ApiController(RedditController):
"%s_%s" % (s._fullname, s.sponsorship_name))
@json_validate(query = nop('query'))
@json_validate(query = VPrintable('query', max_length = 50))
def POST_search_reddit_names(self, query):
names = []
if query:

View File

@@ -19,7 +19,7 @@
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from reddit_base import RedditController
from reddit_base import RedditController, MinimalController, make_key
from r2.lib.pages import Button, ButtonNoBody, ButtonEmbed, ButtonLite, \
ButtonDemoPanel, WidgetDemoPanel, Bookmarklets, BoringPage
from r2.lib.pages.things import wrap_links
@@ -31,6 +31,55 @@ from pylons.i18n import _
from r2.lib.filters import spaceCompress
from r2.controllers.listingcontroller import ListingController
class ButtonjsController(MinimalController):
def pre(self):
MinimalController.pre(self)
# override user loggedin behavior to ensure this page always
# uses the page cache
(user, maybe_admin) = \
valid_cookie(c.cookies[g.login_cookie].value
if g.login_cookie in c.cookies
else '')
if user:
self.user_is_loggedin = True
@validate(buttontype = VInt('t', 1, 5),
url = VSanitizedUrl("url"),
_height = VInt('height', 0, 300),
_width = VInt('width', 0, 800),
autohide = VBoolean("autohide"))
def GET_button_embed(self, buttontype, _height, _width, url, autohide):
# no buttons on domain listings
if isinstance(c.site, DomainSR):
return self.abort404()
c.render_style = 'js'
c.response_content_type = 'text/javascript; charset=UTF-8'
if not c.user_is_loggedin and autohide:
c.response.content = "void(0);"
return c.response
buttontype = buttontype or 1
width, height = ((120, 22), (51, 69), (69, 52),
(51, 52), (600, 52))[min(buttontype - 1, 4)]
if _width: width = _width
if _height: height = _height
bjs = ButtonEmbed(button=buttontype,
width=width,
height=height,
url = url,
referer = request.referer).render()
return self.sendjs(bjs, callback='', escape=False)
def request_key(self):
return make_key('button_request_key',
c.lang,
c.content_langs,
request.host,
c.cname,
request.referer,
request.fullpath)
class ButtonsController(RedditController):
def buttontype(self):
b = request.get.get('t') or 1
@@ -83,7 +132,6 @@ class ButtonsController(RedditController):
width = VInt('width', 0, 800),
l = VByName('id'))
def GET_button_content(self, url, title, css, vote, newwindow, width, l):
# no buttons on domain listings
if isinstance(c.site, DomainSR):
c.site = Default
@@ -108,12 +156,9 @@ class ButtonsController(RedditController):
button = self.buttontype(), **kw)
l = self.get_wrapped_link(url, l, wrapper)
res = l.render()
c.response.content = spaceCompress(res)
return c.response
return l.render()
@validate(buttontype = VInt('t', 1, 5),
url = VSanitizedUrl("url"),
_height = VInt('height', 0, 300),
@@ -191,5 +236,3 @@ class ButtonsController(RedditController):
return BoringPage(_("bookmarklets"),
show_sidebar = False,
content=Bookmarklets()).render()

View File

@@ -32,7 +32,7 @@ from r2.lib.filters import safemarkdown, unsafe
try:
# place all r2 specific imports in here. If there is a code error, it'll get caught and
# the stack trace won't be presented to the user in production
from reddit_base import RedditController
from reddit_base import RedditController, Cookies
from r2.models.subreddit import Default, Subreddit
from r2.models.link import Link
from r2.lib import pages
@@ -122,7 +122,7 @@ class ErrorController(RedditController):
c.site.name)
message = (strings.banned_subreddit %
dict(link = '/message/compose?to=%s&subject=%s' %
(g.admin_message_acct,
(url_escape(g.admin_message_acct),
url_escape(subject))))
res = pages.RedditError(_('this reddit has been banned'),
@@ -146,8 +146,8 @@ class ErrorController(RedditController):
def GET_document(self):
try:
#no cookies on errors
c.cookies.clear()
# clear cookies the old fashioned way
c.cookies = Cookies()
code = request.GET.get('code', '')
srname = request.GET.get('srname', '')
@@ -155,7 +155,7 @@ class ErrorController(RedditController):
if srname:
c.site = Subreddit._by_name(srname)
if c.render_style not in self.allowed_render_styles:
return str(code)
return str(int(code))
elif takedown and code == '404':
link = Link._by_fullname(takedown)
return pages.TakedownPage(link).render()

View File

@@ -0,0 +1,34 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from pylons import request, g
from reddit_base import RedditController
from r2.lib.pages import AdminPage, AdminErrorLog
from validator import *
class ErrorlogController(RedditController):
@validate(VAdmin())
def GET_index(self):
res = AdminPage(content = AdminErrorLog(),
title = 'error log',
show_sidebar = False
).render()
return res

View File

@@ -24,8 +24,8 @@ from pylons.i18n import _
from copy import copy
error_list = dict((
('USER_REQUIRED', _("please login to do that")),
('VERIFIED_USER_REQUIRED', _("you need to set a valid email address to do that.")),
('USER_REQUIRED', _("please login to do that")),
('VERIFIED_USER_REQUIRED', _("you need to set a valid email address to do that.")),
('NO_URL', _('a url is required')),
('BAD_URL', _('you should check that url')),
('BAD_CAPTCHA', _('care to try these again?')),
@@ -33,9 +33,10 @@ error_list = dict((
('USERNAME_TAKEN', _('that username is already taken')),
('NO_THING_ID', _('id not specified')),
('NOT_AUTHOR', _("you can't do that")),
('DELETED_LINK', _('the link you are commenting on has been deleted')),
('DELETED_COMMENT', _('that comment has been deleted')),
('DELETED_THING', _('that element has been deleted.')),
('BAD_PASSWORD', _('invalid password')),
('DELETED_THING', _('that element has been deleted')),
('BAD_PASSWORD', _('that password is unacceptable')),
('WRONG_PASSWORD', _('invalid password')),
('BAD_PASSWORD_MATCH', _('passwords do not match')),
('NO_NAME', _('please enter a name')),
@@ -47,6 +48,7 @@ error_list = dict((
('NO_USER', _('please enter a username')),
('INVALID_PREF', "that preference isn't valid"),
('BAD_NUMBER', _("that number isn't in the right range (%(min)d to %(max)d)")),
('BAD_STRING', _("you used a character here that we can't handle")),
('BAD_BID', _("your bid must be at least $%(min)d per day and no more than to $%(max)d in total.")),
('ALREADY_SUB', _("that link has already been submitted")),
('SUBREDDIT_EXISTS', _('that reddit already exists')),
@@ -58,7 +60,6 @@ error_list = dict((
('EXPIRED', _('your session has expired')),
('DRACONIAN', _('you must accept the terms first')),
('BANNED_IP', "IP banned"),
('BANNED_DOMAIN', "Domain banned"),
('BAD_CNAME', "that domain isn't going to work"),
('USED_CNAME', "that domain is already in use"),
('INVALID_OPTION', _('that option is not valid')),

View File

@@ -89,13 +89,15 @@ class FrontController(RedditController):
else:
links = list(links)[:g.num_serendipity]
rand.shuffle(links)
builder = IDBuilder(links, skip = True,
keep_fn = lambda x: x.fresh,
num = g.num_serendipity)
num = 1)
links = builder.get_items()[0]
if links:
l = rand.choice(links)
l = links[0]
return self.redirect(add_sr("/tb/" + l._id36))
else:
return self.redirect(add_sr('/'))
@@ -274,8 +276,12 @@ class FrontController(RedditController):
content.append(FriendList())
elif location == 'update':
content = PrefUpdate()
elif location == 'feeds' and c.user.pref_private_feeds:
content = PrefFeeds()
elif location == 'delete':
content = PrefDelete()
else:
return self.abort404()
return PrefsPage(content = content, infotext=infotext).render()
@@ -310,7 +316,7 @@ class FrontController(RedditController):
# moderator is either reddit's moderator or an admin
is_moderator = c.user_is_loggedin and c.site.is_moderator(c.user) or c.user_is_admin
extension_handling = False
if is_moderator and location == 'edit':
pane = PaneStack()
if created == 'true':
@@ -320,8 +326,11 @@ class FrontController(RedditController):
pane = ModList(editable = is_moderator)
elif is_moderator and location == 'banned':
pane = BannedList(editable = is_moderator)
elif location == 'contributors' and c.site.type != 'public':
pane = ContributorList(editable = is_moderator)
elif (location == 'contributors' and
(c.site.type != 'public' or
(c.user_is_loggedin and c.site.use_whitelist and
(c.site.is_moderator(c.user) or c.user_is_admin)))):
pane = ContributorList(editable = is_moderator)
elif (location == 'stylesheet'
and c.site.can_change_stylesheet(c.user)
and not g.css_killswitch):
@@ -338,18 +347,35 @@ class FrontController(RedditController):
else c.site.get_spam())
builder_cls = (QueryBuilder if isinstance(query, thing.Query)
else IDBuilder)
def keep_fn(x):
# no need to bother mods with banned users, or deleted content
if x.hidden or x._deleted:
return False
if location == "reports" and not x._spam:
return (x.reported > 0)
if location == "spam":
return x._spam
return True
builder = builder_cls(query,
skip = True,
num = num, after = after,
keep_fn = keep_fn,
count = count, reverse = reverse,
wrap = ListingController.builder_wrapper)
listing = LinkListing(builder)
pane = listing.listing()
if c.user.pref_private_feeds:
extension_handling = "private"
elif is_moderator and location == 'traffic':
pane = RedditTraffic()
elif c.user_is_sponsor and location == 'ads':
pane = RedditAds()
else:
return self.abort404()
return EditReddit(content = pane).render()
return EditReddit(content = pane,
extension_handling = extension_handling).render()
def GET_awards(self):
"""The awards page."""
@@ -517,51 +543,50 @@ class FrontController(RedditController):
return builder.total_num, timing, res
def GET_login(self):
@validate(dest = VDestination())
def GET_login(self, dest):
"""The /login form. No link to this page exists any more on
the site (all actions invoking it now go through the login
cover). However, this page is still used for logging the user
in during submission or voting from the bookmarklets."""
# dest is the location to redirect to upon completion
dest = request.get.get('dest','') or request.referer or '/'
if (c.user_is_loggedin and
not request.environ.get('extension') == 'embed'):
return self.redirect(dest)
return LoginPage(dest = dest).render()
def GET_logout(self):
dest = request.referer or '/'
@validate(VUser(),
VModhash(),
dest = VDestination())
def GET_logout(self, dest):
return self.redirect(dest)
@validate(VUser(),
VModhash())
def POST_logout(self, dest = None):
VModhash(),
dest = VDestination())
def POST_logout(self, dest):
"""wipe login cookie and redirect to referer."""
self.logout()
dest = request.post.get('dest','') or request.referer or '/'
return self.redirect(dest)
@validate(VUser())
def GET_adminon(self):
@validate(VUser(),
dest = VDestination())
def GET_adminon(self, dest):
"""Enable admin interaction with site"""
#check like this because c.user_is_admin is still false
if not c.user.name in g.admins:
return self.abort404()
self.login(c.user, admin = True)
dest = request.referer or '/'
return self.redirect(dest)
@validate(VAdmin())
def GET_adminoff(self):
@validate(VAdmin(),
dest = VDestination())
def GET_adminoff(self, dest):
"""disable admin interaction with site."""
if not c.user.name in g.admins:
return self.abort404()
self.login(c.user, admin = False)
dest = request.referer or '/'
return self.redirect(dest)
def GET_validuser(self):
@@ -604,9 +629,9 @@ class FrontController(RedditController):
captcha = Captcha() if c.user.needs_captcha() else None
sr_names = (Subreddit.submit_sr_names(c.user) or
Subreddit.submit_sr_names(None))
return FormPage(_("submit"),
return FormPage(_("submit"),
show_sidebar = True,
content=NewLink(url=url or '',
title=title or '',
subreddits = sr_names,
@@ -714,7 +739,3 @@ class FrontController(RedditController):
def GET_site_traffic(self):
return BoringPage("traffic",
content = RedditTraffic()).render()
def GET_ad(self, reddit = None):
return Dart_Ad(reddit).render(style="html")

View File

@@ -8,6 +8,8 @@ from pylons import c, g
from reddit_base import RedditController
from r2.lib.amqp import worker
from validator import *
class HealthController(RedditController):
def shutdown(self):
thread_pool = c.thread_pool
@@ -40,9 +42,12 @@ class HealthController(RedditController):
c.response.content = "i'm still alive!"
return c.response
def GET_shutdown(self):
if not g.allow_shutdown:
@validate(secret=nop('secret'))
def GET_shutdown(self, secret):
if not g.shutdown_secret:
self.abort404()
if not secret or secret != g.shutdown_secret:
self.abort403()
c.dontcache = True
#the will make the next health-check initiate the shutdown

View File

@@ -122,7 +122,7 @@ class ListingController(RedditController):
builder_cls = SearchBuilder
elif isinstance(self.query_obj, iters):
builder_cls = IDBuilder
elif isinstance(self.query_obj, queries.CachedResults):
elif isinstance(self.query_obj, (queries.CachedResults, queries.MergedCachedResults)):
builder_cls = IDBuilder
b = builder_cls(self.query_obj,
@@ -253,7 +253,7 @@ class HotController(FixListing, ListingController):
and not isinstance(c.site, FakeSubreddit)
and self.after is None
and self.count == 0):
return get_hot(c.site, only_fullnames = True)
return get_hot([c.site], only_fullnames = True)[0]
else:
return c.site.get_links('hot', 'all')
@@ -286,16 +286,6 @@ class SavedController(ListingController):
def GET_listing(self, **env):
return ListingController.GET_listing(self, **env)
class ToplinksController(ListingController):
where = 'toplinks'
title_text = _('top scoring links')
def query(self):
return c.site.get_links('toplinks', 'all')
def GET_listing(self, **env):
return ListingController.GET_listing(self, **env)
class NewController(ListingController):
where = 'new'
title_text = _('newest submissions')
@@ -503,11 +493,12 @@ class UserController(ListingController):
class MessageController(ListingController):
show_sidebar = False
show_nums = False
render_cls = MessagePage
@property
def menus(self):
if self.where in ('inbox', 'messages', 'comments',
if c.default_sr and self.where in ('inbox', 'messages', 'comments',
'selfreply', 'unread'):
buttons = (NavButton(_("all"), "inbox"),
NavButton(_("unread"), "unread"),
@@ -517,12 +508,27 @@ class MessageController(ListingController):
return [NavMenu(buttons, base_path = '/message/',
default = 'inbox', type = "flatlist")]
elif not c.default_sr or self.where == 'moderator':
buttons = (NavButton(_("all"), "inbox"),
NavButton(_("unread"), "unread"))
return [NavMenu(buttons, base_path = '/message/moderator/',
default = 'inbox', type = "flatlist")]
return []
def title(self):
return _('messages') + ': ' + _(self.where)
def keep_fn(self):
def keep(item):
wouldkeep = item.keep_item(item)
# don't show user their own unread stuff
if ((self.where == 'unread' or self.subwhere == 'unread')
and item.author_id == c.user._id):
return False
return wouldkeep
return keep
@staticmethod
def builder_wrapper(thing):
if isinstance(thing, Comment):
@@ -539,24 +545,32 @@ class MessageController(ListingController):
return w
def builder(self):
if self.where == 'messages':
if (self.where == 'messages' or
(self.where == "moderator" and self.subwhere != "unread")):
root = c.user
message_cls = UserMessageBuilder
if not c.default_sr:
root = c.site
message_cls = SrMessageBuilder
elif self.where == 'moderator' and self.subwhere != 'unread':
message_cls = ModeratorMessageBuilder
parent = None
skip = False
if self.message:
if self.message.first_message:
parent = Message._byID(self.message.first_message)
else:
parent = self.message
return MessageBuilder(c.user, parent = parent,
skip = False,
focal = self.message,
wrap = self.builder_wrapper,
num = self.num)
elif c.user.pref_threaded_messages:
skip = (c.render_style == "html")
return MessageBuilder(c.user, wrap = self.builder_wrapper,
skip = skip,
num = self.num,
after = self.after,
reverse = self.reverse)
return message_cls(root, wrap = self.builder_wrapper,
parent = parent,
skip = skip,
num = self.num,
after = self.after,
reverse = self.reverse)
return ListingController.builder(self)
def listing(self):
@@ -578,7 +592,22 @@ class MessageController(ListingController):
q = queries.get_unread_inbox(c.user)
elif self.where == 'sent':
q = queries.get_sent(c.user)
elif self.where == 'moderator' and self.subwhere == 'unread':
if c.default_sr:
srids = Subreddit.reverse_moderator_ids(c.user)
srs = Subreddit._byID(srids, data = False, return_dict = False)
q = queries.merge_results(
*[queries.get_unread_subreddit_messages(s) for s in srs])
else:
q = queries.get_unread_subreddit_messages(c.site)
elif self.where == 'moderator':
if c.have_mod_messages and self.mark != 'false':
c.user.modmsgtime = False
c.user._commit()
# the query is handled by the builder on the moderator page
return
else:
return self.abort404()
if self.where != 'sent':
#reset the inbox
if c.have_messages and self.mark != 'false':
@@ -590,11 +619,16 @@ class MessageController(ListingController):
@validate(VUser(),
message = VMessageID('mid'),
mark = VOneOf('mark',('true','false'), default = 'true'))
def GET_listing(self, where, mark, message, **env):
self.where = where
def GET_listing(self, where, mark, message, subwhere = None, **env):
if not (c.default_sr or c.site.is_moderator(c.user) or c.user_is_admin):
abort(403, "forbidden")
if not c.default_sr:
self.where = "moderator"
else:
self.where = where
self.subwhere = subwhere
self.mark = mark
self.message = message
c.msg_location = where
return ListingController.GET_listing(self, **env)
@validate(VUser(),
@@ -609,7 +643,7 @@ class MessageController(ListingController):
message = message,
success = success)
return MessagePage(content = content).render()
class RedditsController(ListingController):
render_cls = SubredditsPage
@@ -645,7 +679,8 @@ class MyredditsController(ListingController):
NavButton(plurals.contributor, 'contributor'),
NavButton(plurals.moderator, 'moderator'))
return [NavMenu(buttons, base_path = '/reddits/mine/', default = 'subscriber', type = "flatlist")]
return [NavMenu(buttons, base_path = '/reddits/mine/',
default = 'subscriber', type = "flatlist")]
def title(self):
return _('reddits: ') + self.where

View File

@@ -20,23 +20,24 @@
# CondeNet, Inc. All Rights Reserved.
################################################################################
from validator import *
from reddit_base import RedditController
from reddit_base import MinimalController
from r2.lib.scraper import scrapers
from r2.lib.pages import MediaEmbedBody
from r2.lib.pages import MediaEmbedBody, ComScore, render_ad
from pylons import request
from pylons.controllers.util import abort
class MediaembedController(RedditController):
class MediaembedController(MinimalController):
@validate(link = VLink('link'))
def GET_mediaembed(self, link):
if request.host != g.media_domain:
# don't serve up untrusted content except on our
# specifically untrusted domain
return self.abort404()
abort(404)
if not link or not link.media_object:
return self.abort404()
abort(404)
if isinstance(link.media_object, basestring):
# it's an old-style string
@@ -50,3 +51,16 @@ class MediaembedController(RedditController):
content = media_embed.content
return MediaEmbedBody(body = content).render()
def GET_ad(self, reddit_name = None):
c.render_style = "html"
return render_ad(reddit_name=reddit_name)
def GET_ad_by_codename(self, codename = None):
if not codename:
abort(404)
c.render_style = "html"
return render_ad(codename=codename)
def GET_comscore(self, reddit = None):
return ComScore().render(style="html")

View File

@@ -29,29 +29,10 @@ from pylons.i18n import _
from r2.models import *
import sha
def to_referer(func, **params):
def _to_referer(self, *a, **kw):
res = func(self, *a, **kw)
dest = res.get('redirect') or request.referer or '/'
return self.redirect(dest + query_string(params))
return _to_referer
class PostController(ApiController):
def api_wrapper(self, kw):
return Storage(**kw)
#TODO: feature disabled for now
# @to_referer
# @validate(VUser(),
# key = VOneOf('key', ('pref_bio','pref_location',
# 'pref_url')),
# value = nop('value'))
# def POST_user_desc(self, key, value):
# setattr(c.user, key, value)
# c.user._commit()
# return {}
def set_options(self, all_langs, pref_lang, **kw):
if c.errors.errors:
print "fucker"
@@ -87,7 +68,9 @@ class PostController(ApiController):
self.set_options( all_langs, pref_lang)
return self.redirect(request.referer)
@validate(pref_frame = VBoolean('frame'),
@validate(VUser(),
VModhash(),
pref_frame = VBoolean('frame'),
pref_clickgadget = VBoolean('clickgadget'),
pref_organic = VBoolean('organic'),
pref_newwindow = VBoolean('newwindow'),
@@ -110,6 +93,7 @@ class PostController(ApiController):
pref_mark_messages_read = VBoolean("mark_messages_read"),
pref_threaded_messages = VBoolean("threaded_messages"),
pref_collapse_read_messages = VBoolean("collapse_read_messages"),
pref_private_feeds = VBoolean("private_feeds"),
all_langs = nop('all-langs', default = 'all'))
def POST_options(self, all_langs, pref_lang, **kw):
#temporary. eventually we'll change pref_clickgadget to an
@@ -176,12 +160,12 @@ class PostController(ApiController):
msg_hash = msg_hash)).render()
def POST_login(self, *a, **kw):
@validate(dest = VDestination(default = "/"))
def POST_login(self, dest, *a, **kw):
ApiController.POST_login(self, *a, **kw)
c.render_style = "html"
c.response_content_type = ""
dest = request.post.get('dest', request.referer or '/')
errors = list(c.errors)
if errors:
for e in errors:
@@ -190,18 +174,17 @@ class PostController(ApiController):
c.errors.remove(e)
c.errors.add(e[0], msg)
dest = request.post.get('dest', request.referer or '/')
return LoginPage(user_login = request.post.get('user'),
dest = dest).render()
return self.redirect(dest)
def POST_reg(self, *a, **kw):
@validate(dest = VDestination(default = "/"))
def POST_reg(self, dest, *a, **kw):
ApiController.POST_register(self, *a, **kw)
c.render_style = "html"
c.response_content_type = ""
dest = request.post.get('dest', request.referer or '/')
errors = list(c.errors)
if errors:
for e in errors:

View File

@@ -168,7 +168,7 @@ class PromoteController(ListingController):
promote.reject_promo(thing, reason = reason)
# also reject anything that is live but has a reason given
elif (c.user_is_sponsor and reason and
thing.promte_status == promote.STATUS.promoted):
thing.promote_status == promote.STATUS.promoted):
promote.reject_promo(thing, reason = reason)
# otherwise, mark it as "finished"
else:
@@ -220,6 +220,9 @@ class PromoteController(ListingController):
# want the URL
url = url[0].url
if form.has_errors('bid', errors.BAD_BID):
return
# check dates and date range
start, end = [x.date() for x in dates] if dates else (None, None)
if (not l or
@@ -242,7 +245,6 @@ class PromoteController(ListingController):
if (form.has_errors('title', errors.NO_TEXT,
errors.TOO_LONG) or
form.has_errors('url', errors.NO_URL, errors.BAD_URL) or
form.has_errors('bid', errors.BAD_BID) or
(not l and jquery.has_errors('ratelimit', errors.RATELIMIT))):
return
elif l:

View File

@@ -25,11 +25,11 @@ from pylons.controllers.util import abort, redirect_to
from pylons.i18n import _
from pylons.i18n.translation import LanguageError
from r2.lib.base import BaseController, proxyurl
from r2.lib import pages, utils, filters
from r2.lib import pages, utils, filters, amqp
from r2.lib.utils import http_utils, UniqueIterator
from r2.lib.cache import LocalCache
from r2.lib.cache import LocalCache, make_key, MemcachedError
import random as rand
from r2.models.account import valid_cookie, FakeAccount
from r2.models.account import valid_cookie, FakeAccount, valid_feed
from r2.models.subreddit import Subreddit
from r2.models import *
from errors import ErrorSet
@@ -37,12 +37,14 @@ from validator import *
from r2.lib.template_helpers import add_sr
from r2.lib.jsontemplates import api_type
from Cookie import CookieError
from copy import copy
from Cookie import CookieError
from datetime import datetime
import sha, simplejson, locale
from hashlib import sha1, md5
from urllib import quote, unquote
from simplejson import dumps
import simplejson
import locale
from r2.lib.tracking import encrypt, decrypt
@@ -224,7 +226,7 @@ def over18():
else:
if 'over18' in c.cookies:
cookie = c.cookies['over18'].value
if cookie == sha.new(request.ip).hexdigest():
if cookie == sha1(request.ip).hexdigest():
return True
def set_subreddit():
@@ -281,6 +283,13 @@ def set_content_type():
return utils.to_js(content,callback = request.params.get(
"callback", "document.write"))
c.response_wrappers.append(to_js)
if ext in ("rss", "api", "json") and request.method.upper() == "GET":
user = valid_feed(request.GET.get("user"),
request.GET.get("feed"),
request.path)
if user:
c.user = user
c.user_is_loggedin = True
def get_browser_langs():
browser_langs = []
@@ -403,6 +412,7 @@ def ratelimit_throttled():
if throttled(ip) or throttled(subnet):
abort(503, 'service temporarily unavailable')
#TODO i want to get rid of this function. once the listings in front.py are
#moved into listingcontroller, we shouldn't have a need for this
#anymore
@@ -411,13 +421,16 @@ def base_listing(fn):
after = VByName('after'),
before = VByName('before'),
count = VCount('count'),
target = VTarget("target"))
target = VTarget("target"),
show = VLength('show', 3))
def new_fn(self, before, **env):
if c.render_style == "htmllite":
c.link_target = env.get("target")
elif "target" in env:
del env["target"]
if "show" in env and env['show'] == 'all':
c.ignore_hide_rules = True
kw = build_arg_list(fn, env)
#turn before into after/reverse
@@ -429,40 +442,32 @@ def base_listing(fn):
return fn(self, **kw)
return new_fn
class RedditController(BaseController):
class MinimalController(BaseController):
def request_key(self):
# note that this references the cookie at request time, not
# the current value of it
cookie_keys = []
for x in cache_affecting_cookies:
cookie_keys.append(request.cookies.get(x,''))
try:
cookies_key = [(x, request.cookies.get(x,''))
for x in cache_affecting_cookies]
except CookieError:
cookies_key = ''
key = ''.join((str(c.lang),
str(c.content_langs),
request.host,
str(c.cname),
str(request.fullpath),
str(c.over18),
str(c.firsttime),
''.join(cookie_keys)))
return key
return make_key('request_key',
c.lang,
c.content_langs,
request.host,
c.cname,
request.fullpath,
c.over18,
c.firsttime,
cookies_key)
def cached_response(self):
return c.response
@staticmethod
def login(user, admin = False, rem = False):
c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(admin = admin),
expires = NEVER if rem else None)
@staticmethod
def logout(admin = False):
c.cookies[g.login_cookie] = Cookie(value='')
def pre(self):
c.start_time = datetime.now(g.tz)
g.reset_caches()
c.domain_prefix = request.environ.get("reddit-domain-prefix",
@@ -474,10 +479,106 @@ class RedditController(BaseController):
# the domain has to be set before Cookies get initialized
set_subreddit()
c.errors = ErrorSet()
c.cookies = Cookies()
def try_pagecache(self):
#check content cache
if not c.user_is_loggedin:
r = g.rendercache.get(self.request_key())
if r and request.method == 'GET':
response = c.response
response.headers = r.headers
response.content = r.content
for x in r.cookies.keys():
if x in cache_affecting_cookies:
cookie = r.cookies[x]
response.set_cookie(key = x,
value = cookie.value,
domain = cookie.get('domain',None),
expires = cookie.get('expires',None),
path = cookie.get('path',None))
response.status_code = r.status_code
request.environ['pylons.routes_dict']['action'] = 'cached_response'
# make sure to carry over the content type
c.response_content_type = r.headers['content-type']
if r.headers.has_key('access-control'):
c.response_access_control = r.headers['access-control']
c.used_cache = True
# response wrappers have already been applied before cache write
c.response_wrappers = []
def post(self):
response = c.response
content = filter(None, response.content)
if isinstance(content, (list, tuple)):
content = ''.join(content)
for w in c.response_wrappers:
content = w(content)
response.content = content
if c.response_content_type:
response.headers['Content-Type'] = c.response_content_type
if c.response_access_control:
c.response.headers['Access-Control'] = c.response_access_control
if c.user_is_loggedin:
response.headers['Cache-Control'] = 'no-cache'
response.headers['Pragma'] = 'no-cache'
# send cookies
if not c.used_cache and c.cookies:
# if we used the cache, these cookies should be set by the
# cached response object instead
for k,v in c.cookies.iteritems():
if v.dirty:
response.set_cookie(key = k,
value = quote(v.value),
domain = v.domain,
expires = v.expires)
#return
#set content cache
if (g.page_cache_time
and request.method == 'GET'
and not c.user_is_loggedin
and not c.used_cache
and not c.dontcache
and response.status_code != 503
and response.content and response.content[0]):
try:
g.rendercache.set(self.request_key(),
response,
g.page_cache_time)
except MemcachedError:
# the key was too big to set in the rendercache
g.log.debug("Ignored too-big render cache")
if g.enable_usage_stats:
amqp.add_kw("usage_q",
start_time = c.start_time,
end_time = datetime.now(g.tz),
action = str(c.action) or "static")
class RedditController(MinimalController):
@staticmethod
def login(user, admin = False, rem = False):
c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(admin = admin),
expires = NEVER if rem else None)
@staticmethod
def logout(admin = False):
c.cookies[g.login_cookie] = Cookie(value='')
def pre(self):
MinimalController.pre(self)
set_cnameframe()
# populate c.cookies unless we're on the unsafe media_domain
c.cookies = Cookies()
if request.host != g.media_domain or g.media_domain == g.domain:
try:
for k,v in request.cookies.iteritems():
@@ -489,7 +590,6 @@ class RedditController(BaseController):
request.environ['HTTP_COOKIE'] = ''
c.response_wrappers = []
c.errors = ErrorSet()
c.firsttime = firsttime()
(c.user, maybe_admin) = \
valid_cookie(c.cookies[g.login_cookie].value
@@ -515,6 +615,12 @@ class RedditController(BaseController):
read_mod_cookie()
if hasattr(c.user, 'msgtime') and c.user.msgtime:
c.have_messages = c.user.msgtime
if hasattr(c.user, 'modmsgtime'):
c.show_mod_mail = True
if c.user.modmsgtime:
c.have_mod_messages = c.user.modmsgtime
else:
c.show_mod_mail = Subreddit.reverse_moderator_ids(c.user)
c.user_is_admin = maybe_admin and c.user.name in g.admins
c.user_is_sponsor = c.user_is_admin or c.user.name in g.sponsors
if not g.disallow_db_writes:
@@ -560,74 +666,6 @@ class RedditController(BaseController):
elif c.site.domain and c.site.css_on_cname and not c.cname:
c.allow_styles = False
#check content cache
if not c.user_is_loggedin:
r = g.rendercache.get(self.request_key())
if r and request.method == 'GET':
response = c.response
response.headers = r.headers
response.content = r.content
for x in r.cookies.keys():
if x in cache_affecting_cookies:
cookie = r.cookies[x]
response.set_cookie(key = x,
value = cookie.value,
domain = cookie.get('domain',None),
expires = cookie.get('expires',None),
path = cookie.get('path',None))
response.status_code = r.status_code
request.environ['pylons.routes_dict']['action'] = 'cached_response'
# make sure to carry over the content type
c.response_content_type = r.headers['content-type']
if r.headers.has_key('access-control'):
c.response_access_control = r.headers['access-control']
c.used_cache = True
# response wrappers have already been applied before cache write
c.response_wrappers = []
def post(self):
response = c.response
content = filter(None, response.content)
if isinstance(content, (list, tuple)):
content = ''.join(content)
for w in c.response_wrappers:
content = w(content)
response.content = content
if c.response_content_type:
response.headers['Content-Type'] = c.response_content_type
if c.response_access_control:
c.response.headers['Access-Control'] = c.response_access_control
if c.user_is_loggedin:
response.headers['Cache-Control'] = 'no-cache'
response.headers['Pragma'] = 'no-cache'
# send cookies
if not c.used_cache and c.cookies:
# if we used the cache, these cookies should be set by the
# cached response object instead
for k,v in c.cookies.iteritems():
if v.dirty:
response.set_cookie(key = k,
value = quote(v.value),
domain = v.domain,
expires = v.expires)
#return
#set content cache
if (g.page_cache_time
and request.method == 'GET'
and not c.user_is_loggedin
and not c.used_cache
and not c.dontcache
and response.status_code != 503
and response.content and response.content[0]):
g.rendercache.set(self.request_key(),
response,
g.page_cache_time)
def check_modified(self, thing, action):
if c.user_is_loggedin:
return
@@ -644,6 +682,9 @@ class RedditController(BaseController):
def abort404(self):
abort(404, "not found")
def abort403(self):
abort(403, "forbidden")
def sendpng(self, string):
c.response_content_type = 'image/png'
c.response.content = string
@@ -661,7 +702,7 @@ class RedditController(BaseController):
return request.path + utils.query_string(merged)
def api_wrapper(self, kw):
data = dumps(kw)
data = simplejson.dumps(kw)
if request.method == "GET" and request.GET.get("callback"):
return "%s(%s)" % (websafe_json(request.GET.get("callback")),
websafe_json(data))

View File

@@ -0,0 +1,34 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from pylons import request, g
from reddit_base import RedditController
from r2.lib.pages import AdminPage, AdminUsage
from validator import *
class UsageController(RedditController):
@validate(VAdmin())
def GET_index(self):
res = AdminPage(content = AdminUsage(),
show_sidebar = False,
title = 'usage').render()
return res

View File

@@ -24,11 +24,12 @@ from pylons.i18n import _
from pylons.controllers.util import abort
from r2.lib import utils, captcha, promote
from r2.lib.filters import unkeep_space, websafe, _force_unicode
from r2.lib.filters import markdown_souptest
from r2.lib.db.operators import asc, desc
from r2.lib.template_helpers import add_sr
from r2.lib.jsonresponse import json_respond, JQueryResponse, JsonResponse
from r2.lib.jsontemplates import api_type
from r2.lib.log import log_text
from r2.models import *
from r2.lib.authorize import Address, CreditCard
@@ -37,6 +38,7 @@ from r2.controllers.errors import VerifiedUserRequiredException
from copy import copy
from datetime import datetime, timedelta
from curses.ascii import isprint
import re, inspect
import pycountry
@@ -123,7 +125,6 @@ def _make_validated_kw(fn, simple_vals, param_vals, env):
for var, validator in param_vals.iteritems():
kw[var] = validator(env)
return kw
def validate(*simple_vals, **param_vals):
def val(fn):
@@ -230,7 +231,7 @@ class VRequired(Validator):
if not e: e = self._error
if e:
self.set_error(e)
def run(self, item):
if not item:
self.error()
@@ -266,6 +267,25 @@ class VCommentByID(VThing):
def __init__(self, param, redirect = True, *a, **kw):
VThing.__init__(self, param, Comment, redirect=redirect, *a, **kw)
class VAd(VThing):
def __init__(self, param, redirect = True, *a, **kw):
VThing.__init__(self, param, Ad, redirect=redirect, *a, **kw)
class VAdByCodename(Validator):
def run(self, codename, required_fullname=None):
if not codename:
return self.set_error(errors.NO_TEXT)
try:
a = Ad._by_codename(codename)
except NotFound:
a = None
if a and required_fullname and a._fullname != required_fullname:
return self.set_error(errors.INVALID_OPTION)
else:
return a
class VAward(VThing):
def __init__(self, param, redirect = True, *a, **kw):
VThing.__init__(self, param, Award, redirect=redirect, *a, **kw)
@@ -314,7 +334,7 @@ class VMessageID(Validator):
try:
cid = int(cid, 36)
m = Message._byID(cid, True)
if not m.can_view():
if not m.can_view_slow():
abort(403, 'forbidden')
return m
except (NotFound, ValueError):
@@ -324,14 +344,23 @@ class VCount(Validator):
def run(self, count):
if count is None:
count = 0
return max(int(count), 0)
try:
return max(int(count), 0)
except ValueError:
return 0
class VLimit(Validator):
def run(self, limit):
if limit is None:
return c.user.pref_numsites
return min(max(int(limit), 1), 100)
return c.user.pref_numsites
try:
i = int(limit)
except ValueError:
return c.user.pref_numsites
return min(max(i, 1), 100)
class VCssMeasure(Validator):
measure = re.compile(r"^\s*[\d\.]+\w{0,3}\s*$")
@@ -371,23 +400,47 @@ class VLength(Validator):
self.set_error(self.length_error, {'max_length': self.max_length})
else:
return text
class VPrintable(VLength):
def run(self, text, text2 = ''):
text = VLength.run(self, text, text2)
if text is None:
return None
try:
if all(isprint(str(x)) for x in text):
return str(text)
except UnicodeEncodeError:
pass
self.set_error(errors.BAD_STRING)
return None
class VTitle(VLength):
def __init__(self, param, max_length = 300, **kw):
VLength.__init__(self, param, max_length, **kw)
class VComment(VLength):
def __init__(self, param, max_length = 10000, **kw):
VLength.__init__(self, param, max_length, **kw)
class VSelfText(VLength):
def __init__(self, param, max_length = 10000, **kw):
VLength.__init__(self, param, max_length, **kw)
class VMessage(VLength):
class VMarkdown(VLength):
def __init__(self, param, max_length = 10000, **kw):
VLength.__init__(self, param, max_length, **kw)
def run(self, text, text2 = ''):
text = text or text2
VLength.run(self, text)
try:
markdown_souptest(text)
return text
except ValueError:
import sys
user = "???"
if c.user_is_loggedin:
user = c.user.name
g.log.error("HAX by %s: %s" % (user, text))
s = sys.exc_info()
# reraise the original error with the original stack trace
raise s[1], None, s[2]
class VSubredditName(VRequired):
def __init__(self, item, *a, **kw):
@@ -422,7 +475,7 @@ class VSubredditDesc(Validator):
class VAccountByName(VRequired):
def __init__(self, param, error = errors.USER_DOESNT_EXIST, *a, **kw):
VRequired.__init__(self, param, error, *a, **kw)
def run(self, name):
if name:
try:
@@ -486,7 +539,7 @@ class VUser(Validator):
if (password is not None) and not valid_password(c.user, password):
self.set_error(errors.WRONG_PASSWORD)
class VModhash(Validator):
default_param = 'uh'
def run(self, uh):
@@ -595,7 +648,10 @@ class VSubmitParent(VByName):
if fullname:
parent = VByName.run(self, fullname)
if parent and parent._deleted:
self.set_error(errors.DELETED_COMMENT)
if isinstance(parent, Link):
self.set_error(errors.DELETED_LINK)
else:
self.set_error(errors.DELETED_COMMENT)
if isinstance(parent, Message):
return parent
else:
@@ -623,7 +679,7 @@ class VSubmitSR(Validator):
self.set_error(errors.SUBREDDIT_NOTALLOWED)
else:
return sr
pass_rx = re.compile(r"^.{3,20}$")
def chkpass(x):
@@ -633,12 +689,10 @@ class VPassword(Validator):
def run(self, password, verify):
if not chkpass(password):
self.set_error(errors.BAD_PASSWORD)
return
elif verify != password:
self.set_error(errors.BAD_PASSWORD_MATCH)
return password
else:
return password
return password.encode('utf8')
user_rx = re.compile(r"^[\w-]{3,20}$", re.UNICODE)
@@ -667,11 +721,15 @@ class VUname(VRequired):
class VLogin(VRequired):
def __init__(self, item, *a, **kw):
VRequired.__init__(self, item, errors.WRONG_PASSWORD, *a, **kw)
def run(self, user_name, password):
user_name = chkuser(user_name)
user = None
if user_name:
try:
str(password)
except UnicodeEncodeError:
password = password.encode('utf8')
user = valid_login(user_name, password)
if not user:
return self.error()
@@ -698,7 +756,7 @@ class VUrl(VRequired):
sr = None
else:
sr = None
if not url:
return self.error(errors.NO_URL)
url = utils.sanitize_url(url)
@@ -736,6 +794,21 @@ class VExistingUname(VRequired):
return self.error(errors.USER_DOESNT_EXIST)
self.error()
class VMessageRecipent(VExistingUname):
def run(self, name):
if not name:
return self.error()
if name.startswith('#'):
try:
s = Subreddit._by_name(name.strip('#'))
if isinstance(s, FakeSubreddit):
raise NotFound, "fake subreddit"
return s
except NotFound:
self.set_error(errors.SUBREDDIT_NOEXIST)
else:
return VExistingUname.run(self, name)
class VUserWithEmail(VExistingUname):
def run(self, name):
user = VExistingUname.run(self, name)
@@ -901,9 +974,11 @@ class VRatelimit(Validator):
class VCommentIDs(Validator):
#id_str is a comma separated list of id36's
def run(self, id_str):
cids = [int(i, 36) for i in id_str.split(',')]
comments = Comment._byID(cids, data=True, return_dict = False)
return comments
if id_str:
cids = [int(i, 36) for i in id_str.split(',')]
comments = Comment._byID(cids, data=True, return_dict = False)
return comments
return []
class CachedUser(object):
@@ -1049,14 +1124,9 @@ class ValidIP(Validator):
self.set_error(errors.BANNED_IP)
return request.ip
class ValidDomain(Validator):
class VOkayDomain(Validator):
def run(self, url):
if url and is_banned_domain(url):
self.set_error(errors.BANNED_DOMAIN)
return is_banned_domain(url)
class VDate(Validator):
"""
@@ -1135,9 +1205,32 @@ class VDestination(Validator):
def __init__(self, param = 'dest', default = "", **kw):
self.default = default
Validator.__init__(self, param, **kw)
def run(self, dest):
return dest or request.referer or self.default
if not dest:
dest = request.referer or self.default or "/"
ld = dest.lower()
if (ld.startswith("/") or
ld.startswith("http://") or
ld.startswith("https://")):
u = UrlParser(dest)
if u.is_reddit_url():
return dest
ip = getattr(request, "ip", "[unknown]")
fp = getattr(request, "fullpath", "[unknown]")
dm = c.domain or "[unknown]"
cn = c.cname or "[unknown]"
log_text("invalid redirect",
"%s attempted to redirect from %s to %s with domain %s and cname %s"
% (ip, fp, dest, dm, cn),
"info")
return "/"
class ValidAddress(Validator):
def __init__(self, param, usa_only = True):

File diff suppressed because it is too large Load Diff

View File

@@ -29,10 +29,10 @@ import time
import errno
import socket
import itertools
import pickle
from amqplib import client_0_8 as amqp
from r2.lib.cache import LocalCache
from pylons import g
amqp_host = g.amqp_host
@@ -88,7 +88,7 @@ def get_connection():
virtual_host = amqp_virtual_host,
insist = False)
except (socket.error, IOError):
print 'error connecting to amqp'
print 'error connecting to amqp %s @ %s' % (amqp_user, amqp_host)
time.sleep(1)
# don't run init_queue until someone actually needs it. this
@@ -160,26 +160,42 @@ def add_item(routing_key, body, message_id = None):
worker.do(_add_item, routing_key, body, message_id = message_id)
def handle_items(queue, callback, ack = True, limit = 1, drain = False):
def add_kw(routing_key, **kw):
add_item(routing_key, pickle.dumps(kw))
def handle_items(queue, callback, ack = True, limit = 1, drain = False,
verbose=True, sleep_time = 1):
"""Call callback() on every item in a particular queue. If the
connection to the queue is lost, it will die. Intended to be
used as a long-running process."""
chan = get_channel()
countdown = None
while True:
# NB: None != 0, so we don't need an "is not None" check here
if countdown == 0:
break
msg = chan.basic_get(queue)
if not msg and drain:
return
elif not msg:
time.sleep(1)
time.sleep(sleep_time)
continue
if countdown is None and drain and 'message_count' in msg.delivery_info:
countdown = 1 + msg.delivery_info['message_count']
g.reset_caches()
items = []
while msg:
while msg and countdown != 0:
items.append(msg)
if countdown is not None:
countdown -= 1
if len(items) >= limit:
break # the innermost loop only
msg = chan.basic_get(queue)
@@ -190,7 +206,8 @@ def handle_items(queue, callback, ack = True, limit = 1, drain = False):
# the count from the last message, if the count is
# available
count_str = '(%d remaining)' % items[-1].delivery_info['message_count']
print "%s: %d items %s" % (queue, len(items), count_str)
if verbose:
print "%s: %d items %s" % (queue, len(items), count_str)
callback(items, chan)
if ack:
@@ -205,6 +222,7 @@ def handle_items(queue, callback, ack = True, limit = 1, drain = False):
chan.basic_reject(item.delivery_tag, requeue = True)
raise
def empty_queue(queue):
"""debug function to completely erase the contents of a queue"""
chan = get_channel()

View File

@@ -23,8 +23,9 @@ from __future__ import with_statement
from pylons import config
import pytz, os, logging, sys, socket, re, subprocess
from datetime import timedelta, datetime
from r2.lib.cache import LocalCache, Memcache, HardCache, CacheChain
from r2.lib.cache import SelfEmptyingCache
from r2.lib.cache import LocalCache, SelfEmptyingCache
from r2.lib.cache import Memcache, Permacache, HardCache
from r2.lib.cache import MemcacheChain, DoubleMemcacheChain, PermacacheChain, HardcacheChain
from r2.lib.db.stats import QueryStats
from r2.lib.translation import get_active_langs
from r2.lib.lock import make_lock_factory
@@ -57,7 +58,7 @@ class Globals(object):
]
bool_props = ['debug', 'translator',
'log_start',
'log_start',
'sqlprinting',
'template_debug',
'uncompressedJS',
@@ -67,7 +68,9 @@ class Globals(object):
'css_killswitch',
'db_create_tables',
'disallow_db_writes',
'allow_shutdown']
'exception_logging',
'enable_usage_stats',
]
tuple_props = ['memcaches',
'rec_cache',
@@ -77,9 +80,9 @@ class Globals(object):
'sponsors',
'monitored_servers',
'automatic_reddits',
'skip_precompute_queries',
'agents',
'allowed_css_linked_domains']
'allowed_css_linked_domains',
'authorized_cnames']
def __init__(self, global_conf, app_conf, paths, **extra):
"""
@@ -89,22 +92,22 @@ class Globals(object):
One instance of Globals is created by Pylons during
application initialization and is available during requests
via the 'g' variable.
``global_conf``
The same variable used throughout ``config/middleware.py``
namely, the variables from the ``[DEFAULT]`` section of the
configuration file.
``app_conf``
The same ``kw`` dictionary used throughout
``config/middleware.py`` namely, the variables from the
section in the config file for your application.
``extra``
The configuration returned from ``load_config`` in
``config/middleware.py`` which may be of use in the setup of
your global variables.
"""
# slop over all variables to start with
@@ -122,21 +125,30 @@ class Globals(object):
self.running_as_script = global_conf.get('running_as_script', False)
self.skip_precompute_queries = set(self.skip_precompute_queries)
# initialize caches. Any cache-chains built here must be added
# to reset_caches so that they can properly reset their local
# components
mc = Memcache(self.memcaches, pickleProtocol = 1)
# to cache_chains (closed around by reset_caches) so that they
# can properly reset their local components
localcache_cls = SelfEmptyingCache if self.running_as_script else LocalCache
# we're going to temporarily run the old memcached behind the
# new one so the caches can start warmer
# mc = Memcache(self.memcaches, debug=self.debug)
mc = Permacache(self.memcaches)
rec_cache = Permacache(self.rec_cache)
rmc = Permacache(self.rendercaches)
pmc = Permacache(self.permacaches)
# hardcache is done after the db info is loaded, and then the
# chains are reset to use the appropriate initial entries
self.memcache = mc
self.cache = CacheChain((LocalCache(), mc))
self.permacache = Memcache(self.permacaches, pickleProtocol = 1)
self.rendercache = Memcache(self.rendercaches, pickleProtocol = 1)
self.cache = PermacacheChain((localcache_cls(), mc))
self.permacache = PermacacheChain((localcache_cls(), pmc))
self.rendercache = PermacacheChain((localcache_cls(), rmc))
self.rec_cache = rec_cache
self.make_lock = make_lock_factory(mc)
cache_chains = [self.cache, self.permacache, self.rendercache]
self.rec_cache = Memcache(self.rec_cache, pickleProtocol = 1)
# set default time zone if one is not set
tz = global_conf.get('timezone')
dtz = global_conf.get('display_timezone', tz)
@@ -148,9 +160,19 @@ class Globals(object):
self.dbm = self.load_db_params(global_conf)
# can't do this until load_db_params() has been called
self.hardcache = CacheChain((LocalCache(), mc, HardCache(self)),
cache_negative_results = True)
self.hardcache = HardcacheChain((localcache_cls(), mc, HardCache(self)),
cache_negative_results = True)
cache_chains.append(self.hardcache)
# I know this sucks, but we need non-request-threads to be
# able to reset the caches, so we need them be able to close
# around 'cache_chains' without being able to call getattr on
# 'g'
def reset_caches():
for chain in cache_chains:
chain.reset()
self.reset_caches = reset_caches
self.reset_caches()
#make a query cache
@@ -169,6 +191,8 @@ class Globals(object):
all_languages.sort()
self.all_languages = all_languages
self.paths = paths
# load the md5 hashes of files under static
static_files = os.path.join(paths.get('static_files'), 'static')
self.static_md5 = {}
@@ -185,6 +209,7 @@ class Globals(object):
#set up the logging directory
log_path = self.log_path
process_iden = global_conf.get('scgi_port', 'default')
self.reddit_port = process_iden
if log_path:
if not os.path.exists(log_path):
os.makedirs(log_path)
@@ -207,7 +232,8 @@ class Globals(object):
if not self.media_domain:
self.media_domain = self.domain
if self.media_domain == self.domain:
print "Warning: g.media_domain == g.domain. This may give untrusted content access to user cookies"
print ("Warning: g.media_domain == g.domain. " +
"This may give untrusted content access to user cookies")
#read in our CSS so that it can become a default for subreddit
#stylesheets
@@ -253,12 +279,6 @@ class Globals(object):
if self.log_start:
self.log.error("reddit app started %s at %s" % (self.short_version, datetime.now()))
def reset_caches(self):
for ca in ('cache', 'hardcache'):
cache = getattr(self, ca)
new_cache = SelfEmptyingCache() if self.running_as_script else LocalCache()
cache.caches = (new_cache,) + cache.caches[1:]
@staticmethod
def to_bool(x):
return (x.lower() == 'true') if x else None

View File

@@ -28,10 +28,12 @@ from r2.lib.utils import to_js
from r2.lib.filters import spaceCompress, _force_unicode
from r2.lib.template_helpers import get_domain
from utils import storify, string2js, read_http_date
from r2.lib.log import log_exception
import re, md5
from urllib import quote
from urllib import quote
import urllib2
import sys
#TODO hack
@@ -40,18 +42,22 @@ from r2.lib.utils import UrlParser, query_string
logging.getLogger('scgi-wsgi').setLevel(logging.CRITICAL)
class BaseController(WSGIController):
def __after__(self):
self.post()
def try_pagecache(self):
pass
def __before__(self):
self.pre()
self.try_pagecache()
def __after__(self):
self.post()
def __call__(self, environ, start_response):
true_client_ip = environ.get('HTTP_TRUE_CLIENT_IP')
ip_hash = environ.get('HTTP_TRUE_CLIENT_IP_HASH')
forwarded_for = environ.get('HTTP_X_FORWARDED_FOR', ())
remote_addr = environ.get('REMOTE_ADDR')
if (g.ip_hash
and true_client_ip
and ip_hash
@@ -93,9 +99,18 @@ class BaseController(WSGIController):
c.thread_pool = environ['paste.httpserver.thread_pool']
c.response = Response()
res = WSGIController.__call__(self, environ, start_response)
try:
res = WSGIController.__call__(self, environ, start_response)
except Exception as e:
if g.exception_logging:
try:
log_exception(e, *sys.exc_info())
except Exception as f:
print "log_exception() freaked out: %r" % f
print "sorry for breaking the stack trace:"
raise
return res
def pre(self): pass
def post(self): pass
@@ -154,7 +169,7 @@ class BaseController(WSGIController):
Reformats the new Location (dest) using format_output_url and
sends the user to that location with the provided HTTP code.
"""
dest = cls.format_output_url(dest)
dest = cls.format_output_url(dest or "/")
c.response.headers['Location'] = dest
c.response.status_code = code
return c.response

View File

@@ -0,0 +1,84 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "mkdio.h"
typedef struct rd_opts_s {
const char * target;
int nofollow;
} rd_opts_t;
char *
cb_flagmaker (const char * text, const int size, void * arg)
{
rd_opts_t * opts;
char * rv;
int rv_size;
int bytes_written;
#define TARGET_TAG "target="
#define NOFOLLOW " rel='nofollow'"
opts = (rd_opts_t *) arg;
if (opts->target == NULL) {
opts->target = "";
}
if (opts->target[0] == '\0') {
rv_size = 1; /* need room for a \0 */
} else {
/* Need to add 2 more, for the surrounding quotes */
rv_size = sizeof(TARGET_TAG) + strlen(opts->target) + 2;
}
if (opts->nofollow) {
/* We can subtract 1 because the \0 is already accounted for */
rv_size += sizeof(NOFOLLOW) - 1;
}
rv = malloc(rv_size);
bytes_written = 1 + sprintf (rv, "%s%s%s%s%s",
opts->target[0] == '\0' ? "" : TARGET_TAG,
opts->target[0] == '\0' ? "" : "'",
opts->target,
opts->target[0] == '\0' ? "" : "'",
opts->nofollow ? NOFOLLOW : "");
if (bytes_written > rv_size) {
fprintf (stderr, "Augh, allocated %d bytes and wrote %d bytes\n",
rv_size, bytes_written);
abort();
}
return rv;
}
void
reddit_discount_wrap(const char * text, int nofollow, const char * target,
void ** v_mmiot, char ** html, int * size)
{
rd_opts_t opts;
MMIOT * mmiot;
opts.target = target;
opts.nofollow = nofollow;
mmiot = mkd_string((char *) text, strlen(text), 0);
mkd_compile(mmiot, MKD_NOHTML | MKD_NOIMAGE | MKD_NOPANTS | MKD_NOHEADER |
MKD_NO_EXT | MKD_AUTOLINK | MKD_SAFELINK);
mkd_e_flags (mmiot, &cb_flagmaker);
mkd_e_context(mmiot, &opts);
*size = mkd_document(mmiot, html);
*v_mmiot = mmiot;
}
void
reddit_discount_cleanup (void * v_mmiot) {
mkd_cleanup(v_mmiot);
}

View File

@@ -1,2 +1,19 @@
from ctypes import cdll, c_int, c_void_p, byref, string_at
from r2.lib.filters import _force_utf8
from pylons import g
libmd = cdll.LoadLibrary(g.paths['root'] + '/../reddit-discount.so')
def c_markdown(text, nofollow=False, target=None):
raise NotImplementedError()
u8 = _force_utf8(text)
size = c_int(len(u8))
nofollow = 1 if nofollow else 0
doc = c_void_p()
html = c_void_p()
libmd.reddit_discount_wrap(u8, nofollow, target,
byref(doc), byref(html), byref(size))
r = string_at(html, size)
libmd.reddit_discount_cleanup(doc)
return r

View File

@@ -20,19 +20,22 @@
# CondeNet, Inc. All Rights Reserved.
################################################################################
from threading import local
from hashlib import md5
from utils import lstrips, in_chunks
import pylibmc
from _pylibmc import MemcachedError
from contrib import memcache
from utils import lstrips, in_chunks, tup
from r2.lib.hardcachebackend import HardCacheBackend
class NoneResult(object): pass
class CacheUtils(object):
def incr_multi(self, keys, delta=1, time=0, prefix=''):
def incr_multi(self, keys, delta=1, prefix=''):
for k in keys:
try:
self.incr(prefix + k, time=time, delta=delta)
self.incr(prefix + k, delta)
except ValueError:
pass
@@ -53,9 +56,14 @@ class CacheUtils(object):
return dict((key_map[k], r[k]) for k in r.keys())
class Memcache(CacheUtils, memcache.Client):
class Permacache(CacheUtils, memcache.Client):
"""We still use our patched python-memcache to talk to the
permacaches for legacy reasons"""
simple_get_multi = memcache.Client.get_multi
def __init__(self, servers):
memcache.Client.__init__(self, servers, pickleProtocol = 1)
def set_multi(self, keys, prefix='', time=0):
new_keys = {}
@@ -79,6 +87,62 @@ class Memcache(CacheUtils, memcache.Client):
memcache.Client.delete_multi(self, keys, time = time,
key_prefix = prefix)
def get_local_client(self):
return self # memcache.py handles this itself
class Memcache(CacheUtils, pylibmc.Client):
simple_get_multi = pylibmc.Client.get_multi
def __init__(self, servers,
debug = False,
binary=True,
noreply=False):
pylibmc.Client.__init__(self, servers, binary=binary)
behaviors = {'no_block': True, # use async I/O
'cache_lookups': True, # cache DNS lookups
'tcp_nodelay': True, # no nagle
'ketama': True, # consistant hashing
'_noreply': int(noreply),
'verify_key': int(debug)} # spend the CPU to verify keys
self.behaviors.update(behaviors)
self.local_clients = local()
def get_local_client(self):
# if this thread hasn't had one yet, make one
if not getattr(self.local_clients, 'client', None):
self.local_clients.client = self.clone()
return self.local_clients.client
def set_multi(self, keys, prefix='', time=0):
new_keys = {}
for k,v in keys.iteritems():
new_keys[str(k)] = v
pylibmc.Client.set_multi(self, new_keys, key_prefix = prefix,
time = time)
def incr(self, key, delta=1, time=0):
# ignore the time on these
return pylibmc.Client.incr(self, key, delta)
def add(self, key, val, time=0):
try:
return pylibmc.Client.add(self, key, val, time=time)
except pylibmc.DataExists:
return None
def get(self, key, default=None):
r = pylibmc.Client.get(self, key)
if r is None:
return default
return r
def set(self, key, val, time=0):
pylibmc.Client.set(self, key, val, time = time)
def delete_multi(self, keys, prefix='', time=0):
pylibmc.Client.delete_multi(self, keys, time = time,
key_prefix = prefix)
class HardCache(CacheUtils):
backend = None
@@ -161,7 +225,6 @@ class LocalCache(dict, CacheUtils):
for k in keys:
if self.has_key(k):
out[k] = self[k]
# print "Local cache answers: " + str(out)
return out
def set(self, key, val, time = 0):
@@ -175,7 +238,9 @@ class LocalCache(dict, CacheUtils):
def add(self, key, val, time = 0):
self._check_key(key)
return self.setdefault(key, val)
was = key in self
self.setdefault(key, val)
return not was
def delete(self, key):
if self.has_key(key):
@@ -221,6 +286,12 @@ class CacheChain(CacheUtils, local):
return ret
return fn
# note that because of the naive nature of `add' when used on a
# cache chain, its return value isn't reliable. if you need to
# verify its return value you'll either need to make it smarter or
# use the underlying cache directly
add = make_set_fn('add')
set = make_set_fn('set')
append = make_set_fn('append')
prepend = make_set_fn('prepend')
@@ -234,33 +305,6 @@ class CacheChain(CacheUtils, local):
flush_all = make_set_fn('flush_all')
cache_negative_results = False
def add(self, key, val, time=0):
authority = self.caches[-1]
added_val = authority.add(key, val, time=time)
for cache in self.caches[:-1]:
# Calling set() rather than add() to ensure that all caches are
# in sync and that de-syncs repair themselves
cache.set(key, added_val, time=time)
return added_val
def accrue(self, key, time=0, delta=1):
auth_value = self.caches[-1].get(key)
if auth_value is None:
self.caches[-1].set(key, 0, time)
auth_value = 0
try:
auth_value = int(auth_value)
except ValueError:
raise ValueError("Can't accrue %s; it's a %s (%r)" %
(key, auth_value.__class__.__name__, auth_value))
for c in self.caches:
c.set(key, auth_value, time=time)
self.incr(key, time=time, delta=delta)
def get(self, key, default = None, local = True):
for c in self.caches:
if not local and isinstance(c,LocalCache):
@@ -271,12 +315,12 @@ class CacheChain(CacheUtils, local):
if val is not None:
#update other caches
for d in self.caches:
if c == d:
if c is d:
break # so we don't set caches later in the chain
d.set(key, val)
if self.cache_negative_results and val is NoneResult:
return None
return default
else:
return val
@@ -293,12 +337,13 @@ class CacheChain(CacheUtils, local):
need = set(keys)
for c in self.caches:
if len(out) == len(keys):
# we've found them all
break
r = c.simple_get_multi(need)
#update other caches
if r:
for d in self.caches:
if c == d:
if c is d:
break # so we don't set caches later in the chain
d.set_multi(r)
r.update(out)
@@ -319,12 +364,89 @@ class CacheChain(CacheUtils, local):
return out
def __repr__(self):
return '<%s>' % (self.__class__.__name__,)
def debug(self, key):
print "Looking up [%r]" % key
for i, c in enumerate(self.caches):
print "[%d] %10s has value [%r]" % (i, c.__class__.__name__,
c.get(key))
def reset(self):
# the first item in a cache chain is a LocalCache
self.caches = (self.caches[0].__class__(),) + self.caches[1:]
class MemcacheChain(CacheChain):
def __init__(self, caches):
CacheChain.__init__(self, caches)
self.mc_master = self.caches[-1]
def reset(self):
CacheChain.reset(self)
localcache, old_mc = self.caches
self.caches = (localcache, self.mc_master.get_local_client())
class DoubleMemcacheChain(CacheChain):
"""Temporary cache chain that places the new cache ahead of the
old one for easier deployment"""
def __init__(self, caches):
self.caches = localcache, memcache, permacache = caches
self.mc_master = memcache
def reset(self):
CacheChain.reset(self)
self.caches = (self.caches[0],
self.mc_master.get_local_client(),
self.caches[2])
class PermacacheChain(CacheChain):
pass
class HardcacheChain(CacheChain):
def __init__(self, caches, cache_negative_results = False):
CacheChain.__init__(self, caches, cache_negative_results)
localcache, memcache, hardcache = self.caches
self.mc_master = memcache
def add(self, key, val, time=0):
authority = self.caches[-1] # the authority is the hardcache
# itself
added_val = authority.add(key, val, time=time)
for cache in self.caches[:-1]:
# Calling set() rather than add() to ensure that all caches are
# in sync and that de-syncs repair themselves
cache.set(key, added_val, time=time)
return added_val
def accrue(self, key, time=0, delta=1):
auth_value = self.caches[-1].get(key)
if auth_value is None:
self.caches[-1].set(key, 0, time)
auth_value = 0
try:
auth_value = int(auth_value) + delta
except ValueError:
raise ValueError("Can't accrue %s; it's a %s (%r)" %
(key, auth_value.__class__.__name__, auth_value))
for c in self.caches:
c.set(key, auth_value, time=time)
@property
def backend(self):
# the hardcache is always the last item in a HardCacheChain
return self.caches[-1].backend
def reset(self):
CacheChain.reset(self)
assert len(self.caches) == 3
self.caches = (self.caches[0],
self.mc_master.get_local_client(),
self.caches[2])
#smart get multi
def sgm(cache, keys, miss_fn, prefix='', time=0):
keys = set(keys)
@@ -340,34 +462,56 @@ def sgm(cache, keys, miss_fn, prefix='', time=0):
return dict((s_keys[k], v) for k,v in r.iteritems())
def test_cache(cache):
def test_cache(cache, prefix=''):
#basic set/get
cache.set('1', 1)
assert cache.get('1') == 1
cache.set('%s1' % prefix, 1)
assert cache.get('%s1' % prefix) == 1
#python data
cache.set('2', [1,2,3])
assert cache.get('2') == [1,2,3]
cache.set('%s2' % prefix, [1,2,3])
assert cache.get('%s2' % prefix) == [1,2,3]
#set multi, no prefix
cache.set_multi({'3':3, '4': 4})
assert cache.get_multi(('3', '4')) == {'3':3, '4': 4}
cache.set_multi({'%s3' % prefix:3, '%s4' % prefix: 4})
assert cache.get_multi(('%s3' % prefix, '%s4' % prefix)) == {'%s3' % prefix: 3,
'%s4' % prefix: 4}
#set multi, prefix
cache.set_multi({'3':3, '4': 4}, prefix='p_')
assert cache.get_multi(('3', 4), prefix='p_') == {'3':3, 4: 4}
assert cache.get_multi(('p_3', 'p_4')) == {'p_3':3, 'p_4': 4}
cache.set_multi({'3':3, '4': 4}, prefix='%sp_' % prefix)
assert cache.get_multi(('3', 4), prefix='%sp_' % prefix) == {'3':3, 4: 4}
assert cache.get_multi(('%sp_3' % prefix, '%sp_4' % prefix)) == {'%sp_3'%prefix: 3,
'%sp_4'%prefix: 4}
#incr
cache.set('5', 1)
cache.set('6', 1)
cache.incr('5')
assert cache.get('5') == 2
cache.incr('5',2)
assert cache.get('5') == 4
cache.incr_multi(('5', '6'), 1)
assert cache.get('5') == 5
assert cache.get('6') == 2
cache.set('%s5'%prefix, 1)
cache.set('%s6'%prefix, 1)
cache.incr('%s5'%prefix)
assert cache.get('%s5'%prefix) == 2
cache.incr('%s5'%prefix,2)
assert cache.get('%s5'%prefix) == 4
cache.incr_multi(('%s5'%prefix, '%s6'%prefix), 1)
assert cache.get('%s5'%prefix) == 5
assert cache.get('%s6'%prefix) == 2
def test_multi(cache):
from threading import Thread
num_threads = 100
num_per_thread = 1000
threads = []
for x in range(num_threads):
def _fn(prefix):
def __fn():
for y in range(num_per_thread):
test_cache(cache,prefix=prefix)
return __fn
t = Thread(target=_fn(str(x)))
t.start()
threads.append(t)
for thread in threads:
thread.join()
# a cache that occasionally dumps itself to be used for long-running
# processes
@@ -379,9 +523,38 @@ class SelfEmptyingCache(LocalCache):
if len(self) > self.max_size:
self.clear()
def set(self,key,val,time = 0):
def set(self, key, val, time=0):
self.maybe_reset()
return LocalCache.set(self,key,val,time)
def add(self,key,val):
return self.set(key,val)
def add(self, key, val, time=0):
self.maybe_reset()
return LocalCache.add(self, key, val)
def make_key(iden, *a, **kw):
"""
A helper function for making memcached-usable cache keys out of
arbitrary arguments. Hashes the arguments but leaves the `iden'
human-readable
"""
h = md5()
def _conv(s):
if isinstance(s, str):
return s
elif isinstance(s, unicode):
return s.encode('utf-8')
elif isinstance(s, (tuple, list)):
return ','.join(_conv(x) for x in s)
elif isinstance(s, dict):
return ','.join('%s:%s' % (_conv(k), _conv(v))
for (k, v) in sorted(s.iteritems()))
else:
return str(s)
iden = _conv(iden)
h.update(iden)
h.update(_conv(a))
h.update(_conv(kw))
return '%s(%s)' % (iden, h.hexdigest())

View File

@@ -24,6 +24,8 @@ from pylons import g
from Captcha.Base import randomIdentifier
from Captcha.Visual import Text, Backgrounds, Distortions, ImageCaptcha
from r2.lib.cache import make_key
IDEN_LENGTH = 32
SOL_LENGTH = 6
@@ -47,21 +49,24 @@ def make_solution():
return randomIdentifier(alphabet=string.ascii_letters, length = SOL_LENGTH).upper()
def get_image(iden):
solution = g.rendercache.get(str(iden))
key = make_key(iden)
solution = g.rendercache.get(key)
if not solution:
solution = make_solution()
g.rendercache.set(str(iden), solution, time = 300)
g.rendercache.set(key, solution, time = 300)
return RandCaptcha(solution=solution).render()
def valid_solution(iden, solution):
key = make_key(iden)
if (not iden
or not solution
or len(iden) != IDEN_LENGTH
or len(solution) != SOL_LENGTH
or solution.upper() != g.rendercache.get(str(iden))):
or solution.upper() != g.rendercache.get(key)):
solution = make_solution()
g.rendercache.set(str(iden), solution, time = 300)
g.rendercache.set(key, solution, time = 300)
return False
else:
g.rendercache.delete(str(iden))
g.rendercache.delete(key)
return True

View File

@@ -24,6 +24,7 @@ from __future__ import with_statement
from pylons import g
from itertools import chain
from utils import tup
from cache import sgm
def comments_key(link_id):
return 'comments_' + str(link_id)
@@ -86,10 +87,12 @@ def delete_comment(comment):
#nothing really to do here, atm
pass
def link_comments(link_id):
def link_comments(link_id, _update=False):
key = comments_key(link_id)
r = g.permacache.get(key)
if r:
if r and not _update:
return r
else:
with g.make_lock(lock_key(link_id)):
@@ -148,14 +151,22 @@ def add_message(message):
# add the message to the author's list and the recipient
with g.make_lock(messages_lock_key(message.author_id)):
add_message_nolock(message.author_id, message)
with g.make_lock(messages_lock_key(message.to_id)):
add_message_nolock(message.to_id, message)
if message.to_id:
with g.make_lock(messages_lock_key(message.to_id)):
add_message_nolock(message.to_id, message)
if message.sr_id:
with g.make_lock(sr_messages_lock_key(message.sr_id)):
add_sr_message_nolock(message.sr_id, message)
def add_message_nolock(user_id, message):
def _add_message_nolock(key, message):
from r2.models import Account, Message
key = messages_key(user_id)
trees = g.permacache.get(key)
if not trees:
# in case an empty list got written at some point, delete it to
# force a recompute
if trees is not None:
g.permacache.delete(key)
# no point computing it now. We'll do it when they go to
# their message page.
return
@@ -184,10 +195,11 @@ def add_message_nolock(user_id, message):
g.permacache.set(key, trees)
def conversation(user, parent):
from r2.models import Message
trees = dict(user_messages(user))
def add_message_nolock(user_id, message):
return _add_message_nolock(messages_key(user_id), message)
def _conversation(trees, parent):
from r2.models import Message
if parent._id in trees:
convo = trees[parent._id]
if convo:
@@ -202,42 +214,95 @@ def conversation(user, parent):
data = True)
return compute_message_trees([parent] + list(m))
def user_messages(user):
def conversation(user, parent):
trees = dict(user_messages(user))
return _conversation(trees, parent)
def user_messages(user, update = False):
key = messages_key(user._id)
trees = g.permacache.get(key)
if trees is None:
if not trees or update:
trees = user_messages_nocache(user)
g.permacache.set(key, trees)
return trees
def _process_message_query(inbox):
if hasattr(inbox, 'prewrap_fn'):
return [inbox.prewrap_fn(i) for i in inbox]
return list(inbox)
def _load_messages(mlist):
from r2.models import Message
m = {}
ids = [x for x in mlist if not isinstance(x, Message)]
if ids:
m = Message._by_fullname(ids, return_dict = True, data = True)
messages = [m.get(x, x) for x in mlist]
return messages
def user_messages_nocache(user):
"""
Just like user_messages, but avoiding the cache
"""
from r2.lib.db import queries
from r2.models import Message
inbox = queries.get_inbox_messages(user)
if hasattr(inbox, 'prewrap_fn'):
inbox = [inbox.prewrap_fn(i) for i in inbox]
else:
inbox = list(inbox)
sent = queries.get_sent(user)
if hasattr(sent, 'prewrap_fn'):
sent = [sent.prewrap_fn(i) for i in sent]
else:
sent = list(sent)
m = {}
ids = [x for x in chain(inbox, sent) if not isinstance(x, Message)]
if ids:
m = Message._by_fullname(ids, return_dict = True, data = True)
messages = [m.get(x, x) for x in chain(inbox, sent)]
inbox = _process_message_query(queries.get_inbox_messages(user))
sent = _process_message_query(queries.get_sent(user))
messages = _load_messages(list(chain(inbox, sent)))
return compute_message_trees(messages)
def sr_messages_key(sr_id):
return 'sr_messages_conversation_' + str(sr_id)
def sr_messages_lock_key(sr_id):
return 'sr_messages_conversation_lock_' + str(sr_id)
def subreddit_messages(sr, update = False):
key = sr_messages_key(sr._id)
trees = g.permacache.get(key)
if not trees or update:
trees = subreddit_messages_nocache(sr)
g.permacache.set(key, trees)
return trees
def moderator_messages(user):
from r2.models import Subreddit
sr_ids = Subreddit.reverse_moderator_ids(user)
def multi_load_tree(sr_ids):
srs = Subreddit._byID(sr_ids, return_dict = False)
res = {}
for sr in srs:
trees = subreddit_messages_nocache(sr)
if trees:
res[sr._id] = trees
return res
res = sgm(g.permacache, sr_ids, miss_fn = multi_load_tree,
prefix = sr_messages_key(""))
return sorted(chain(*res.values()), key = tree_sort_fn, reverse = True)
def subreddit_messages_nocache(sr):
"""
Just like user_messages, but avoiding the cache
"""
from r2.lib.db import queries
inbox = _process_message_query(queries.get_subreddit_messages(sr))
messages = _load_messages(inbox)
return compute_message_trees(messages)
def add_sr_message_nolock(sr_id, message):
return _add_message_nolock(sr_messages_key(sr_id), message)
def sr_conversation(sr, parent):
trees = dict(subreddit_messages(sr))
return _conversation(trees, parent)
def compute_message_trees(messages):
from r2.models import Message
roots = set()

View File

@@ -0,0 +1,47 @@
->Copyright (C) 2007 David Loren Parsons.
All rights reserved.<-
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicence, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions, and the following disclaimer.
2. Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution, and in the same place and form as other
copyright, license and disclaimer information.
3. The end-user documentation included with the redistribution, if
any, must include the following acknowledgment:
This product includes software developed by
David Loren Parsons <http://www.pell.portland.or.us/~orc>
in the same place and form as other third-party acknowledgments.
Alternately, this acknowledgment may appear in the software
itself, in the same form and location as other such third-party
acknowledgments.
4. Except as contained in this notice, the name of David Loren
Parsons shall not be used in advertising or otherwise to promote
the sale, use or other dealings in this Software without prior
written authorization from David Loren Parsons.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL DAVID LOREN PARSONS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,33 @@
Discount is primarily my work, but it has only reached the point
where it is via contributions, critiques, and bug reports from a
host of other people, some of which are listed before. If your
name isn't on this list, please remind me
-david parsons (orc@pell.chi.il.us)
Josh Wood -- Plan9 support.
Mike Schiraldi -- Reddit style automatic links, MANY MANY MANY
bug reports about boundary conditions and
places where I didn't get it right.
Jjgod Jiang -- Table of contents support.
Petite Abeille -- Many bug reports about places where I didn't
get it right.
Tim Channon -- inspiration for the `mkd_xhtmlpage()` function
Christian Herenz-- Many bug reports regarding my implementation of
`[]()` and `![]()`
A.S.Bradbury -- Portability bug reports for 64 bit systems.
Joyent -- Loan of a solaris box so I could get discount
working under solaris.
Ryan Tomayko -- Portability requests (and the rdiscount ruby
binding.)
yidabu -- feedback on the documentation, bug reports
against utf-8 support.
Pierre Joye -- bug reports, php discount binding.
Masayoshi Sekimura- perl discount binding.
Jeremy Hinegardner- bug reports about list handling.
Andrew White -- bug reports about the format of generated urls.
Steve Huff -- bug reports about Makefile portability (for Fink)
Ignacio Burgue?o-- bug reports about `>%class%`
Henrik Nyh -- bug reports about embedded html handling.

View File

@@ -0,0 +1,61 @@
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include "cstring.h"
#include "markdown.h"
#include "amalloc.h"
/* putc() into a cstring
*/
void
Csputc(int c, Cstring *iot)
{
EXPAND(*iot) = c;
}
/* printf() into a cstring
*/
int
Csprintf(Cstring *iot, char *fmt, ...)
{
va_list ptr;
int siz=100;
do {
RESERVE(*iot, siz);
va_start(ptr, fmt);
siz = vsnprintf(T(*iot)+S(*iot), ALLOCATED(*iot)-S(*iot), fmt, ptr);
va_end(ptr);
} while ( siz > (ALLOCATED(*iot)-S(*iot)) );
S(*iot) += siz;
return siz;
}
/* write() into a cstring
*/
int
Cswrite(Cstring *iot, char *bfr, int size)
{
RESERVE(*iot, size);
memcpy(T(*iot)+S(*iot), bfr, size);
S(*iot) += size;
return size;
}
/* reparse() into a cstring
*/
void
Csreparse(Cstring *iot, char *buf, int size, int flags)
{
MMIOT f;
___mkd_initmmiot(&f, 0);
___mkd_reparse(buf, size, 0, &f);
___mkd_emblock(&f);
SUFFIX(*iot, T(f.out), S(f.out));
___mkd_freemmiot(&f, 0);
}

View File

@@ -0,0 +1,41 @@
HOW TO BUILD AND INSTALL DISCOUNT
1) Unpacking the distribution
The DISCOUNT sources are distributed in tarballs. After extracting from
the tarball, you should end up with all the source and build files in the
directory
discount-(version)
2) Installing the distribution
DISCOUNT uses configure.sh to set itself up for compilation. To run
configure, just do ``./configure.sh'' and it will check your system for
build dependencies and build makefiles for you. If configure.sh finishes
without complaint, you can then do a ``make'' to compile everything and a
``make install'' to install the binaries.
Configure.sh has a few options that can be set:
--src=DIR where the source lives (.)
--prefix=DIR where to install the final product (/usr/local)
--execdir=DIR where to put executables (prefix/bin)
--sbindir=DIR where to put static executables (prefix/sbin)
--confdir=DIR where to put configuration information (/etc)
--libdir=DIR where to put libraries (prefix/lib)
--libexecdir=DIR where to put private executables
--mandir=DIR where to put manpages
--enable-dl-tag Use the DL tag extension
--enable-pandoc-header Use pandoc-style header blocks
--enable-superscript A^B expands to A<sup>B</sup>
--enable-amalloc Use a debugging memory allocator (to detect leaks)
--relaxed-emphasis Don't treat _ in the middle of a word as emphasis
--with-tabstops=N Set tabstops to N characters (default is 4)
3) Installing sample programs and manpages
The standard ``make install'' rule just installs the binaries. If you
want to install the sample programs, they are installed with
``make install.samples''; to install manpages, ``make install.man''.
A shortcut to install everything is ``make install.everything''

View File

@@ -0,0 +1,96 @@
CC=cc -I. -L.
AR=/usr/bin/ar
RANLIB=/usr/bin/ranlib
BINDIR=/usr/local/bin
MANDIR=/usr/local/man
LIBDIR=/usr/local/lib
INCDIR=/usr/local/include
PGMS=markdown
SAMPLE_PGMS=mkd2html makepage
SAMPLE_PGMS+= theme
MKDLIB=libmarkdown.a
OBJS=mkdio.o markdown.o dumptree.o generate.o \
resource.o docheader.o version.o toc.o css.o \
xml.o Csio.o xmlpage.o
all: $(PGMS) $(SAMPLE_PGMS)
install: $(PGMS)
/usr/bin/install -s -m 755 $(PGMS) $(DESTDIR)/$(BINDIR)
/usr/bin/install -m 444 $(MKDLIB) $(DESTDIR)/$(LIBDIR)
/usr/bin/install -m 444 mkdio.h $(DESTDIR)/$(INCDIR)
install.everything: install install.samples install.man
install.samples: $(SAMPLE_PGMS) install
/usr/bin/install -s -m 755 $(SAMPLE_PGMS) $(DESTDIR)/$(BINDIR)
/home/raldi/reddit/r2/r2/lib/contrib/discount-1.6.0/config.md $(DESTDIR)/$(MANDIR)/man1
/usr/bin/install -m 444 theme.1 $(DESTDIR)/$(MANDIR)/man1
install.man:
/home/raldi/reddit/r2/r2/lib/contrib/discount-1.6.0/config.md $(DESTDIR)/$(MANDIR)/man3
/usr/bin/install -m 444 mkd-functions.3 markdown.3 mkd-line.3 $(DESTDIR)/$(MANDIR)/man3
for x in mkd_line mkd_generateline; do \
( echo '.\"' ; echo ".so man3/mkd-line.3" ) > $(DESTDIR)/$(MANDIR)/man3/$$x.3;\
done
for x in mkd_in mkd_string; do \
( echo '.\"' ; echo ".so man3/markdown.3" ) > $(DESTDIR)/$(MANDIR)/man3/$$x.3;\
done
for x in mkd_compile mkd_css mkd_generatecss mkd_generatehtml mkd_cleanup mkd_doc_title mkd_doc_author mkd_doc_date; do \
( echo '.\"' ; echo ".so man3/mkd-functions.3" ) > $(DESTDIR)/$(MANDIR)/man3/$$x.3; \
done
/home/raldi/reddit/r2/r2/lib/contrib/discount-1.6.0/config.md $(DESTDIR)/$(MANDIR)/man7
/usr/bin/install -m 444 markdown.7 mkd-extensions.7 $(DESTDIR)/$(MANDIR)/man7
/home/raldi/reddit/r2/r2/lib/contrib/discount-1.6.0/config.md $(DESTDIR)/$(MANDIR)/man1
/usr/bin/install -m 444 markdown.1 $(DESTDIR)/$(MANDIR)/man1
install.everything: install install.man
version.o: version.c VERSION
$(CC) -DVERSION=\"`cat VERSION`\" -c version.c
markdown: main.o $(MKDLIB)
$(CC) -o markdown main.o -lmarkdown
# example programs
theme: theme.o $(MKDLIB) mkdio.h
$(CC) -o theme theme.o -lmarkdown
mkd2html: mkd2html.o $(MKDLIB) mkdio.h
$(CC) -o mkd2html mkd2html.o -lmarkdown
makepage: makepage.c $(MKDLIB) mkdio.h
$(CC) -o makepage makepage.c -lmarkdown
main.o: main.c mkdio.h config.h
$(CC) -I. -c main.c
$(MKDLIB): $(OBJS)
$(AR) crv $(MKDLIB) $(OBJS)
$(RANLIB) $(MKDLIB)
test: $(PGMS) echo cols
@for x in tests/*.t; do \
sh $$x || exit 1; \
done
cols: tools/cols.c
$(CC) -o cols tools/cols.c
echo: tools/echo.c
$(CC) -o echo tools/echo.c
clean:
rm -f $(PGMS) $(SAMPLE_PGMS) *.o $(MKDLIB)
distclean spotless: clean
rm -f Makefile version.c markdown.1 config.cmd config.sub config.h config.mak config.log config.md
markdown.o: markdown.c config.h cstring.h markdown.h
generate.o: generate.c config.h cstring.h markdown.h
dumptree.o: dumptree.c cstring.h markdown.h
mkdio.o: mkdio.c mkdio.h cstring.h config.h
xmlpage.o: xmlpage.c mkdio.h cstring.h config.h
toc.o: toc.c mkdio.h cstring.h config.h

View File

@@ -0,0 +1,96 @@
CC=@CC@ -I. -L.
AR=@AR@
RANLIB=@RANLIB@
BINDIR=@exedir@
MANDIR=@mandir@
LIBDIR=@libdir@
INCDIR=@prefix@/include
PGMS=markdown
SAMPLE_PGMS=mkd2html makepage
@THEME@SAMPLE_PGMS+= theme
MKDLIB=libmarkdown.a
OBJS=mkdio.o markdown.o dumptree.o generate.o \
resource.o docheader.o version.o toc.o css.o \
xml.o Csio.o xmlpage.o @AMALLOC@
all: $(PGMS) $(SAMPLE_PGMS)
install: $(PGMS)
@INSTALL_PROGRAM@ $(PGMS) $(DESTDIR)/$(BINDIR)
@INSTALL_DATA@ $(MKDLIB) $(DESTDIR)/$(LIBDIR)
@INSTALL_DATA@ mkdio.h $(DESTDIR)/$(INCDIR)
install.everything: install install.samples install.man
install.samples: $(SAMPLE_PGMS) install
@INSTALL_PROGRAM@ $(SAMPLE_PGMS) $(DESTDIR)/$(BINDIR)
@INSTALL_DIR@ $(DESTDIR)/$(MANDIR)/man1
@INSTALL_DATA@ theme.1 $(DESTDIR)/$(MANDIR)/man1
install.man:
@INSTALL_DIR@ $(DESTDIR)/$(MANDIR)/man3
@INSTALL_DATA@ mkd-functions.3 markdown.3 mkd-line.3 $(DESTDIR)/$(MANDIR)/man3
for x in mkd_line mkd_generateline; do \
( echo '.\"' ; echo ".so man3/mkd-line.3" ) > $(DESTDIR)/$(MANDIR)/man3/$$x.3;\
done
for x in mkd_in mkd_string; do \
( echo '.\"' ; echo ".so man3/markdown.3" ) > $(DESTDIR)/$(MANDIR)/man3/$$x.3;\
done
for x in mkd_compile mkd_css mkd_generatecss mkd_generatehtml mkd_cleanup mkd_doc_title mkd_doc_author mkd_doc_date; do \
( echo '.\"' ; echo ".so man3/mkd-functions.3" ) > $(DESTDIR)/$(MANDIR)/man3/$$x.3; \
done
@INSTALL_DIR@ $(DESTDIR)/$(MANDIR)/man7
@INSTALL_DATA@ markdown.7 mkd-extensions.7 $(DESTDIR)/$(MANDIR)/man7
@INSTALL_DIR@ $(DESTDIR)/$(MANDIR)/man1
@INSTALL_DATA@ markdown.1 $(DESTDIR)/$(MANDIR)/man1
install.everything: install install.man
version.o: version.c VERSION
$(CC) -DVERSION=\"`cat VERSION`\" -c version.c
markdown: main.o $(MKDLIB)
$(CC) -o markdown main.o -lmarkdown @LIBS@
# example programs
@THEME@theme: theme.o $(MKDLIB) mkdio.h
@THEME@ $(CC) -o theme theme.o -lmarkdown @LIBS@
mkd2html: mkd2html.o $(MKDLIB) mkdio.h
$(CC) -o mkd2html mkd2html.o -lmarkdown @LIBS@
makepage: makepage.c $(MKDLIB) mkdio.h
$(CC) -o makepage makepage.c -lmarkdown @LIBS@
main.o: main.c mkdio.h config.h
$(CC) -I. -c main.c
$(MKDLIB): $(OBJS)
$(AR) crv $(MKDLIB) $(OBJS)
$(RANLIB) $(MKDLIB)
test: $(PGMS) echo cols
@for x in tests/*.t; do \
sh $$x || exit 1; \
done
cols: tools/cols.c
$(CC) -o cols tools/cols.c
echo: tools/echo.c
$(CC) -o echo tools/echo.c
clean:
rm -f $(PGMS) $(SAMPLE_PGMS) *.o $(MKDLIB)
distclean spotless: clean
rm -f @GENERATED_FILES@ @CONFIGURE_FILES@
markdown.o: markdown.c config.h cstring.h markdown.h
generate.o: generate.c config.h cstring.h markdown.h
dumptree.o: dumptree.c cstring.h markdown.h
mkdio.o: mkdio.c mkdio.h cstring.h config.h
xmlpage.o: xmlpage.c mkdio.h cstring.h config.h
toc.o: toc.c mkdio.h cstring.h config.h

View File

@@ -0,0 +1,40 @@
% Discount on Plan 9
% Josh Wood
% 2009-06-12
# *Discount* Markdown compiler on Plan 9
## Build
% CONFIG='--enable-all-features' mk config
% mk install
% markdown -V
markdown: discount X.Y.Z DL_TAG HEADER DEBUG SUPERSCRIPT RELAXED DIV
`--enable-all-features` may be replaced by zero or more of:
--enable-dl-tag Use the DL tag extension
--enable-pandoc-header Use pandoc-style header blocks
--enable-superscript A^B becomes A<sup>B</sup>
--enable-amalloc Enable memory allocation debugging
--relaxed-emphasis underscores aren't special in the middle of words
--with-tabstops=N Set tabstops to N characters (default is 4)
--enable-div Enable >%id% divisions
--enable-alpha-list Enable (a)/(b)/(c) lists
--enable-all-features Turn on all stable optional features
## Notes
The supplied mkfile merely drives Discount's own configure script and
then APE's *psh* environment to build the Discount source, then copies
the result(s) to locations appropriate for system-wide use on Plan 9.
There are a few other *mk*(1) targets:
`install.libs`: Discount includes a C library and header.
Installation is optional. Plan 9 binaries are statically linked.
`install.man`: Add manual pages for markdown(1) and (6).
`install.progs`: Extra programs. *makepage* writes complete XHTML
documents, rather than fragments. *mkd2html* is similar, but produces
HTML.

View File

@@ -0,0 +1,169 @@
.TH MARKDOWN 1
.SH NAME
markdown \- convert Markdown text to HTML
.SH SYNOPSIS
.B markdown
[
.B -dTV
]
[
.BI -b " url-base
]
[
.BI -F " bitmap
]
[
.BI -f " flags
]
[
.BI -o " ofile
]
[
.BI -s " text
]
[
.BI -t " text
]
[
.I file
]
.SH DESCRIPTION
The
.I markdown
utility reads the
.IR Markdown (6)-formatted
.I file
(or standard input) and writes its
.SM HTML
fragment representation on standard output.
.PP
The options are:
.TF dfdoptions
.TP
.BI -b " url-base
Links in source begining with
.B /
will be prefixed with
.I url-base
in the output.
.TP
.B -d
Instead of printing an
.SM HTML
fragment, print a parse tree.
.TP
.BI -F " bitmap
Set translation flags.
.I Bitmap
is a bit map of the various configuration options described in
.IR markdown (2).
.TP
.BI -f " flags
Set or clear various translation
.IR flags ,
described below.
.I Flags
are in a comma-delimited list, with an optional
.B +
(set) prefix on each flag.
.TP
.BI -o " ofile
Write the generated
.SM HTML
to
.IR ofile .
.TP
.BI -s " text
Use the
.IR markdown (2)
function to format the
.I text
on standard input.
.TP
.B -T
Under
.B -f
.BR toc ,
print the table of contents as an unordered list before the usual
.SM HTML
output.
.TP
.BI -t " text
Use
.IR mkd_text
(in
.IR markdown (2))
to format
.I text
instead of processing standard input with
.IR markdown .
.TP
.B -V
Show version number and configuration. If the version includes the string
.BR DL_TAG ,
.I markdown
was configured with definition list support. If the version includes the string
.BR HEADER ,
.I markdown
was configured to support pandoc header blocks.
.PD
.SS TRANSLATION FLAGS
The translation flags understood by
.B -f
are:
.TF \ noheader
.TP
.B noimage
Don't allow image tags.
.TP
.B nolinks
Don't allow links.
.TP
.B nohtml
Don't allow any embedded HTML.
.TP
.B cdata
Generate valid XML output.
.TP
.B noheader
Do not process pandoc headers.
.TP
.B notables
Do not process the syntax extension for tables.
.TP
.B tabstops
Use Markdown-standard 4-space tabstops.
.TP
.B strict
Disable superscript and relaxed emphasis.
.TP
.B relax
Enable superscript and relaxed emphasis (the default).
.TP
.B toc
Enable table of contents support, generated from headings (in
.IR markdown (6))
in the source.
.TP
.B 1.0
Revert to Markdown 1.0 compatibility.
.PD
.PP
For example,
.B -f nolinks,quot
tells
.I markdown
not to allow
.B <a>
tags, and to expand double quotes.
.SH SOURCE
.B /sys/src/cmd/discount
.SH SEE ALSO
.IR markdown (2),
.IR markdown (6)
.PP
http://daringfireball.net/projects/markdown/,
``Markdown''.
.SH DIAGNOSTICS
.I Markdown
exits 0 on success and >0 if an error occurs.

View File

@@ -0,0 +1,332 @@
.TH MARKDOWN 2
.SH NAME
mkd_in, mkd_string, markdown, mkd_compile, mkd_css, mkd_generatecss,
mkd_document, mkd_generatehtml, mkd_xhtmlpage, mkd_toc, mkd_generatetoc,
mkd_cleanup, mkd_doc_title, mkd_doc_author, mkd_doc_date, mkd_line,
mkd_generateline \- convert Markdown text to HTML
.SH SYNOPSIS
.ta \w'MMIOT* 'u
.B #include <mkdio.h>
.PP
.B
MMIOT* mkd_in(FILE *input, int flags)
.PP
.B
MMIOT* mkd_string(char *buf, int size, int flags)
.PP
.B
int markdown(MMIOT *doc, FILE *output, int flags)
.PP
.B
int mkd_compile(MMIOT *document, int flags)
.PP
.B
int mkd_css(MMIOT *document, char **doc)
.PP
.B
int mkd_generatecss(MMIOT *document, FILE *output)
.PP
.B
int mkd_document(MMIOT *document, char **doc)
.PP
.B
int mkd_generatehtml(MMIOT *document, FILE *output)
.PP
.B
int mkd_xhtmlpage(MMIOT *document, int flags, FILE *output)
.PP
.B
int mkd_toc(MMIOT *document, char **doc)
.PP
.B
int mkd_generatetoc(MMIOT *document, FILE *output)
.PP
.B
void mkd_cleanup(MMIOT*);
.PP
.B
char* mkd_doc_title(MMIOT*)
.PP
.B
char* mkd_doc_author(MMIOT*)
.PP
.B
char* mkd_doc_date(MMIOT*)
.PP
.B
int mkd_line(char *string, int size, char **doc, int flags)
.PP
.B
int mkd_generateline(char *string, int size, FILE *output, int flags)
.PD
.PP
.SH DESCRIPTION
These functions convert
.IR Markdown (6)
text into
.SM HTML
markup.
.PP
.I Mkd_in
reads the text referenced by pointer to
.B FILE
.I input
and returns a pointer to an
.B MMIOT
structure of the form expected by
.I markdown
and the other converters.
.I Mkd_string
accepts one
.I string
and returns a pointer to
.BR MMIOT .
.PP
After such preparation,
.I markdown
converts
.I doc
and writes the result to
.IR output ,
while
.I mkd_compile
transforms
.I document
in-place.
.PP
One or more of the following
.I flags
(combined with
.BR OR )
control
.IR markdown 's
processing of
.IR doc :
.TF MKD_NOIMAGE
.TP
.B MKD_NOIMAGE
Do not process
.B ![]
and remove
.B <img>
tags from the output.
.TP
.B MKD_NOLINKS
Do not process
.B []
and remove
.B <a>
tags from the output.
.TP
.B MKD_NOPANTS
Suppress Smartypants-style replacement of quotes, dashes, or ellipses.
.TP
.B MKD_STRICT
Disable superscript and relaxed emphasis processing if configured; otherwise a no-op.
.TP
.B MKD_TAGTEXT
Process as inside an
.SM HTML
tag: no
.BR <em> ,
no
.BR <bold> ,
no
.SM HTML
or
.B []
expansion.
.TP
.B MKD_NO_EXT
Don't process pseudo-protocols (in
.IR markdown (6)).
.TP
.B MKD_CDATA
Generate code for
.SM XML
.B ![CDATA[...]]
element.
.TP
.B MKD_NOHEADER
Don't process Pandoc-style headers.
.TP
.B MKD_TABSTOP
When reading documents, expand tabs to 4 spaces, overriding any compile-time configuration.
.TP
.B MKD_TOC
Label headings for use with the
.I mkd_generatetoc
and
.I mkd_toc
functions.
.TP
.B MKD_1_COMPAT
MarkdownTest_1.0 compatibility. Trim trailing spaces from first line of code blocks and disable implicit reference links (in
.IR markdown (6)).
.TP
.B MKD_AUTOLINK
Greedy
.SM URL
generation. When set, any
.SM URL
is converted to a hyperlink, even those not encased in
.BR <> .
.TP
.B MKD_SAFELINK
Don't make hyperlinks from
.B [][]
links that have unknown
.SM URL
protocol types.
.TP
.B MKD_NOTABLES
Do not process the syntax extension for tables (in
.IR markdown (6)).
.TP
.B MKD_EMBED
All of
.BR MKD_NOLINKS ,
.BR MKD_NOIMAGE ,
and
.BR MKD_TAGTEXT .
.PD
.PP
This implementation supports
Pandoc-style
headers and inline
.SM CSS
.B <style>
blocks, but
.I markdown
does not access the data provided by these extensions.
The following functions do, and allow other manipulations.
.PP
Given a pointer to
.B MMIOT
prepared by
.I mkd_in
or
.IR mkd_string ,
.I mkd_compile
compiles the
.I document
into
.BR <style> ,
Pandoc, and
.SM HTML
sections. It accepts the
.I flags
described for
.IR markdown ,
above.
.PP
Once compiled, the document particulars can be read and written:
.PP
.I Mkd_css
allocates a string and populates it with any
.B <style>
sections from the document.
.I Mkd_generatecss
writes any
.B <style>
sections to
.IR output .
.PP
.I Mkd_document
points
.I doc
to the
.B MMIOT
.IR document ,
returning
.IR document 's
size.
.PP
.I Mkd_generatehtml
writes the rest of the
.I document
to the
.IR output .
.PP
.IR Mkd_doc_title ,
.IR mkd_doc_author ,
and
.I mkd_doc_date
read the contents of any Pandoc header.
.PP
.I Mkd_xhtmlpage
writes an
.SM XHTML
page representation of the document.
It accepts the
.I flags
described for
.IR markdown ,
above.
.PP
.I Mkd_toc
.IR malloc s
a buffer into which it writes an outline, in the form of a
.B <ul>
element populated with
.BR <li> s
each containing a link to successive headings in the
.IR document .
It returns the size of that string.
.I Mkd_generatetoc
is similar,
but writes the outline to the
.I output
referenced by a pointer to
.BR FILE .
.PP
.I Mkd_cleanup
deletes a processed
.BR MMIOT .
.PP
The last two functions convert a single line of markdown source, for example a page title or a signature.
.I Mkd_line
allocates a buffer into which it writes an
.SM HTML
fragment representation of the
.IR string .
.I Mkd_generateline
writes the result to
.IR output .
.SH SOURCE
.B /sys/src/cmd/discount
.SH SEE ALSO
.IR markdown (1),
.IR markdown (6)
.SH DIAGNOSTICS
The
.I mkd_in
and
.I mkd_string
functions return a pointer to
.B MMIOT
on success, null on failure.
.IR Markdown ,
.IR mkd_compile ,
.IR mkd_style ,
and
.I mkd_generatehtml
return
.B 0
on success,
.B -1
otherwise.
.SH BUGS
Error handling is minimal at best.
.PP
The
.B MMIOT
created by
.I mkd_string
is deleted by the
.I markdown
function.
.PP
This is an
.SM APE
library.

View File

@@ -0,0 +1,543 @@
.TH MARKDOWN 6
.SH NAME
Markdown \- text formatting syntax
.SH DESCRIPTION
Markdown
is a text markup syntax for machine conversion to
the more complex
.SM HTML
or
.SM XHTML
markup languages.
It is intended to be easy to read and to write, with
emphasis on readability.
A Markdown-formatted document should be publishable as-is,
in plain text, without the formatting distracting the reader.
.PP
The biggest source of inspiration for Markdown's
syntax is the format of plain text email. The markup is comprised entirely
of punctuation characters, chosen so as to look like what they mean.
Asterisks around a word look like
.IR *emphasis* .
Markdown lists look like lists. Even
blockquotes look like quoted passages of text, assuming the reader has
used email.
.PP
.SS Block Elements
.TF W
.PD
.TP
Paragraphs and Line Breaks
A paragraph is one or more consecutive lines of text, separated
by one or more blank lines. (A blank line is any line that looks like a
blank line -- a line containing nothing but spaces or tabs is considered
blank.) Normal paragraphs should not be indented with spaces or tabs.
.IP
Lines may be freely broken for readability; Markdown
does not translate source line breaks to
.B <br />
tags. To request generation of
.B <br />
in the output, end a line with two or more spaces, then a newline.
.TP
Headings
Headings can be marked in two ways, called
.I setext
and
.IR atx .
.IP
Setext-style headings are
``underlined'' using equal signs (for first-level
headings) and dashes (for second-level headings).
.IP
Atx-style headings use 1-6 hash characters at the start of the line,
corresponding to
.SM HTML
.BR <h^(1-6)^> .
Optional closing hashes may follow
the heading text.
.TP
Blockquotes
Lines beginning with
.B >
are output in blockquotes.
Blockquotes can be nested
by multiple levels of
.BR >> .
Blockquotes can contain other Markdown elements, including
headings, lists, and code blocks.
.TP
Lists
Markdown supports ordered (numbered) and unordered (bulleted) lists.
List markers typically start at the left margin, but may be indented by
up to three spaces.
List markers must be followed by one or more spaces
or a tab, then the list item text.
A newline terminates each list item.
.IP
Unordered lists use asterisks, pluses, and hyphens interchangeably as
list markers.
.IP
Ordered lists use integers followed by periods as list markers.
The order of the integers is not interpreted,
but the list should begin with
.BR 1 .
.IP
If list items are separated by blank lines, Markdown will wrap each list
item in
.B <p>
tags in the
.SM HTML
output.
.IP
List items may consist of multiple paragraphs.
Each subsequent
paragraph within a list item must be indented by either 4 spaces
or one tab.
To put a blockquote within a list item, the blockquote's
.B >
marker needs to be indented.
To put a code block within a list item, the code block needs
to be indented
.I twice
-- 8 spaces or two tabs.
.TP
Code Blocks
To produce a code block, indent every line of the
block by at least 4 spaces or 1 tab.
A code block continues until it reaches a line that is not indented.
.IP
Rather than forming normal paragraphs, the lines
of a code block are interpreted literally.
Regular Markdown syntax is not processed within code blocks.
Markdown wraps a code block in both
.B <pre>
and
.B <code>
tags.
One level of indentation -- 4
spaces or 1 tab -- is removed from each line of the code block in
the output.
.TP
Horizontal Rules
Produce a horizontal rule tag
.RB ( <hr\ /> )
by placing three or
more hyphens, asterisks, or underscores on a line by themselves.
.SS Span Elements
.TF W
.PD
.TP
Links
Markdown supports two styles of links:
.I inline
and
.IR reference .
In both styles, the link text is delimited by square brackets
.RB ( [] ).
To create an inline link, use a set of regular parentheses immediately
after the link text's closing square bracket.
Inside the parentheses,
put the link URL, along with an optional
title for the link surrounded in double quotes.
For example:
.IP
.EX
An [example](http://example.com/ "Title") inline link.
.EE
.IP
Reference-style links use a second set of square brackets, inside
which you place a label of your choosing to identify the link:
.IP
.EX
An [example][id] reference-style link.
.EE
.IP
The label is then assigned a value on its own line, anywhere in the document:
.IP
.EX
[id]: http://example.com/ "Optional Title"
.EE
.IP
Link label names may consist of letters, numbers, spaces, and
punctuation.
Labels are not case sensitive.
An empty label bracket
set after a reference-style link implies the link label is equivalent to
the link text.
A URL value can then be assigned to the link by referencing
the link text as the label name.
.TP
Emphasis
Markdown treats asterisks
.RB ( * )
and underscores
.RB ( _ )
as indicators of emphasis.
Text surrounded with single asterisks or underscores
will be wrapped with an
.SM HTML
.B <em>
tag.
Double asterisks or underscores generate an
.SM HTML
.B <strong>
tag.
.TP
Code
To indicate a span of code, wrap it with backtick quotes
.RB ( ` ).
Unlike a code block, a code span indicates code within a
normal paragraph.
To include a literal backtick character within a code span, you can use
multiple backticks as the opening and closing delimiters:
.IP
.EX
``There is a literal backtick (`) here.``
.EE
.TP
Images
Markdown image syntax is intended to resemble that
for links, allowing for two styles, once again
.I inline
and
.IR reference .
The syntax is as for each respective style of link, described above, but
prefixed with an exclamation mark character
.RB ( ! ).
Inline image syntax looks like this:
.IP
.EX
![Alt text](/path/to/img.jpg "Optional title")
.EE
.IP
That is:
An exclamation mark;
followed by a set of square brackets containing the `alt'
attribute text for the image;
followed by a set of parentheses containing the URL or path to
the image, and an optional `title' attribute enclosed in double
or single quotes.
.IP
Reference-style image syntax looks like this:
.IP
.EX
![Alt text][id]
.EE
.IP
Where
.I id
is a label used as for reference-style URL links, described above.
.SS Convenience
.TF W
.PD
.TP
Automatic Links
There is a shortcut style for creating ``automatic''
links for URLs and email addresses.
Surround the URL
or address with angle brackets.
.TP
Backslash Escapes
Use backslash escapes to generate literal
characters which would otherwise have special meaning in Markdown's
formatting syntax.
.TP
Inline HTML
For markup that is not covered by Markdown's
syntax, simply use the
.SM HTML
directly.
The only restrictions are that block-level
.SM HTML
elements --
.BR <div> ,
.BR <table> ,
.BR <pre> ,
.BR <p> ,
etc. -- must be separated from surrounding
content by blank lines, and the start and end tags of the block should
not be indented with tabs or spaces. Markdown formatting syntax is
not processed within block-level
.SM HTML
tags.
.IP
Span-level
.SM HTML
tags -- e.g.
.BR <span> ,
.BR <cite> ,
or
.B <del>
-- can be
used anywhere in a Markdown
paragraph, list item, or heading.
It is permitted to use
.SM HTML
tags instead of Markdown formatting; e.g.
.SM HTML
.B <a>
or
.B <img>
tags instead of Markdown's
link or image syntax.
Unlike block-level
.SM HTML
tags, Markdown
syntax
.I is
processed within the elements of span-level tags.
.TP
Automatic Special Character Escapes
To be displayed literally in a user agent, the characters
.B <
and
.B &
must appear as escaped entities in
.SM HTML
source, e.g.
.B &lt;
and
.BR &amp; .
Markdown
allows natural use of these characters, taking care of
the necessary escaping.
The ampersand part of a directly-used
.SM HTML
entity remains unchanged; otherwise it will be translated
into
.BR &amp; .
Inside code spans and blocks, angle brackets and
ampersands are always encoded automatically.
This makes it easy to use Markdown to write about
.SM HTML
code.
.PP
.SS Smarty Pants
The
.IR markdown (1)
utility transforms a few plain text symbols into their typographically-fancier
.SM HTML
entity equivalents.
These are extensions to the standard Markdown syntax.
.TF W
.PD
.TP
Punctuation
Input single- and double-quotes are transformed
into ``curly'' quote entities in the output (e.g.,
.B 'text'
becomes
.BR &lsquo;text&rsquo; ).
Input double-dashes
.RB ( -- )
and triple-dashes become en- and em-dashes, respectively,
while a series of three dots
.RB ( ... )
in the input becomes an ellipsis entity
.RB ( &hellip; )
in the
.SM HTML
output.
.TP
Symbols
Three other transformations replace the common plain-text shorthands
.BR (c) ,
.BR (r) ,
and
.BR (tm)
from the input with their respective
.SM HTML
entities. (As in
.B (c)
becoming
.BR &copy; ,
the Copyright symbol entity.)
.TP
Fractions
A small set of plain-text shorthands for fractions is recognized.
.B 1/4
becomes
.BR &frac14; ,
for example. These fraction notations are replaced with their
.SM HTML
entity equivalents:
.BR 1/4 ,
.BR 1/2 ,
.BR 3/4 .
.B 1/4th
and
.B 3/4ths
are replaced with their entity and the indicated ordinal suffix letters.
.PP
Like the basic Markdown syntax, none of the ``Smarty Pants'' extensions are processed
inside code blocks or spans.
.PP
.SS Discount Extensions
.IR Markdown (1)
recognizes some extensions to the Markdown format,
many of them adopted or adapted from other Markdown
interpreters or document formatting systems.
.TF W
.PD
.TP
Pandoc Headers
If
.I markdown
was configured with
.BR --enable-pandoc-header ,
the markdown source can have a 3-line Pandoc header in the format of
.IP
.EX
% Title
% Author
% Date
.EE
.IP
whose data is available to the
.IR mkd_doc_title ,
.IR mkd_doc_author ,
and
.I mkd_doc_date
(in
.IR markdown (2))
functions.
.TP
Embedded Stylesheets
Stylesheets may be defined and modified in a
.B <style>
block. A style block is parsed like any other block-level
.SM HTML;
.B <style>
starting on column 1, raw
.SM HTML
(or, in this case,
.SM CSS \)
following it, and either ending with a
.B </style>
at the end of the line or at the beginning of a subsequent line.
.IP
Style blocks apply to the entire document regardless of where they are defined.
.TP
Image Dimensions
Image specification has been extended with an argument describing image dimensions:
.BI = height x width.
For an image 400 pixels high and 300 wide, the new syntax is:
.IP
.EX
![Alt text](/path/to/image.jpg =400x300 "Title")
.EE
.TP
Pseudo-Protocols
Pseudo-protocols that may replace the common
.B http:
or
.B mailto:
have been added to the link syntax described above.
.IP
.BR abbr :
Text following is used as the
.B title
attribute of an
.B abbr
tag wrapping the link text. So
.B [LT](abbr:Link Text)
gives
.B <abbr title="Link Text">LT</abbr>.
.IP
.BR id :
The link text is marked up and written to the output, wrapped with
.B <a id=text following>
and
.BR </a> .
.IP
.BR class :
The link text is marked up and written to the output, wrapped with
.B <span class=text following>
and
.BR </span> .
.IP
.BR raw :
Text following is written to the output with no further processing.
The link text is discarded.
.TP
Alphabetic Lists
If
.I markdown
was configured with
.BR --enable-alpha-list ,
.IP
.EX
a. this
b. is
c. an alphabetic
d. list
.EE
.IP
yields an
.SM HTML
.B ol
ordered list.
.TP
Definition Lists
If configured with
.BR --enable-dl-tag ,
markup for definition lists is enabled. A definition list item is defined as
.IP
.EX
=term=
definition
.EE
.TP
Tables
Tables are specified with a pipe
.RB ( | )
and dash
.RB ( - )
marking. The markdown text
.IP
.EX
header0|header1
-------|-------
textA|textB
textC|textD
.EE
.IP
will produce an
.SM HTML
.B table
of two columns and three rows.
A header row is designated by ``underlining'' with dashes.
Declare a column's alignment by affixing a colon
.RB ( : )
to the left or right end of the dashes underlining its header.
In the output, this
yields the corresponding value for the
.B align
attribute on each
.B td
cell in the column.
A colon at both ends of a column's header dashes indicates center alignment.
.TP
Relaxed Emphasis
If configured with
.BR --relaxed-emphasis ,
the rules for emphasis are changed so that a single
.B _
will not count as an emphasis character in the middle of a word.
This is useful for documenting some code where
.B _
appears frequently, and would normally require a backslash escape.
.PD
.SH SEE ALSO
.IR markdown (1),
.IR markdown (2)
.PP
http://daringfireball.net/projects/markdown/syntax/,
``Markdown: Syntax''.
.PP
http://daringfireball.net/projects/smartypants/,
``Smarty Pants''.
.PP
http://michelf.com/projects/php-markdown/extra/#table,
``PHP Markdown Extra: Tables''.

View File

@@ -0,0 +1,37 @@
BIN=/$objtype/bin
CC='cc -D_BSD_EXTENSION'
markdown:
ape/psh -c 'cd .. && make'
none:V: markdown
test: markdown
ape/psh -c 'cd ..&& make test'
install: markdown
cp ../markdown $BIN/markdown
install.progs: install
cp ../makepage $BIN/makepage
cp ../mkd2html $BIN/mkd2html
install.libs: install
cp ../mkdio.h /sys/include/ape/mkdio.h
cp ../libmarkdown.a /$objtype/lib/ape/libmarkdown.a
install.man: install
cp markdown.1 /sys/man/1/markdown
cp markdown.2 /sys/man/2/markdown
cp markdown.6 /sys/man/6/markdown
installall:V: install.libs install.man install.progs
config:
ape/psh -c 'cd .. && ./configure.sh $CONFIG'
clean:
ape/psh -c 'cd .. && make clean'
nuke:
ape/psh -c 'cd .. && make distclean'

View File

@@ -0,0 +1,16 @@
DISCOUNT is a implementation of John Gruber's Markdown markup
language. It implements, as far as I can tell, all of the
language as described in
<http://daringfireball.net/projects/markdown/syntax>
and passes the Markdown test suite at
<http://daringfireball.net/projects/downloads/MarkdownTest_1.0.zip>
DISCOUNT is free software written by David Parsons <orc@pell.chi.il.us>;
it is released under a BSD-style license that allows you to do
as you wish with it as long as you don't attempt to claim it as
your own work.
Most of the programs included in the DISCOUNT distribution have
manual pages describing how they work.
The file INSTALL describes how to build and install discount

View File

@@ -0,0 +1 @@
1.6.1

View File

@@ -0,0 +1,111 @@
/*
* debugging malloc()/realloc()/calloc()/free() that attempts
* to keep track of just what's been allocated today.
*/
#include <stdio.h>
#include <stdlib.h>
#define MAGIC 0x1f2e3d4c
struct alist { int magic, size; struct alist *next, *last; };
static struct alist list = { 0, 0, 0, 0 };
static int mallocs=0;
static int reallocs=0;
static int frees=0;
void *
acalloc(int size, int count)
{
struct alist *ret = calloc(size + sizeof(struct alist), count);
if ( ret ) {
ret->magic = MAGIC;
ret->size = size * count;
if ( list.next ) {
ret->next = list.next;
ret->last = &list;
ret->next->last = ret;
list.next = ret;
}
else {
ret->last = ret->next = &list;
list.next = list.last = ret;
}
++mallocs;
return ret+1;
}
return 0;
}
void*
amalloc(int size)
{
return acalloc(size,1);
}
void
afree(void *ptr)
{
struct alist *p2 = ((struct alist*)ptr)-1;
if ( p2->magic == MAGIC ) {
p2->last->next = p2->next;
p2->next->last = p2->last;
++frees;
free(p2);
}
else
free(ptr);
}
void *
arealloc(void *ptr, int size)
{
struct alist *p2 = ((struct alist*)ptr)-1;
struct alist save;
if ( p2->magic == MAGIC ) {
save.next = p2->next;
save.last = p2->last;
p2 = realloc(p2, sizeof(*p2) + size);
if ( p2 ) {
p2->size = size;
p2->next->last = p2;
p2->last->next = p2;
++reallocs;
return p2+1;
}
else {
save.next->last = save.last;
save.last->next = save.next;
return 0;
}
}
return realloc(ptr, size);
}
void
adump()
{
struct alist *p;
for ( p = list.next; p && (p != &list); p = p->next ) {
fprintf(stderr, "allocated: %d byte%s\n", p->size, (p->size==1) ? "" : "s");
fprintf(stderr, " [%.*s]\n", p->size, p+1);
}
if ( getenv("AMALLOC_STATISTICS") ) {
fprintf(stderr, "%d malloc%s\n", mallocs, (mallocs==1)?"":"s");
fprintf(stderr, "%d realloc%s\n", reallocs, (reallocs==1)?"":"s");
fprintf(stderr, "%d free%s\n", frees, (frees==1)?"":"s");
}
}

View File

@@ -0,0 +1,29 @@
/*
* debugging malloc()/realloc()/calloc()/free() that attempts
* to keep track of just what's been allocated today.
*/
#ifndef AMALLOC_D
#define AMALLOC_D
#include "config.h"
#ifdef USE_AMALLOC
extern void *amalloc(int);
extern void *acalloc(int,int);
extern void *arealloc(void*,int);
extern void afree(void*);
extern void adump();
#define malloc amalloc
#define calloc acalloc
#define realloc arealloc
#define free afree
#else
#define adump() (void)1
#endif
#endif/*AMALLOC_D*/

View File

@@ -0,0 +1,2 @@
#! /bin/sh
./configure.sh

View File

@@ -0,0 +1,29 @@
/*
* configuration for markdown, generated Fri Jan 29 13:52:23 PST 2010
* by raldi@zork
*/
#ifndef __AC_MARKDOWN_D
#define __AC_MARKDOWN_D 1
#define OS_LINUX 1
#define DWORD unsigned long
#define WORD unsigned short
#define BYTE unsigned char
#define HAVE_BASENAME 1
#define HAVE_LIBGEN_H 1
#define HAVE_PWD_H 1
#define HAVE_GETPWUID 1
#define HAVE_SRANDOM 1
#define INITRNG(x) srandom((unsigned int)x)
#define HAVE_BZERO 1
#define HAVE_RANDOM 1
#define COINTOSS() (random()&1)
#define HAVE_STRCASECMP 1
#define HAVE_STRNCASECMP 1
#define HAVE_FCHDIR 1
#define TABSTOP 4
#define HAVE_MALLOC_H 1
#define PATH_SED "/bin/sed"
#endif/* __AC_MARKDOWN_D */

View File

@@ -0,0 +1,31 @@
[echo -n] works
Configuring for [markdown]
Looking for cpp
CPP=[/lib/cpp], CPPFLAGS=[]
looking for install
(/usr/bin/install)
checking out the C compiler
checking for "volatile" keyword
checking for "const" keyword
defining WORD & DWORD scalar types
/tmp/pd18169.c: In function 'main':
/tmp/pd18169.c:13: warning: incompatible implicit declaration of built-in function 'exit'
/tmp/ngc18169.c: In function 'main':
/tmp/ngc18169.c:5: warning: initialization makes pointer from integer without a cast
/tmp/ngc18169.c:6: warning: initialization makes pointer from integer without a cast
looking for header libgen.h
looking for header pwd.h
looking for the getpwuid function
looking for the srandom function
looking for the bzero function
/tmp/ngc18169.c: In function 'main':
/tmp/ngc18169.c:4: warning: incompatible implicit declaration of built-in function 'bzero'
looking for the random function
looking for the strcasecmp function
looking for the strncasecmp function
looking for the fchdir function
looking for header malloc.h
sed is /bin/sed
generating Makefile
generating version.c
generating markdown.1

View File

@@ -0,0 +1 @@
HAVE_SED = 1

View File

@@ -0,0 +1,5 @@
#! /bin/sh
# script generated Fri Jan 29 13:52:24 PST 2010 by configure.sh
test -d "$1" || mkdir -p "$1"
exit 0

View File

@@ -0,0 +1,27 @@
s;@CPP@;/lib/cpp;g
s;@CPPFLAGS@;;g
s;@INSTALL@;/usr/bin/install;g
s;@INSTALL_PROGRAM@;/usr/bin/install -s -m 755;g
s;@INSTALL_DATA@;/usr/bin/install -m 444;g
s;@INSTALL_DIR@;/home/raldi/reddit/r2/r2/lib/contrib/discount-1.6.0/config.md;g
s;@CC@;cc;g
s;@AR@;/usr/bin/ar;g
s;@RANLIB@;/usr/bin/ranlib;g
s;@THEME@;;g
s;@TABSTOP@;4;g
s;@AMALLOC@;;g
s;@STRICT@;.\";g
s;@LIBS@;;g
s;@CONFIGURE_FILES@;config.cmd config.sub config.h config.mak config.log config.md;g
s;@GENERATED_FILES@;Makefile version.c markdown.1;g
s;@CFLAGS@;-g;g
s;@LDFLAGS@;-g;g
s;@srcdir@;/home/raldi/reddit/r2/r2/lib/contrib/discount-1.6.0;g
s;@prefix@;/usr/local;g
s;@exedir@;/usr/local/bin;g
s;@sbindir@;/usr/local/sbin;g
s;@libdir@;/usr/local/lib;g
s;@libexec@;/usr/local/lib;g
s;@confdir@;/etc;g
s;@mandir@;/usr/local/man;g
s;@SED@;/bin/sed;g

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
#! /bin/sh
# local options: ac_help is the help message that describes them
# and LOCAL_AC_OPTIONS is the script that interprets them. LOCAL_AC_OPTIONS
# is a script that's processed with eval, so you need to be very careful to
# make certain that what you quote is what you want to quote.
# load in the configuration file
#
ac_help='--enable-dl-tag Use the DL tag extension
--enable-pandoc-header Use pandoc-style header blocks
--enable-superscript A^B becomes A<sup>B</sup>
--enable-amalloc Enable memory allocation debugging
--relaxed-emphasis underscores aren'\''t special in the middle of words
--with-tabstops=N Set tabstops to N characters (default is 4)
--enable-div Enable >%id% divisions
--enable-alpha-list Enable (a)/(b)/(c) lists
--enable-all-features Turn on all stable optional features'
LOCAL_AC_OPTIONS='
set=`locals $*`;
if [ "$set" ]; then
eval $set
shift 1
else
ac_error=T;
fi'
locals() {
K=`echo $1 | $AC_UPPERCASE`
case "$K" in
--RELAXED-EMPHAS*)
echo RELAXED_EMPHASIS=T
;;
--ENABLE-ALL|--ENABLE-ALL-FEATURES)
echo WITH_DL_TAG=T
echo RELAXED_EMPHASIS=T
echo WITH_PANDOC_HEADER=T
echo WITH_SUPERSCRIPT=T
echo WITH_AMALLOC=T
echo WITH_DIV=T
#echo WITH_ALPHA_LIST=T
;;
--ENABLE-*) enable=`echo $K | sed -e 's/--ENABLE-//' | tr '-' '_'`
echo WITH_${enable}=T ;;
esac
}
TARGET=markdown
. ./configure.inc
AC_INIT $TARGET
AC_PROG_CC
case "$AC_CC $AC_CFLAGS" in
*-Wall*) AC_DEFINE 'while(x)' 'while( (x) != 0 )'
AC_DEFINE 'if(x)' 'if( (x) != 0 )' ;;
esac
AC_PROG ar || AC_FAIL "$TARGET requires ar"
AC_PROG ranlib
AC_C_VOLATILE
AC_C_CONST
AC_SCALAR_TYPES
AC_CHECK_BASENAME
AC_CHECK_HEADERS sys/types.h pwd.h && AC_CHECK_FUNCS getpwuid
if AC_CHECK_FUNCS srandom; then
AC_DEFINE 'INITRNG(x)' 'srandom((unsigned int)x)'
elif AC_CHECK_FUNCS srand; then
AC_DEFINE 'INITRNG(x)' 'srand((unsigned int)x)'
else
AC_DEFINE 'INITRNG(x)' '(void)1'
fi
if AC_CHECK_FUNCS 'bzero((char*)0,0)'; then
: # Yay
elif AC_CHECK_FUNCS 'memset((char*)0,0,0)'; then
AC_DEFINE 'bzero(p,s)' 'memset(p,s,0)'
else
AC_FAIL "$TARGET requires bzero or memset"
fi
if AC_CHECK_FUNCS random; then
AC_DEFINE 'COINTOSS()' '(random()&1)'
elif AC_CHECK_FUNCS rand; then
AC_DEFINE 'COINTOSS()' '(rand()&1)'
else
AC_DEFINE 'COINTOSS()' '1'
fi
if AC_CHECK_FUNCS strcasecmp; then
:
elif AC_CHECK_FUNCS stricmp; then
AC_DEFINE strcasecmp stricmp
else
AC_FAIL "$TARGET requires either strcasecmp() or stricmp()"
fi
if AC_CHECK_FUNCS strncasecmp; then
:
elif AC_CHECK_FUNCS strnicmp; then
AC_DEFINE strncasecmp strnicmp
else
AC_FAIL "$TARGET requires either strncasecmp() or strnicmp()"
fi
if AC_CHECK_FUNCS fchdir || AC_CHECK_FUNCS getcwd ; then
AC_SUB 'THEME' ''
else
AC_SUB 'THEME' '#'
fi
if [ -z "$WITH_TABSTOPS" ]; then
TABSTOP=4
elif [ "$WITH_TABSTOPS" -eq 1 ]; then
TABSTOP=8
else
TABSTOP=$WITH_TABSTOPS
fi
AC_DEFINE 'TABSTOP' $TABSTOP
AC_SUB 'TABSTOP' $TABSTOP
test -z "$WITH_SUPERSCRIPT" || AC_DEFINE 'SUPERSCRIPT' 1
test -z "$RELAXED_EMPHASIS" || AC_DEFINE 'RELAXED_EMPHASIS' 1
test -z "$WITH_DIV" || AC_DEFINE 'DIV_QUOTE' 1
test -z "$WITH_ALPHA_LIST" || AC_DEFINE 'ALPHA_LIST' 1
if [ "$WITH_AMALLOC" ]; then
AC_DEFINE 'USE_AMALLOC' 1
AC_SUB 'AMALLOC' 'amalloc.o'
else
AC_SUB 'AMALLOC' ''
fi
if [ "$RELAXED_EMPHASIS" -o "$WITH_SUPERSCRIPT" ]; then
AC_SUB 'STRICT' ''
else
AC_SUB 'STRICT' '.\"'
fi
[ "$OS_FREEBSD" -o "$OS_DRAGONFLY" ] || AC_CHECK_HEADERS malloc.h
[ "$WITH_DL_TAG" ] && AC_DEFINE 'DL_TAG_EXTENSION' '1'
[ "$WITH_PANDOC_HEADER" ] && AC_DEFINE 'PANDOC_HEADER' '1'
AC_OUTPUT Makefile version.c markdown.1

View File

@@ -0,0 +1,76 @@
/* markdown: a C implementation of John Gruber's Markdown markup language.
*
* Copyright (C) 2009 David L Parsons.
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <time.h>
#include <ctype.h>
#include "config.h"
#include "cstring.h"
#include "markdown.h"
#include "amalloc.h"
/*
* dump out stylesheet sections.
*/
static void
stylesheets(Paragraph *p, Cstring *f)
{
Line* q;
for ( ; p ; p = p->next ) {
if ( p->typ == STYLE ) {
for ( q = p->text; q ; q = q->next )
Cswrite(f, T(q->text), S(q->text));
Csputc('\n', f);
}
if ( p->down )
stylesheets(p->down, f);
}
}
/* dump any embedded styles to a string
*/
int
mkd_css(Document *d, char **res)
{
Cstring f;
if ( res && *res && d && d->compiled ) {
CREATE(f);
RESERVE(f, 100);
stylesheets(d->code, &f);
/* HACK ALERT! HACK ALERT! HACK ALERT! */
*res = T(f); /* we know that a T(Cstring) is a character pointer */
/* so we can simply pick it up and carry it away, */
return S(f); /* leaving the husk of the Ctring on the stack */
/* END HACK ALERT */
}
return EOF;
}
/* dump any embedded styles to a file
*/
int
mkd_generatecss(Document *d, FILE *f)
{
char *res;
int written = EOF, size = mkd_css(d, &res);
if ( size > 0 )
written = fwrite(res, size, 1, f);
if ( res )
free(res);
return (written == size) ? size : EOF;
}

View File

@@ -0,0 +1,75 @@
/* two template types: STRING(t) which defines a pascal-style string
* of element (t) [STRING(char) is the closest to the pascal string],
* and ANCHOR(t) which defines a baseplate that a linked list can be
* built up from. [The linked list /must/ contain a ->next pointer
* for linking the list together with.]
*/
#ifndef _CSTRING_D
#define _CSTRING_D
#include <string.h>
#include <stdlib.h>
#include "amalloc.h"
/* expandable Pascal-style string.
*/
#define STRING(type) struct { type *text; int size, alloc; }
#define CREATE(x) T(x) = (void*)(S(x) = (x).alloc = 0)
#define EXPAND(x) (S(x)++)[(S(x) < (x).alloc) \
? (T(x)) \
: (T(x) = T(x) ? realloc(T(x), sizeof T(x)[0] * ((x).alloc += 100)) \
: malloc(sizeof T(x)[0] * ((x).alloc += 100)) )]
#define DELETE(x) ALLOCATED(x) ? (free(T(x)), S(x) = (x).alloc = 0) \
: ( S(x) = 0 )
#define CLIP(t,i,sz) \
( ((i) >= 0) && ((sz) > 0) && (((i)+(sz)) <= S(t)) ) ? \
(memmove(&T(t)[i], &T(t)[i+sz], (S(t)-(i+sz)+1)*sizeof(T(t)[0])), \
S(t) -= (sz)) : -1
#define RESERVE(x, sz) T(x) = ((x).alloc > S(x) + (sz) \
? T(x) \
: T(x) \
? realloc(T(x), sizeof T(x)[0] * ((x).alloc = 100+(sz)+S(x))) \
: malloc(sizeof T(x)[0] * ((x).alloc = 100+(sz)+S(x))))
#define SUFFIX(t,p,sz) \
memcpy(((S(t) += (sz)) - (sz)) + \
(T(t) = T(t) ? realloc(T(t), sizeof T(t)[0] * ((t).alloc += sz)) \
: malloc(sizeof T(t)[0] * ((t).alloc += sz))), \
(p), sizeof(T(t)[0])*(sz))
#define PREFIX(t,p,sz) \
RESERVE( (t), (sz) ); \
if ( S(t) ) { memmove(T(t)+(sz), T(t), S(t)); } \
memcpy( T(t), (p), (sz) ); \
S(t) += (sz)
/* reference-style links (and images) are stored in an array
*/
#define T(x) (x).text
#define S(x) (x).size
#define ALLOCATED(x) (x).alloc
/* abstract anchor type that defines a list base
* with a function that attaches an element to
* the end of the list.
*
* the list base field is named .text so that the T()
* macro will work with it.
*/
#define ANCHOR(t) struct { t *text, *end; }
#define E(t) ((t).end)
#define ATTACH(t, p) ( T(t) ? ( (E(t)->next = (p)), (E(t) = (p)) ) \
: ( (T(t) = E(t) = (p)) ) )
typedef STRING(char) Cstring;
extern void Csputc(int, Cstring *);
extern int Csprintf(Cstring *, char *, ...);
extern int Cswrite(Cstring *, char *, int);
extern void Csreparse(Cstring *, char *, int, int);
#endif/*_CSTRING_D*/

View File

@@ -0,0 +1,43 @@
/*
* docheader -- get values from the document header
*
* Copyright (C) 2007 David L Parsons.
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include "cstring.h"
#include "markdown.h"
#include "amalloc.h"
#define afterdle(t) (T((t)->text) + (t)->dle)
char *
mkd_doc_title(Document *doc)
{
if ( doc && doc->headers )
return afterdle(doc->headers);
return 0;
}
char *
mkd_doc_author(Document *doc)
{
if ( doc && doc->headers && doc->headers->next )
return afterdle(doc->headers->next);
return 0;
}
char *
mkd_doc_date(Document *doc)
{
if ( doc && doc->headers && doc->headers->next && doc->headers->next->next )
return afterdle(doc->headers->next->next);
return 0;
}

View File

@@ -0,0 +1,151 @@
/* markdown: a C implementation of John Gruber's Markdown markup language.
*
* Copyright (C) 2007 David L Parsons.
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
#include <stdio.h>
#include "markdown.h"
#include "cstring.h"
#include "amalloc.h"
struct frame {
int indent;
char c;
};
typedef STRING(struct frame) Stack;
static char *
Pptype(int typ)
{
switch (typ) {
case WHITESPACE: return "whitespace";
case CODE : return "code";
case QUOTE : return "quote";
case MARKUP : return "markup";
case HTML : return "html";
case DL : return "dl";
case UL : return "ul";
case OL : return "ol";
case LISTITEM : return "item";
case HDR : return "header";
case HR : return "hr";
case TABLE : return "table";
case SOURCE : return "source";
default : return "mystery node!";
}
}
static void
pushpfx(int indent, char c, Stack *sp)
{
struct frame *q = &EXPAND(*sp);
q->indent = indent;
q->c = c;
}
static void
poppfx(Stack *sp)
{
S(*sp)--;
}
static void
changepfx(Stack *sp, char c)
{
char ch;
if ( !S(*sp) ) return;
ch = T(*sp)[S(*sp)-1].c;
if ( ch == '+' || ch == '|' )
T(*sp)[S(*sp)-1].c = c;
}
static void
printpfx(Stack *sp, FILE *f)
{
int i;
char c;
if ( !S(*sp) ) return;
c = T(*sp)[S(*sp)-1].c;
if ( c == '+' || c == '-' ) {
fprintf(f, "--%c", c);
T(*sp)[S(*sp)-1].c = (c == '-') ? ' ' : '|';
}
else
for ( i=0; i < S(*sp); i++ ) {
if ( i )
fprintf(f, " ");
fprintf(f, "%*s%c", T(*sp)[i].indent + 2, " ", T(*sp)[i].c);
if ( T(*sp)[i].c == '`' )
T(*sp)[i].c = ' ';
}
fprintf(f, "--");
}
static void
dumptree(Paragraph *pp, Stack *sp, FILE *f)
{
int count;
Line *p;
int d;
static char *Begin[] = { 0, "P", "center" };
while ( pp ) {
if ( !pp->next )
changepfx(sp, '`');
printpfx(sp, f);
d = fprintf(f, "[%s", Pptype(pp->typ));
if ( pp->ident )
d += fprintf(f, " %s", pp->ident);
if ( pp->align )
d += fprintf(f, ", <%s>", Begin[pp->align]);
for (count=0, p=pp->text; p; ++count, (p = p->next) )
;
if ( count )
d += fprintf(f, ", %d line%s", count, (count==1)?"":"s");
d += fprintf(f, "]");
if ( pp->down ) {
pushpfx(d, pp->down->next ? '+' : '-', sp);
dumptree(pp->down, sp, f);
poppfx(sp);
}
else fputc('\n', f);
pp = pp->next;
}
}
int
mkd_dump(Document *doc, FILE *out, int flags, char *title)
{
Stack stack;
if (mkd_compile(doc, flags) ) {
CREATE(stack);
pushpfx(fprintf(out, "%s", title), doc->code->next ? '+' : '-', &stack);
dumptree(doc->code, &stack, out);
DELETE(stack);
mkd_cleanup(doc);
return 0;
}
return -1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
/*
* markdown: convert a single markdown document into html
*/
/*
* Copyright (C) 2007 David L Parsons.
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <unistd.h>
#include <mkdio.h>
#include <errno.h>
#include <string.h>
#include "config.h"
#include "amalloc.h"
#if HAVE_LIBGEN_H
#include <libgen.h>
#endif
#ifndef HAVE_BASENAME
#include <string.h>
char*
basename(char *p)
{
char *ret = strrchr(p, '/');
return ret ? (1+ret) : p;
}
#endif
char *pgm = "markdown";
static struct {
char *name;
int off;
int flag;
} opts[] = {
{ "tabstop", 0, MKD_TABSTOP },
{ "image", 1, MKD_NOIMAGE },
{ "links", 1, MKD_NOLINKS },
{ "relax", 1, MKD_STRICT },
{ "strict", 0, MKD_STRICT },
{ "tables", 1, MKD_NOTABLES },
{ "header", 1, MKD_NOHEADER },
{ "html", 1, MKD_NOHTML },
{ "ext", 1, MKD_NO_EXT },
{ "cdata", 0, MKD_CDATA },
{ "pants", 1, MKD_NOPANTS },
{ "smarty", 1, MKD_NOPANTS },
{ "toc", 0, MKD_TOC },
{ "autolink",0, MKD_AUTOLINK },
{ "safelink",0, MKD_SAFELINK },
{ "1.0", 0, MKD_1_COMPAT },
} ;
#define NR(x) (sizeof x / sizeof x[0])
void
set(int *flags, char *optionstring)
{
int i;
int enable;
char *arg;
for ( arg = strtok(optionstring, ","); arg; arg = strtok(NULL, ",") ) {
if ( *arg == '+' || *arg == '-' )
enable = (*arg++ == '+') ? 1 : 0;
else if ( strncasecmp(arg, "no", 2) == 0 ) {
arg += 2;
enable = 0;
}
else
enable = 1;
for ( i=0; i < NR(opts); i++ )
if ( strcasecmp(arg, opts[i].name) == 0 )
break;
if ( i < NR(opts) ) {
if ( opts[i].off )
enable = !enable;
if ( enable )
*flags |= opts[i].flag;
else
*flags &= ~opts[i].flag;
}
else
fprintf(stderr, "%s: unknown option <%s>\n", pgm, arg);
}
}
char *
e_flags(char *text, int size, char *context)
{
return context;
}
float
main(int argc, char **argv)
{
int opt;
int rc;
int flags = 0;
int debug = 0;
int toc = 0;
int use_mkd_line = 0;
char *urlflags = 0;
char *text = 0;
char *ofile = 0;
char *urlbase = 0;
char *q;
MMIOT *doc;
if ( q = getenv("MARKDOWN_FLAGS") )
flags = strtol(q, 0, 0);
pgm = basename(argv[0]);
opterr = 1;
while ( (opt=getopt(argc, argv, "b:df:F:o:s:t:TVZ:")) != EOF ) {
switch (opt) {
case 'b': urlbase = optarg;
break;
case 'd': debug = 1;
break;
case 'V': printf("%s: discount %s\n", pgm, markdown_version);
exit(0);
case 'F': flags = strtol(optarg, 0, 0);
break;
case 'f': set(&flags, optarg);
break;
case 't': text = optarg;
use_mkd_line = 1;
break;
case 'T': toc = 1;
break;
case 's': text = optarg;
break;
case 'o': if ( ofile ) {
fprintf(stderr, "Too many -o options\n");
exit(1);
}
if ( !freopen(ofile = optarg, "w", stdout) ) {
perror(ofile);
exit(1);
}
break;
case 'Z': urlflags = optarg;
break;
default: fprintf(stderr, "usage: %s [-dTV] [-b url-base]"
" [-F bitmap] [-f {+-}flags]"
" [-o ofile] [-s text]"
" [-t text] [file]\n", pgm);
exit(1);
}
}
argc -= optind;
argv += optind;
if ( use_mkd_line )
rc = mkd_generateline( text, strlen(text), stdout, flags);
else {
if ( text ) {
if ( (doc = mkd_string(text, strlen(text), flags)) == 0 ) {
perror(text);
exit(1);
}
}
else {
if ( argc && !freopen(argv[0], "r", stdin) ) {
perror(argv[0]);
exit(1);
}
if ( (doc = mkd_in(stdin,flags)) == 0 ) {
perror(argc ? argv[0] : "stdin");
exit(1);
}
}
if ( urlbase )
mkd_basename(doc, urlbase);
if ( debug )
rc = mkd_dump(doc, stdout, 0, argc ? basename(argv[0]) : "stdin");
else {
rc = 1;
if ( mkd_compile(doc, flags) ) {
if ( urlflags ) {
mkd_e_context(doc, urlflags);
mkd_e_flags(doc, e_flags);
}
rc = 0;
if ( toc )
mkd_generatetoc(doc, stdout);
mkd_generatehtml(doc, stdout);
mkd_cleanup(doc);
}
}
}
adump();
exit( (rc == 0) ? 0 : errno );
}

View File

@@ -0,0 +1,27 @@
/*
* makepage: Use mkd_xhtmlpage() to convert markdown input to a
* fully-formed xhtml page.
*/
#include <stdio.h>
#include <stdlib.h>
#include <mkdio.h>
float
main(argc, argv)
int argc;
char **argv;
{
MMIOT *doc;
if ( (argc > 1) && !freopen(argv[1], "r", stdin) ) {
perror(argv[1]);
exit(1);
}
if ( (doc = mkd_in(stdin, 0)) == 0 ) {
perror( (argc > 1) ? argv[1] : "stdin" );
exit(1);
}
exit(mkd_xhtmlpage(doc, 0, stdout));
}

View File

@@ -0,0 +1,128 @@
.\" %A%
.\"
.Dd January 7, 2008
.Dt MARKDOWN 1
.Os MASTODON
.Sh NAME
.Nm markdown
.Nd text to html conversion tool
.Sh SYNOPSIS
.Nm
.Op Fl d
.Op Fl T
.Op Fl V
.Op Fl b Ar url-base
.Op Fl F Pa bitmap
.Op Fl f Ar flags
.Op Fl o Pa file
.Op Fl s Pa text
.Op Fl t Pa text
.Op Pa textfile
.Sh DESCRIPTION
The
.Nm
utility reads the
.Xr markdown 7 Ns -formatted
.Pa textfile
.Pq or stdin if not specified,
compiles it, and writes the html output
to stdout.
.Pp
The options are as follows:
.Bl -tag -width "-o file"
.It Fl b Ar url-base
Links in source begining with / will be prefixed with
.Ar url-base
in the output.
.It Fl d
Instead of writing the html file, dump a parse
tree to stdout.
.It Fl f Ar flags
Set or clear various translation flags. The flags
are in a comma-delimited list, with an optional
.Ar +
(set) prefix on each flag.
.Bl -tag -width "NOHEADER"
.It Ar noimage
Don't allow image tags.
.It Ar nolinks
Don't allow links.
.It Ar nohtml
Don't allow
.B any
embedded html.
.It Ar cdata
Generate valid XML output.
.It Ar noheader
Do not process pandoc headers.
.It Ar notables
Do not process Markdown Extra-style tables.
.It Ar tabstops
Use markdown-standard 4-space tabstops.
.".It Ar strict
."Disable superscript and relaxed emphasis.
.".It Ar relax
."Enable superscript and relaxed emphasis (this is the default.)
.It Ar toc
Enable table-of-contents support
.It Ar 1.0
Revert to Markdown 1.0 compatability.
.El
.Pp
As an example, the option
.Fl f Ar nolinks,quot
tells
.Nm
to not allow \<a tags, and to expand
double-quotes.
.It Fl F Ar bitmap
Set translation flags.
.Ar Bitmap
is a bit map of the various configuration options
described in
.Xr markdown 3
(the flag values are defined in
.Pa mkdio.h )
.It Fl V
Show the version# and configuration data.
.Pp
If the version includes the string
.Em DL_TAG ,
.Nm
was configured with definition list support.
.Pp
If the version includes the string
.Em HEADER ,
.Nm
was configured to support pandoc header blocks.
.It Fl o Pa file
Write the generated html to
.Pa file .
.It Fl t Ar text
Use
.Xr mkd_text 3
to format
.Ar text
instead of processing stdin with the
.Xr markdown 3
function.
.It Fl T
If run with the table-of-content flag on, dump the
table of contents before the formatted text.
.It Fl s Ar text
Use the
.Xr markdown 3
function to format
.Ar text .
.El
.Sh RETURN VALUES
The
.Nm
utility exits 0 on success, and >0 if an error occurs.
.Sh SEE ALSO
.Xr markdown 3 ,
.Xr markdown 7 ,
.Xr mkd-extensions 7 .
.Sh AUTHOR
.An David Parsons
.Pq Li orc@pell.chi.il.us

View File

@@ -0,0 +1,128 @@
.\" %A%
.\"
.Dd January 7, 2008
.Dt MARKDOWN 1
.Os MASTODON
.Sh NAME
.Nm markdown
.Nd text to html conversion tool
.Sh SYNOPSIS
.Nm
.Op Fl d
.Op Fl T
.Op Fl V
.Op Fl b Ar url-base
.Op Fl F Pa bitmap
.Op Fl f Ar flags
.Op Fl o Pa file
.Op Fl s Pa text
.Op Fl t Pa text
.Op Pa textfile
.Sh DESCRIPTION
The
.Nm
utility reads the
.Xr markdown 7 Ns -formatted
.Pa textfile
.Pq or stdin if not specified,
compiles it, and writes the html output
to stdout.
.Pp
The options are as follows:
.Bl -tag -width "-o file"
.It Fl b Ar url-base
Links in source begining with / will be prefixed with
.Ar url-base
in the output.
.It Fl d
Instead of writing the html file, dump a parse
tree to stdout.
.It Fl f Ar flags
Set or clear various translation flags. The flags
are in a comma-delimited list, with an optional
.Ar +
(set) prefix on each flag.
.Bl -tag -width "NOHEADER"
.It Ar noimage
Don't allow image tags.
.It Ar nolinks
Don't allow links.
.It Ar nohtml
Don't allow
.B any
embedded html.
.It Ar cdata
Generate valid XML output.
.It Ar noheader
Do not process pandoc headers.
.It Ar notables
Do not process Markdown Extra-style tables.
.It Ar tabstops
Use markdown-standard 4-space tabstops.
@STRICT@.It Ar strict
@STRICT@Disable superscript and relaxed emphasis.
@STRICT@.It Ar relax
@STRICT@Enable superscript and relaxed emphasis (this is the default.)
.It Ar toc
Enable table-of-contents support
.It Ar 1.0
Revert to Markdown 1.0 compatability.
.El
.Pp
As an example, the option
.Fl f Ar nolinks,quot
tells
.Nm
to not allow \<a tags, and to expand
double-quotes.
.It Fl F Ar bitmap
Set translation flags.
.Ar Bitmap
is a bit map of the various configuration options
described in
.Xr markdown 3
(the flag values are defined in
.Pa mkdio.h )
.It Fl V
Show the version# and configuration data.
.Pp
If the version includes the string
.Em DL_TAG ,
.Nm
was configured with definition list support.
.Pp
If the version includes the string
.Em HEADER ,
.Nm
was configured to support pandoc header blocks.
.It Fl o Pa file
Write the generated html to
.Pa file .
.It Fl t Ar text
Use
.Xr mkd_text 3
to format
.Ar text
instead of processing stdin with the
.Xr markdown 3
function.
.It Fl T
If run with the table-of-content flag on, dump the
table of contents before the formatted text.
.It Fl s Ar text
Use the
.Xr markdown 3
function to format
.Ar text .
.El
.Sh RETURN VALUES
The
.Nm
utility exits 0 on success, and >0 if an error occurs.
.Sh SEE ALSO
.Xr markdown 3 ,
.Xr markdown 7 ,
.Xr mkd-extensions 7 .
.Sh AUTHOR
.An David Parsons
.Pq Li orc@pell.chi.il.us

View File

@@ -0,0 +1,123 @@
.\"
.Dd December 20, 2007
.Dt MARKDOWN 3
.Os Mastodon
.Sh NAME
.Nm markdown
.Nd process Markdown documents
.Sh LIBRARY
Markdown
.Pq libmarkdown , -lmarkdown
.Sh SYNOPSIS
.Fd #include <mkdio.h>
.Ft MMIOT
.Fn *mkd_in "FILE *input" "int flags"
.Ft MMIOT
.Fn *mkd_string "char *string" "int size" "int flags"
.Ft int
.Fn markdown "MMIOT *doc" "FILE *output" "int flags"
.Sh DESCRIPTION
These functions
convert
.Em Markdown
documents and strings into HTML.
.Fn markdown
processes an entire document, while
.Fn mkd_text
processes a single string.
.Pp
To process a file, you pass a FILE* to
.Fn mkd_in ,
and if it returns a nonzero value you pass that in to
.Fn markdown ,
which then writes the converted document to the specified
.Em FILE* .
If your input has already been written into a string (generated
input or a file opened
with
.Xr mmap 2 )
you can feed that string to
.Fn mkd_string
and pass its return value to
.Fn markdown.
.Pp
.Fn Markdown
accepts the following flag values (or-ed together if needed)
to restrict how it processes input:
.Bl -tag -width MKD_SAFELINK -compact
.It Ar MKD_NOIMAGE
Do not process `![]' and
remove
.Em \<img\>
tags from the output.
.It Ar MKD_NOLINKS
Do not process `[]' and remove
.Em \<a\>
tags from the output.
.It Ar MKD_NOPANTS
Do not do Smartypants-style mangling of quotes, dashes, or ellipses.
.It Ar MKD_STRICT
Disable superscript and relaxed emphasis processing (if they are configured;
otherwise it's a no-op.)
.\" .It Ar MKD_QUOT
.\" Expand
.\" .Ar \&"
.\" to \&&quot;.
.It Ar MKD_NOHEADER
Do not attempt to parse any Pandoc-style headers.
.It Ar MKD_TABSTOP
When reading documents, expand tabs to
.Em 4
spaces instead of whatever
.Nm
was originally configured for.
.It Ar MKD_TOC
Label all headers for use with the
.Fn mkd_generatetoc
and
.Fn mkd_toc
functions.
.It Ar MKD_1_COMPAT
MarkdownTest_1.0 compatability flag; trim trailing spaces from the
first line of code blocks and disable implicit reference links.
.It Ar MKD_AUTOLINK
Greedily urlify links -- if
.Em MKD_AUTOLINK
is set, urls will be converted into hyperlinks even if they
aren't encased in
.Em <> .
.It Ar MKD_SAFELINK
Don't make hyperlinks from
.Em [][]
links that have unknown url types.
.It Ar MKD_NOTABLES
Don't process tables.
.El
.Sh RETURN VALUES
.Fn markdown
returns 0 on success, 1 on failure.
The
.Fn mkd_in
and
.Fn mkd_string
functions return a MMIOT* on success, null on failure.
.Sh SEE ALSO
.Xr markdown 1 ,
.Xr mkd-functions 3 ,
.Xr mkd-line 3 ,
.Xr markdown 7 ,
.Xr mkd-extensions 7 ,
.Xr mmap 2 .
.Pp
http://daringfireball.net/projects/markdown/syntax
.Sh BUGS
Error handling is minimal at best.
.Pp
The
.Ar MMIOT
created by
.Fn mkd_string
is deleted by the
.Nm
function.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,151 @@
#ifndef _MARKDOWN_D
#define _MARKDOWN_D
#include "cstring.h"
/* reference-style links (and images) are stored in an array
* of footnotes.
*/
typedef struct footnote {
Cstring tag; /* the tag for the reference link */
Cstring link; /* what this footnote points to */
Cstring title; /* what it's called (TITLE= attribute) */
int height, width; /* dimensions (for image link) */
int dealloc; /* deallocation needed? */
} Footnote;
/* each input line is read into a Line, which contains the line,
* the offset of the first non-space character [this assumes
* that all tabs will be expanded to spaces!], and a pointer to
* the next line.
*/
typedef struct line {
Cstring text;
struct line *next;
int dle;
} Line;
/* a paragraph is a collection of Lines, with links to the next paragraph
* and (if it's a QUOTE, UL, or OL) to the reparsed contents of this
* paragraph.
*/
typedef struct paragraph {
struct paragraph *next; /* next paragraph */
struct paragraph *down; /* recompiled contents of this paragraph */
struct line *text; /* all the text in this paragraph */
char *ident; /* %id% tag for QUOTE */
enum { WHITESPACE=0, CODE, QUOTE, MARKUP,
HTML, STYLE, DL, UL, OL, AL, LISTITEM,
HDR, HR, TABLE, SOURCE } typ;
enum { IMPLICIT=0, PARA, CENTER} align;
int hnumber; /* <Hn> for typ == HDR */
} Paragraph;
enum { ETX, SETEXT }; /* header types */
typedef struct block {
enum { bTEXT, bSTAR, bUNDER } b_type;
int b_count;
char b_char;
Cstring b_text;
Cstring b_post;
} block;
typedef STRING(block) Qblock;
/* a magic markdown io thing holds all the data structures needed to
* do the backend processing of a markdown document
*/
typedef char* (*e_func)(const char*, const int, void*);
typedef struct mmiot {
Cstring out;
Cstring in;
Qblock Q;
int isp;
STRING(Footnote) *footnotes;
int flags;
#define DENY_A 0x0001
#define DENY_IMG 0x0002
#define DENY_SMARTY 0x0004
#define DENY_HTML 0x0008
#define STRICT 0x0010
#define INSIDE_TAG 0x0020
#define NO_PSEUDO_PROTO 0x0040
#define CDATA_OUTPUT 0x0080
#define NOTABLES 0x0400
#define TOC 0x1000
#define MKD_1_COMPAT 0x2000
#define AUTOLINK 0x4000
#define SAFELINK 0x8000
#define USER_FLAGS 0xFCFF
#define EMBEDDED DENY_A|DENY_IMG|NO_PSEUDO_PROTO|CDATA_OUTPUT
char *base;
void *e_context;
e_func e_url, e_flags;
void (*e_free)(void*,void*);
} MMIOT;
/*
* the mkdio text input functions return a document structure,
* which contains a header (retrieved from the document if
* markdown was configured * with the * --enable-pandoc-header
* and the document begins with a pandoc-style header) and the
* root of the linked list of Lines.
*/
typedef struct document {
Line *headers; /* title -> author(s) -> date */
ANCHOR(Line) content; /* uncompiled text, not valid after compile() */
Paragraph *code; /* intermediate code generated by compile() */
int compiled; /* set after mkd_compile() */
int html; /* set after (internal) htmlify() */
int tabstop; /* for properly expanding tabs (ick) */
MMIOT *ctx; /* backend buffers, flags, and structures */
char *base; /* url basename for url fragments */
} Document;
extern int mkd_firstnonblank(Line *);
extern int mkd_compile(Document *, int);
extern int mkd_document(Document *, char **);
extern int mkd_generatehtml(Document *, FILE *);
extern int mkd_css(Document *, char **);
extern int mkd_generatecss(Document *, FILE *);
#define mkd_style mkd_generatecss
extern int mkd_xml(char *, int , char **);
extern int mkd_generatexml(char *, int, FILE *);
extern void mkd_cleanup(Document *);
extern int mkd_line(char *, int, char **, int);
extern int mkd_generateline(char *, int, FILE*, int);
#define mkd_text mkd_generateline
extern void mkd_basename(Document*, char *);
extern void mkd_string_to_anchor(char*,int, void(*)(int,void*), void*);
extern Document *mkd_in(FILE *, int);
extern Document *mkd_string(char*,int, int);
#define NO_HEADER 0x0100
#define STD_TABSTOP 0x0200
#define INPUT_MASK (NO_HEADER|STD_TABSTOP)
/* internal resource handling functions.
*/
extern void ___mkd_freeLine(Line *);
extern void ___mkd_freeLines(Line *);
extern void ___mkd_freeParagraph(Paragraph *);
extern void ___mkd_freefootnote(Footnote *);
extern void ___mkd_freefootnotes(MMIOT *);
extern void ___mkd_initmmiot(MMIOT *, void *);
extern void ___mkd_freemmiot(MMIOT *, void *);
extern void ___mkd_freeLineRange(Line *, Line *);
extern void ___mkd_xml(char *, int, FILE *);
extern void ___mkd_reparse(char *, int, int, MMIOT*);
extern void ___mkd_emblock(MMIOT*);
extern void ___mkd_tidy(Cstring *);
#endif/*_MARKDOWN_D*/

View File

@@ -0,0 +1,178 @@
.\"
.Dd Dec 22, 2007
.Dt MKD-EXTENSIONS 7
.Os MASTODON
.Sh NAME
.Nm mkd-extensions
.Nd Extensions to the Markdown text formatting syntax
.Sh DESCRIPTION
This version of markdown has been extended in a few ways by
extending existing markup, creating new markup from scratch,
and borrowing markup from other markup languages.
.Ss Image dimensions
Markdown embedded images have been extended to allow specifying
the dimensions of the image by adding a new argument
.Em =/height/x/width/
to the link description.
.Pp
The new image syntax is
.nf
![alt text](image =/height/x/width/ "title")
.fi
.Ss pseudo-protocols
Three pseudo-protocols have been added to links
.Bl -tag -width XXXXX
.It Ar id:
The
.Ar "alt text"
is marked up and written to the output, wrapped with
.Em "<a id=id>"
and
.Em "</a>" .
.It Ar class:
The
.Ar "alt text"
is marked up and written to the output, wrapped with
.Em "<span class=class>"
and
.Em "</span>" .
.It Ar raw:
The
.Ar title
is written
.Em -- with no further processing --
to the output. The
.Ar "alt text"
is discarded.
.It Ar abbr:
The
.Ar "alt text"
is marked up and written to the output, wrapped with
.Em "<abbr title="abbr">
and
.Em "</abbr>" .
.El
.Ss Pandoc headers
If markdown was configured with
.Ar --enable-pandoc-header ,
the markdown source document can have a 3-line
.Xr Pandoc
header in the format of
.nf
% title
% author(s)
% date
.fi
which will be made available to the
.Fn mkd_doc_title ,
.Fn mkd_doc_author ,
and
.Fn mkd_doc_date
functions.
.Ss Definition lists
If markdown was configured with
.Ar --enable-dl-tag ,
markup for definition lists is enabled. A definition list item
is defined as
.nf
=tag=
description
.fi
(that is a
.Ar = ,
followed by text, another
.Ar = ,
a newline, 4 spaces of intent, and then more text.)
.Pp
.Ss embedded stylesheets
Stylesheets may be defined and modified in a
.Em <style>
block. A style block is parsed like any other
block level html;
.Em <style>
starting on column 1, raw html (or, in this case, css) following
it, and either ending with a
.Em </style>
at the end of the line or a
.Em </style>
at the beginning of a subsequent line.
.Pp
Be warned that style blocks work like footnote links -- no matter
where you define them they are valid for the entire document.
.Ss relaxed emphasis
If markdown was configured with
.Ar --relaxed-emphasis ,
the rules for emphasis are changed so that a single
.Ar _
will
.Em not
count as a emphasis character if it's in the middle of a word.
This is primarily for documenting code, if you don't wish to
have to backquote all code references.
.Ss alpha lists
If markdown was configured with
.Ar --enable-alpha-list ,
alphabetic lists (like regular numeric lists, but with alphabetic
items) are supported. So:
.nf
a. this
b. is
c. an alphabetic
d. list
.fi
will produce:
.nf
<ol type=a>
<li>this</li>
<li>is</li>
<li>an alphabetic</li>
<li>list</li>
</ol>
.fi
.Ss tables
.Ar PHP Markdown Extra -style
tables are supported; input of the form
.nf
header|header
------|------
text | text
.fi
will produce:
.nf
<table>
<thead>
<tr>
<th>header</th>
<th>header</th>
</tr>
</thead>
<tbody>
<tr>
<td>text</td>
<td>text</td>
</tr>
</tbody>
</table>
.fi
The dashed line can also contain
.Em :
characters for formatting; if a
.Em :
is at the start of a column, it tells
.Nm discount
to align the cell contents to the left; if it's at the end, it
aligns right, and if there's one at the start and at the
end, it centers.
.Sh AUTHOR
David Parsons
.%T http://www.pell.portland.or.us/~orc/
.Sh SEE ALSO
.Xr markdown 1 ,
.Xr markdown 3 ,
.Xr mkd-functions 3 ,
.Xr mkd-line 3 ,
.Xr mkd-extensions 7 .
.Pp
.%T http://daringfireball.net/projects/markdown
.Pp
.%T http://michelf.com/projects/php-markdown

View File

@@ -0,0 +1,180 @@
.\"
.Dd January 18, 2008
.Dt MKD_FUNCTIONS 3
.Os Mastodon
.Sh NAME
.Nm mkd_functions
.Nd access and process Markdown documents.
.Sh LIBRARY
Markdown
.Pq libmarkdown , -lmarkdown
.Sh SYNOPSIS
.Fd #include <mkdio.h>
.Ft int
.Fn mkd_compile "MMIOT *document" "int flags"
.Ft int
.Fn mkd_css "MMIOT *document" "char **doc"
.Ft int
.Fn mkd_generatecss "MMIOT *document" "FILE *output"
.Ft int
.Fn mkd_document "MMIOT *document" "char **doc"
.Ft int
.Fn mkd_generatehtml "MMIOT *document" "FILE *output"
.Ft int
.Fn mkd_xhtmlpage "MMIOT *document" "int flags" "FILE *output"
.Ft int
.Fn mkd_toc "MMIOT *document" "char **doc"
.Ft void
.Fn mkd_generatetoc "MMIOT *document" "FILE *output"
.Ft void
.Fn mkd_cleanup "MMIOT*"
.Ft char*
.Fn mkd_doc_title "MMIOT*"
.Ft char*
.Fn mkd_doc_author "MMIOT*"
.Ft char*
.Fn mkd_doc_date "MMIOT*"
.Sh DESCRIPTION
.Pp
The
.Nm markdown
format supported in this implementation includes
Pandoc-style header and inline
.Ar \<style\>
blocks, and the standard
.Xr markdown 3
functions do not provide access to
the data provided by either of those extensions.
These functions give you access to that data, plus
they provide a finer-grained way of converting
.Em Markdown
documents into HTML.
.Pp
Given a
.Ar MMIOT*
generated by
.Fn mkd_in
or
.Fn mkd_string ,
.Fn mkd_compile
compiles the document into
.Em \<style\> ,
.Em Pandoc ,
and
.Em html
sections.
.Pp
Once compiled, the document can be examined and written
by the
.Fn mkd_css ,
.Fn mkd_document ,
.Fn mkd_generatecss ,
.Fn mkd_generatehtml ,
.Fn mkd_generatetoc ,
.Fn mkd_toc ,
.Fn mkd_xhtmlpage ,
.Fn mkd_doc_title ,
.Fn mkd_doc_author ,
and
.Fn mkd_doc_date
functions.
.Pp
.Fn mkd_css
allocates a string and populates it with any \<style\> sections
provided in the document,
.Fn mkd_generatecss
writes any \<style\> sections to the output,
.Fn mkd_document
points
.Ar text
to the text of the document and returns the
size of the document,
.Fn mkd_generatehtml
writes the rest of the document to the output,
and
.Fn mkd_doc_title ,
.Fn mkd_doc_author ,
.Fn mkd_doc_date
are used to read the contents of a Pandoc header,
if any.
.Pp
.Fn mkd_xhtmlpage
writes a xhtml page containing the document. The regular set of
flags can be passed.
.Pp
.Fn mkd_toc
writes a document outline, in the form of a collection of nested
lists with links to each header in the document, into a string
allocated with
.Fn malloc ,
and returns the size.
.Pp
.Fn mkd_generatetoc
is like
.Fn mkd_toc ,
except that it writes the document outline to the given
.Pa FILE*
argument.
.Pp
.Fn mkd_cleanup
deletes a
.Ar MMIOT*
after processing is done.
.Pp
.Fn mkd_compile
accepts the same flags that
.Fn markdown
and
.Fn mkd_string
do;
.Bl -tag -width MKD_NOIMAGE -compact
.It Ar MKD_NOIMAGE
Do not process `![]' and
remove
.Em \<img\>
tags from the output.
.It Ar MKD_NOLINKS
Do not process `[]' and remove
.Em \<a\>
tags from the output.
.It Ar MKD_NOPANTS
Do not do Smartypants-style mangling of quotes, dashes, or ellipses.
.It Ar MKD_TAGTEXT
Process the input as if you were inside a html tag. This means that
no html tags will be generated, and
.Fn mkd_compile
will attempt to escape anything that might terribly confuse a
web browser.
.It Ar MKD_NO_EXT
Do not process any markdown pseudo-protocols when
handing
.Ar [][]
links.
.It Ar MKD_NOHEADER
Do not attempt to parse any Pandoc-style headers.
.It Ar MKD_TOC
Label all headers for use with the
.Fn mkd_generatetoc
function.
.It Ar MKD_1_COMPAT
MarkdownTest_1.0 compatability flag; trim trailing spaces from the
first line of code blocks and disable implicit reference links.
.El
.Sh RETURN VALUES
The functions
.Fn mkd_compile ,
.Fn mkd_style ,
and
.Fn mkd_generatehtml
return 0 on success, -1 on failure.
.Sh SEE ALSO
.Xr markdown 1 ,
.Xr markdown 3 ,
.Xr mkd-line 3 ,
.Xr markdown 7 ,
.Xr mkd-extensions 7 ,
.Xr mmap 2 .
.Pp
http://daringfireball.net/projects/markdown/syntax
.Sh BUGS
Error handling is minimal at best.

View File

@@ -0,0 +1,41 @@
.\"
.Dd January 18, 2008
.Dt MKD_LINE 3
.Os Mastodon
.Sh NAME
.Nm mkd_line
.Nd do Markdown translation of small items
.Sh LIBRARY
Markdown
.Pq libmarkdown , -lmarkdown
.Sh SYNOPSIS
.Fd #include <mkdio.h>
.Ft int
.Fn mkd_line "char *string" "int size" "char **doc" "int flags"
.Ft int
.Fn mkd_generateline "char *string" "int size" "FILE *output" "int flags"
.Sh DESCRIPTION
.Pp
Occasionally one might want to do markdown translations on fragments of
data, like the title of an weblog article, a date, or a simple signature
line.
.Nm mkd_line
and
.Nm mkd_generateline
allow you to do markdown translations on small blocks of text.
.Nm mkd_line
allocates a buffer, then writes the translated text into that buffer,
and
.Nm mkd_generateline
writes the output to the specified
.Ar FILE* .
.Sh SEE ALSO
.Xr markdown 1 ,
.Xr markdown 3 ,
.Xr markdown 7 ,
.Xr mkd-extensions 7 ,
.Xr mmap 2 .
.Pp
http://daringfireball.net/projects/markdown/syntax
.Sh BUGS
Error handling is minimal at best.

View File

@@ -0,0 +1,185 @@
/*
* mkd2html: parse a markdown input file and generate a web page.
*
* usage: mkd2html [options] filename
* or mkd2html [options] < markdown > html
*
* options
* -css css-file
* -header line-to-add-to-<HEADER>
* -footer line-to-add-before-</BODY>
*
* example:
*
* mkd2html -cs /~orc/pages.css syntax
* ( read syntax OR syntax.text, write syntax.html )
*/
/*
* Copyright (C) 2007 David L Parsons.
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_BASENAME
# ifdef HAVE_LIBGEN_H
# include <libgen.h>
# else
# include <unistd.h>
# endif
#endif
#include <stdarg.h>
#include "mkdio.h"
#include "cstring.h"
#include "amalloc.h"
char *pgm = "mkd2html";
#ifndef HAVE_BASENAME
char *
basename(char *path)
{
char *p;
if (( p = strrchr(path, '/') ))
return 1+p;
return path;
}
#endif
void
fail(char *why, ...)
{
va_list ptr;
va_start(ptr,why);
fprintf(stderr, "%s: ", pgm);
vfprintf(stderr, why, ptr);
fputc('\n', stderr);
va_end(ptr);
exit(1);
}
void
main(argc, argv)
char **argv;
{
char *h;
char *source = 0, *dest = 0;
MMIOT *mmiot;
int i;
FILE *input, *output;
STRING(char*) css, headers, footers;
CREATE(css);
CREATE(headers);
CREATE(footers);
pgm = basename(argv[0]);
while ( argc > 2 ) {
if ( strcmp(argv[1], "-css") == 0 ) {
EXPAND(css) = argv[2];
argc -= 2;
argv += 2;
}
else if ( strcmp(argv[1], "-header") == 0 ) {
EXPAND(headers) = argv[2];
argc -= 2;
argv += 2;
}
else if ( strcmp(argv[1], "-footer") == 0 ) {
EXPAND(footers) = argv[2];
argc -= 2;
argv += 2;
}
}
if ( argc > 1 ) {
char *p, *dot;
source = malloc(strlen(argv[1]) + 6);
dest = malloc(strlen(argv[1]) + 6);
if ( !(source && dest) )
fail("out of memory allocating name buffers");
strcpy(source, argv[1]);
if (( p = strrchr(source, '/') ))
p = source;
else
++p;
if ( (input = fopen(source, "r")) == 0 ) {
strcat(source, ".text");
if ( (input = fopen(source, "r")) == 0 )
fail("can't open either %s or %s", argv[1], source);
}
strcpy(dest, source);
if (( dot = strrchr(dest, '.') ))
*dot = 0;
strcat(dest, ".html");
if ( (output = fopen(dest, "w")) == 0 )
fail("can't write to %s", dest);
}
else {
input = stdin;
output = stdout;
}
if ( (mmiot = mkd_in(input, 0)) == 0 )
fail("can't read %s", source ? source : "stdin");
if ( !mkd_compile(mmiot, 0) )
fail("couldn't compile input");
h = mkd_doc_title(mmiot);
/* print a header */
fprintf(output,
"<!doctype html public \"-//W3C//DTD HTML 4.0 Transitional //EN\">\n"
"<html>\n"
"<head>\n"
" <meta name=\"GENERATOR\" content=\"mkd2html %s\">\n", markdown_version);
fprintf(output," <meta http-equiv=\"Content-Type\"\n"
" content=\"text/html; charset-us-ascii\">");
for ( i=0; i < S(css); i++ )
fprintf(output, " <link rel=\"stylesheet\"\n"
" type=\"text/css\"\n"
" href=\"%s\" />\n", T(css)[i]);
if ( h ) {
fprintf(output," <title>");
mkd_generateline(h, strlen(h), output, 0);
fprintf(output, "</title>\n");
}
for ( i=0; i < S(headers); i++ )
fprintf(output, " %s\n", T(headers)[i]);
fprintf(output, "</head>\n"
"<body>\n");
/* print the compiled body */
mkd_generatehtml(mmiot, output);
for ( i=0; i < S(footers); i++ )
fprintf(output, "%s\n", T(footers)[i]);
fprintf(output, "</body>\n"
"</html>\n");
mkd_cleanup(mmiot);
exit(0);
}

View File

@@ -0,0 +1,339 @@
/*
* mkdio -- markdown front end input functions
*
* Copyright (C) 2007 David L Parsons.
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include "cstring.h"
#include "markdown.h"
#include "amalloc.h"
typedef ANCHOR(Line) LineAnchor;
/* create a new blank Document
*/
static Document*
new_Document()
{
Document *ret = calloc(sizeof(Document), 1);
if ( ret ) {
if (( ret->ctx = calloc(sizeof(MMIOT), 1) ))
return ret;
free(ret);
}
return 0;
}
/* add a line to the markdown input chain
*/
static void
queue(Document* a, Cstring *line)
{
Line *p = calloc(sizeof *p, 1);
unsigned char c;
int xp = 0;
int size = S(*line);
unsigned char *str = (unsigned char*)T(*line);
CREATE(p->text);
ATTACH(a->content, p);
while ( size-- ) {
if ( (c = *str++) == '\t' ) {
/* expand tabs into ->tabstop spaces. We use ->tabstop
* because the ENTIRE FREAKING COMPUTER WORLD uses editors
* that don't do ^T/^D, but instead use tabs for indentation,
* and, of course, set their tabs down to 4 spaces
*/
do {
EXPAND(p->text) = ' ';
} while ( ++xp % a->tabstop );
}
else if ( c >= ' ' ) {
EXPAND(p->text) = c;
++xp;
}
}
EXPAND(p->text) = 0;
S(p->text)--;
p->dle = mkd_firstnonblank(p);
}
#ifdef PANDOC_HEADER
/* trim leading blanks from a header line
*/
static void
snip(Line *p)
{
CLIP(p->text, 0, 1);
p->dle = mkd_firstnonblank(p);
}
#endif
/* build a Document from any old input.
*/
typedef int (*getc_func)(void*);
Document *
populate(getc_func getc, void* ctx, int flags)
{
Cstring line;
Document *a = new_Document();
int c;
#ifdef PANDOC_HEADER
int pandoc = 0;
#endif
if ( !a ) return 0;
a->tabstop = (flags & STD_TABSTOP) ? 4 : TABSTOP;
CREATE(line);
while ( (c = (*getc)(ctx)) != EOF ) {
if ( c == '\n' ) {
#ifdef PANDOC_HEADER
if ( pandoc != EOF && pandoc < 3 ) {
if ( S(line) && (T(line)[0] == '%') )
pandoc++;
else
pandoc = EOF;
}
#endif
queue(a, &line);
S(line) = 0;
}
else if ( isprint(c) || isspace(c) || (c & 0x80) )
EXPAND(line) = c;
}
if ( S(line) )
queue(a, &line);
DELETE(line);
#ifdef PANDOC_HEADER
if ( (pandoc == 3) && !(flags & NO_HEADER) ) {
/* the first three lines started with %, so we have a header.
* clip the first three lines out of content and hang them
* off header.
*/
a->headers = T(a->content);
T(a->content) = a->headers->next->next->next;
a->headers->next->next->next = 0;
snip(a->headers);
snip(a->headers->next);
snip(a->headers->next->next);
}
#endif
return a;
}
/* convert a file into a linked list
*/
Document *
mkd_in(FILE *f, int flags)
{
return populate((getc_func)fgetc, f, flags & INPUT_MASK);
}
/* return a single character out of a buffer
*/
struct string_ctx {
char *data; /* the unread data */
int size; /* and how much is there? */
} ;
static int
strget(struct string_ctx *in)
{
if ( !in->size ) return EOF;
--(in->size);
return *(in->data)++;
}
/* convert a block of text into a linked list
*/
Document *
mkd_string(char *buf, int len, int flags)
{
struct string_ctx about;
about.data = buf;
about.size = len;
return populate((getc_func)strget, &about, flags & INPUT_MASK);
}
/* write the html to a file (xmlified if necessary)
*/
int
mkd_generatehtml(Document *p, FILE *output)
{
char *doc;
int szdoc;
if ( (szdoc = mkd_document(p, &doc)) != EOF ) {
if ( p->ctx->flags & CDATA_OUTPUT )
mkd_generatexml(doc, szdoc, output);
else
fwrite(doc, szdoc, 1, output);
putc('\n', output);
return 0;
}
return -1;
}
/* convert some markdown text to html
*/
int
markdown(Document *document, FILE *out, int flags)
{
if ( mkd_compile(document, flags) ) {
mkd_generatehtml(document, out);
mkd_cleanup(document);
return 0;
}
return -1;
}
void
mkd_basename(Document *document, char *base)
{
if ( document )
document->base = base;
}
/* write out a Cstring, mangled into a form suitable for `<a href=` or `<a id=`
*/
void
mkd_string_to_anchor(char *s, int len, void(*outchar)(int,void*), void *out)
{
unsigned char c;
for ( ; len-- > 0; ) {
c = *s++;
if ( c == ' ' || c == '&' || c == '<' || c == '"' )
(*outchar)('+', out);
else if ( isalnum(c) || ispunct(c) || (c & 0x80) )
(*outchar)(c, out);
else
(*outchar)('~',out);
}
}
/* ___mkd_reparse() a line
*/
static void
mkd_parse_line(char *bfr, int size, MMIOT *f, int flags)
{
___mkd_initmmiot(f, 0);
f->flags = flags & USER_FLAGS;
___mkd_reparse(bfr, size, 0, f);
___mkd_emblock(f);
}
/* ___mkd_reparse() a line, returning it in malloc()ed memory
*/
int
mkd_line(char *bfr, int size, char **res, int flags)
{
MMIOT f;
int len;
mkd_parse_line(bfr, size, &f, flags);
if ( len = S(f.out) ) {
/* kludge alert; we know that T(f.out) is malloced memory,
* so we can just steal it away. This is awful -- there
* should be an opaque method that transparently moves
* the pointer out of the embedded Cstring.
*/
*res = T(f.out);
T(f.out) = 0;
S(f.out) = 0;
}
else {
*res = 0;
len = EOF;
}
___mkd_freemmiot(&f, 0);
return len;
}
/* ___mkd_reparse() a line, writing it to a FILE
*/
int
mkd_generateline(char *bfr, int size, FILE *output, int flags)
{
MMIOT f;
mkd_parse_line(bfr, size, &f, flags);
if ( flags & CDATA_OUTPUT )
mkd_generatexml(T(f.out), S(f.out), output);
else
fwrite(T(f.out), S(f.out), 1, output);
___mkd_freemmiot(&f, 0);
return 0;
}
/* set the url display callback
*/
void
mkd_e_url(Document *f, e_func edit)
{
if ( f && f->ctx ) f->ctx->e_url = edit;
}
/* set the url options callback
*/
void
mkd_e_flags(Document *f, e_func edit)
{
if ( f && f->ctx ) f->ctx->e_flags = edit;
}
/* set the url display/options deallocator
*/
void
mkd_e_free(Document *f, void (*dealloc)(void*,void*))
{
if ( f && f->ctx ) f->ctx->e_free = dealloc;
}
/* set the url display/options context field
*/
void
mkd_e_context(Document *f, void *context)
{
if ( f && f->ctx ) f->ctx->e_context = context;
}

View File

@@ -0,0 +1,87 @@
#ifndef _MKDIO_D
#define _MKDIO_D
#include <stdio.h>
typedef void MMIOT;
/* line builder for markdown()
*/
MMIOT *mkd_in(FILE*,int); /* assemble input from a file */
MMIOT *mkd_string(char*,int,int); /* assemble input from a buffer */
void mkd_basename(MMIOT*,char*);
/* compilation, debugging, cleanup
*/
int mkd_compile(MMIOT*, int);
int mkd_cleanup(MMIOT*);
/* markup functions
*/
int mkd_dump(MMIOT*, FILE*, int, char*);
int markdown(MMIOT*, FILE*, int);
int mkd_line(char *, int, char **, int);
void mkd_string_to_anchor(char *, int, int (*)(int,void*), void*);
int mkd_xhtmlpage(MMIOT*,int,FILE*);
/* header block access
*/
char* mkd_doc_title(MMIOT*);
char* mkd_doc_author(MMIOT*);
char* mkd_doc_date(MMIOT*);
/* compiled data access
*/
int mkd_document(MMIOT*, char**);
int mkd_toc(MMIOT*, char**);
int mkd_css(MMIOT*, char **);
int mkd_xml(char *, int, char **);
/* write-to-file functions
*/
int mkd_generatehtml(MMIOT*,FILE*);
int mkd_generatetoc(MMIOT*,FILE*);
int mkd_generatexml(char *, int,FILE*);
int mkd_generatecss(MMIOT*,FILE*);
#define mkd_style mkd_generatecss
int mkd_generateline(char *, int, FILE*, int);
#define mkd_text mkd_generateline
/* url generator callbacks
*/
typedef char * (e_func)(char*, int, void*);
void mkd_e_url(void *, e_func);
void mkd_e_flags(void *, e_func);
void mkd_e_free(void *, void (*dealloc)(void*, void*) );
void mkd_e_context(void *, void *);
/* version#.
*/
extern char markdown_version[];
/* special flags for markdown() and mkd_text()
*/
#define MKD_NOLINKS 0x0001 /* don't do link processing, block <a> tags */
#define MKD_NOIMAGE 0x0002 /* don't do image processing, block <img> */
#define MKD_NOPANTS 0x0004 /* don't run smartypants() */
#define MKD_NOHTML 0x0008 /* don't allow raw html through AT ALL */
#define MKD_STRICT 0x0010 /* disable SUPERSCRIPT, RELAXED_EMPHASIS */
#define MKD_TAGTEXT 0x0020 /* process text inside an html tag; no
* <em>, no <bold>, no html or [] expansion */
#define MKD_NO_EXT 0x0040 /* don't allow pseudo-protocols */
#define MKD_CDATA 0x0080 /* generate code for xml ![CDATA[...]] */
#define MKD_NOTABLES 0x0400 /* disallow tables */
#define MKD_TOC 0x1000 /* do table-of-contents processing */
#define MKD_1_COMPAT 0x2000 /* compatability with MarkdownTest_1.0 */
#define MKD_AUTOLINK 0x4000 /* make http://foo.com link even without <>s */
#define MKD_SAFELINK 0x8000 /* paranoid check for link protocol */
#define MKD_EMBED MKD_NOLINKS|MKD_NOIMAGE|MKD_TAGTEXT
/* special flags for mkd_in() and mkd_string()
*/
#define MKD_NOHEADER 0x0100 /* don't process header blocks */
#define MKD_TABSTOP 0x0200 /* expand tabs to 4 spaces */
#endif/*_MKDIO_D*/

View File

@@ -0,0 +1,155 @@
/* markdown: a C implementation of John Gruber's Markdown markup language.
*
* Copyright (C) 2007 David L Parsons.
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <time.h>
#include <ctype.h>
#include "config.h"
#include "cstring.h"
#include "markdown.h"
#include "amalloc.h"
/* free a (single) line
*/
void
___mkd_freeLine(Line *ptr)
{
DELETE(ptr->text);
free(ptr);
}
/* free a list of lines
*/
void
___mkd_freeLines(Line *p)
{
if (p->next)
___mkd_freeLines(p->next);
___mkd_freeLine(p);
}
/* bye bye paragraph.
*/
void
___mkd_freeParagraph(Paragraph *p)
{
if (p->next)
___mkd_freeParagraph(p->next);
if (p->down)
___mkd_freeParagraph(p->down);
if (p->text)
___mkd_freeLines(p->text);
if (p->ident)
free(p->ident);
free(p);
}
/* bye bye footnote.
*/
void
___mkd_freefootnote(Footnote *f)
{
DELETE(f->tag);
DELETE(f->link);
DELETE(f->title);
}
/* bye bye footnotes.
*/
void
___mkd_freefootnotes(MMIOT *f)
{
int i;
if ( f->footnotes ) {
for (i=0; i < S(*f->footnotes); i++)
___mkd_freefootnote( &T(*f->footnotes)[i] );
DELETE(*f->footnotes);
free(f->footnotes);
}
}
/* initialize a new MMIOT
*/
void
___mkd_initmmiot(MMIOT *f, void *footnotes)
{
if ( f ) {
memset(f, 0, sizeof *f);
CREATE(f->in);
CREATE(f->out);
CREATE(f->Q);
if ( footnotes )
f->footnotes = footnotes;
else {
f->footnotes = malloc(sizeof f->footnotes[0]);
CREATE(*f->footnotes);
}
}
}
/* free the contents of a MMIOT, but leave the object alone.
*/
void
___mkd_freemmiot(MMIOT *f, void *footnotes)
{
if ( f ) {
DELETE(f->in);
DELETE(f->out);
DELETE(f->Q);
if ( f->footnotes != footnotes )
___mkd_freefootnotes(f);
memset(f, 0, sizeof *f);
}
}
/* free lines up to an barrier.
*/
void
___mkd_freeLineRange(Line *anchor, Line *stop)
{
Line *r = anchor->next;
if ( r != stop ) {
while ( r && (r->next != stop) )
r = r->next;
if ( r ) r->next = 0;
___mkd_freeLines(anchor->next);
}
anchor->next = 0;
}
/* clean up everything allocated in __mkd_compile()
*/
void
mkd_cleanup(Document *doc)
{
if ( doc ) {
if ( doc->ctx ) {
___mkd_freemmiot(doc->ctx, 0);
free(doc->ctx);
}
if ( doc->code) ___mkd_freeParagraph(doc->code);
if ( doc->headers ) ___mkd_freeLines(doc->headers);
if ( T(doc->content) ) ___mkd_freeLines(T(doc->content));
memset(doc, 0, sizeof doc[0]);
free(doc);
}
}

View File

@@ -0,0 +1,46 @@
./echo 'Reddit-style automatic links'
rc=0
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try -fautolink 'single link' \
'http://www.pell.portland.or.us/~orc/Code/discount' \
'<p><a href="http://www.pell.portland.or.us/~orc/Code/discount">http://www.pell.portland.or.us/~orc/Code/discount</a></p>'
try -fautolink '[!](http://a.com "http://b.com")' \
'[!](http://a.com "http://b.com")' \
'<p><a href="http://a.com" title="http://b.com">!</a></p>'
try -fautolink 'link surrounded by text' \
'here http://it is?' \
'<p>here <a href="http://it">http://it</a> is?</p>'
try -fautolink 'naked @' '@' '<p>@</p>'
try -fautolink 'parenthesised (url)' \
'(http://here)' \
'<p>(<a href="http://here">http://here</a>)</p>'
try -fautolink 'token with trailing @' 'orc@' '<p>orc@</p>'
exit $rc

View File

@@ -0,0 +1,53 @@
./echo "automatic links"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
match() {
./echo -n " $1" '..................................' | ./cols 36
if ./echo "$2" | ./markdown | grep "$3" >/dev/null; then
./echo " ok"
else
./echo " FAILED"
rc=1
fi
}
try 'http url' '<http://here>' '<p><a href="http://here">http://here</a></p>'
try 'ftp url' '<ftp://here>' '<p><a href="ftp://here">ftp://here</a></p>'
match '<orc@pell.portland.or.us>' '<orc@pell.portland.or.us>' '<a href='
match '<orc@pell.com.>' '<orc@pell.com.>' '<a href='
try 'invalid <orc@>' '<orc@>' '<p>&lt;orc@></p>'
try 'invalid <@pell>' '<@pell>' '<p>&lt;@pell></p>'
try 'invalid <orc@pell>' '<orc@pell>' '<p>&lt;orc@pell></p>'
try 'invalid <orc@.pell>' '<orc@.pell>' '<p>&lt;orc@.pell></p>'
try 'invalid <orc@pell.>' '<orc@pell.>' '<p>&lt;orc@pell.></p>'
match '<mailto:orc@pell>' '<mailto:orc@pell>' '<a href='
match '<mailto:orc@pell.com>' '<mailto:orc@pell.com>' '<a href='
match '<mailto:orc@>' '<mailto:orc@>' '<a href='
match '<mailto:@pell>' '<mailto:@pell>' '<a href='
exit $rc

View File

@@ -0,0 +1,35 @@
./echo "backslash escapes"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got: $Q"
rc=1
fi
}
try 'backslashes in []()' '[foo](http://\this\is\.a\test\(here\))' \
'<p><a href="http://\this\is.a\test(here)">foo</a></p>'
try -fautolink 'autolink url with trailing \' \
'http://a.com/\' \
'<p><a href="http://a.com/\">http://a.com/\</a></p>'
exit $rc

View File

@@ -0,0 +1,13 @@
->###chrome with my markdown###<-
1. `(c)` -> `&copy;` (c)
2. `(r)` -> `&reg;` (r)
3. `(tm)` -> `&trade;` (tm)
4. `...` -> `&hellip;` ...
5. `--` -> `&emdash;` --
6. `-` -> `&ndash;` - (but not if it's between-words)
7. "fancy quoting"
8. 'fancy quoting (#2)'
9. don't do it unless it's a real quote.
10. `` (`) ``

View File

@@ -0,0 +1,39 @@
./echo "code blocks"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'format for code block html' \
' this is
code' \
'<pre><code>this is
code
</code></pre>'
try 'unclosed single backtick' '`hi there' '<p>`hi there</p>'
try 'unclosed double backtick' '``hi there' '<p>``hi there</p>'
try 'remove space around code' '`` hi there ``' '<p><code>hi there</code></p>'
exit $rc

View File

@@ -0,0 +1,47 @@
./echo "markdown 1.0 compatability"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
LINKY='[this] is a test
[this]: /this'
try 'implicit reference links' "$LINKY" '<p><a href="/this">this</a> is a test</p>'
try -f1.0 'implicit reference links (-f1.0)' "$LINKY" '<p>[this] is a test</p>'
WSP=' '
WHITESPACE="
white space$WSP
and more"
try 'trailing whitespace' "$WHITESPACE" '<pre><code>white space ''
and more
</code></pre>'
try -f1.0 'trailing whitespace (-f1.0)' "$WHITESPACE" '<pre><code>white space''
and more
</code></pre>'
exit $rc

View File

@@ -0,0 +1,57 @@
./echo "crashes"
rc=0
MARKDOWN_FLAGS=
./echo -n ' zero-length input ................ '
if ./markdown < /dev/null >/dev/null; then
./echo "ok"
else
./echo "FAILED"
rc=1
fi
./echo -n ' hanging quote in list ............ '
./markdown >/dev/null 2>/dev/null << EOF
* > this should not die
no.
EOF
if [ "$?" -eq 0 ]; then
./echo "ok"
else
./echo "FAILED"
rc=1
fi
./echo -n ' dangling list item ............... '
if ./echo ' - ' | ./markdown >/dev/null 2>/dev/null; then
./echo "ok"
else
./echo "FAILED"
rc=1
fi
./echo -n ' empty []() with baseurl .......... '
if ./markdown -bHOHO -s '[]()' >/dev/null 2>/dev/null; then
./echo "ok"
else
./echo "FAILED"
rc=1
fi
./echo -n ' unclosed html block .............. '
if ./echo '<table></table' | ./markdown >/dev/null 2>/dev/null; then
./echo 'ok'
else
./echo "FAILED"
rc=1
fi
exit $rc

View File

@@ -0,0 +1,67 @@
./markdown -V | grep DIV >/dev/null || exit 0
./echo "%div% blocks"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'simple >%div% block' \
'>%this%
this this' \
'<div class="this"><p>this this</p></div>'
try 'two >%div% blocks in a row' \
'>%this%
this this
>%that%
that that' \
'<div class="this"><p>this this</p></div>
<div class="that"><p>that that</p></div>'
try '>%class:div%' \
'>%class:this%
this this' \
'<div class="this"><p>this this</p></div>'
try '>%id:div%' \
'>%id:this%
this this' \
'<div id="this"><p>this this</p></div>'
try 'nested >%div%' \
'>%this%
>>%that%
>>that
>%more%
more' \
'<div class="this"><div class="that"><p>that</p></div></div>
<div class="more"><p>more</p></div>'
exit $rc

View File

@@ -0,0 +1,69 @@
./echo "definition lists"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
SRC='
=this=
is an ugly
=test=
eh?'
RSLT='<dl>
<dt>this</dt>
<dd>is an ugly</dd>
<dt>test</dt>
<dd>eh?</dd>
</dl>'
if ./markdown -V | grep DL_TAG >/dev/null; then
try '=tag= generates definition lists' "$SRC" "$RSLT"
try 'one item with two =tags=' \
'=this=
=is=
A test, eh?' \
'<dl>
<dt>this</dt>
<dt>is</dt>
<dd>A test, eh?</dd>
</dl>'
else
try '=tag= does nothing' "$SRC" \
'<p>=this=</p>
<pre><code>is an ugly
</code></pre>
<p>=test=</p>
<pre><code>eh?
</code></pre>'
fi
exit $rc

View File

@@ -0,0 +1,9 @@
* [![an image](http://dustmite.org/mite.jpg =50x50)] (http://dustmite.org)
* [[an embedded link](http://wontwork.org)](http://willwork.org)
* [![dustmite][]] (http:/dustmite.org)
* ![dustmite][]
* ![dustmite][dustmite]
* [<a href="http://cheating.us">cheat me</a>](http://I.am.cheating)
[dustmite]: http://dustmite.org/mite.jpg =25x25 "here I am!"

View File

@@ -0,0 +1,40 @@
./echo "emphasis"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try '*hi* -> <em>hi</em>' '*hi*' '<p><em>hi</em></p>'
try '* -> *' 'A * A' '<p>A * A</p>'
try -fstrict '***A**B*' '***A**B*' '<p><em><strong>A</strong>B</em></p>'
try -fstrict '***A*B**' '***A*B**' '<p><strong><em>A</em>B</strong></p>'
try -fstrict '**A*B***' '**A*B***' '<p><strong>A<em>B</em></strong></p>'
try -fstrict '*A**B***' '*A**B***' '<p><em>A<strong>B</strong></em></p>'
if ./markdown -V | grep RELAXED >/dev/null; then
try -frelax '_A_B with -frelax' '_A_B' '<p>_A_B</p>'
try -fstrict '_A_B with -fstrict' '_A_B' '<p><em>A</em>B</p>'
fi
exit $rc

View File

@@ -0,0 +1,52 @@
./echo "paragraph flow"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'header followed by paragraph' \
'###Hello, sailor###
And how are you today?' \
'<h3>Hello, sailor</h3>
<p>And how are you today?</p>'
try 'two lists punctuated with a HR' \
'* A
* * *
* B
* C' \
'<ul>
<li>A</li>
</ul>
<hr />
<ul>
<li>B</li>
<li>C</li>
</ul>'
exit $rc

View File

@@ -0,0 +1,34 @@
./echo "footnotes"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'a line with multiple []s' '[a][] [b][]:' '<p>[a][] [b][]:</p>'
try 'a valid footnote' \
'[alink][]
[alink]: link_me' \
'<p><a href="link_me">alink</a></p>'
exit $rc

View File

@@ -0,0 +1,46 @@
./echo "headers"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
S=`./echo -n "$1" '..................................' | ./cols 34`
./echo -n " $S "
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo "ok"
else
./echo "FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'single #' '#' '<p>#</p>'
try 'empty ETX' '##' '<h1>#</h1>'
try 'single-char ETX (##W)' '##W' '<h2>W</h2>'
try 'single-char ETX (##W )' '##W ' '<h2>W</h2>'
try 'single-char ETX (## W)' '## W' '<h2>W</h2>'
try 'single-char ETX (## W )' '## W ' '<h2>W</h2>'
try 'single-char ETX (##W##)' '##W##' '<h2>W</h2>'
try 'single-char ETX (##W ##)' '##W ##' '<h2>W</h2>'
try 'single-char ETX (## W##)' '## W##' '<h2>W</h2>'
try 'single-char ETX (## W ##)' '## W ##' '<h2>W</h2>'
try 'multiple-char ETX (##Hello##)' '##Hello##' '<h2>Hello</h2>'
try 'SETEXT with trailing whitespace' \
'hello
===== ' '<h1>hello</h1>'
exit $rc

View File

@@ -0,0 +1,112 @@
./echo "html blocks"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'self-closing block tags (hr)' \
'<hr>
text' \
'<hr>
<p>text</p>'
try 'self-closing block tags (hr/)' \
'<hr/>
text' \
'<hr/>
<p>text</p>'
try 'self-closing block tags (br)' \
'<br>
text' \
'<br>
<p>text</p>'
try 'html comments' \
'<!--
**hi**
-->' \
'<!--
**hi**
-->'
try 'no smartypants inside tags (#1)' \
'<img src="linky">' \
'<p><img src="linky"></p>'
try 'no smartypants inside tags (#2)' \
'<img src="linky" alt=":)" />' \
'<p><img src="linky" alt=":)" /></p>'
try -fnohtml 'block html with -fnohtml' '<b>hi!</b>' '<p>&lt;b>hi!&lt;/b></p>'
try -fnohtml 'malformed tag injection' '<x <script>' '<p>&lt;x &lt;script></p>'
try -fhtml 'allow html with -fhtml' '<b>hi!</b>' '<p><b>hi!</b></p>'
# check that nested raw html blocks terminate properly.
#
BLOCK1SRC='Markdown works fine *here*.
*And* here.
<div><pre>
</pre></div>
Markdown here is *not* parsed by RDiscount.
Nor in *this* paragraph, and there are no paragraph breaks.'
BLOCK1OUT='<p>Markdown works fine <em>here</em>.</p>
<p><em>And</em> here.</p>
<div><pre>
</pre></div>
<p>Markdown here is <em>not</em> parsed by RDiscount.</p>
<p>Nor in <em>this</em> paragraph, and there are no paragraph breaks.</p>'
try 'nested html blocks (1)' "$BLOCK1SRC" "$BLOCK1OUT"
try 'nested html blocks (2)' \
'<div>This is inside a html block
<div>This is, too</div>and
so is this</div>' \
'<div>This is inside a html block
<div>This is, too</div>and
so is this</div>'
try 'unfinished tags' '<foo bar' '<p>&lt;foo bar</p>'
exit $rc

View File

@@ -0,0 +1,14 @@
1. <http://automatic>
2. [automatic] (http://automatic "automatic link")
3. [automatic](http://automatic "automatic link")
4. [automatic](http://automatic)
5. [automatic] (http://automatic)
6. [automatic] []
7. [automatic][]
8. [my][automatic]
9. [my] [automatic]
[automatic]: http://automatic "footnote"
[automatic] [

View File

@@ -0,0 +1,124 @@
./echo "embedded links"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'url contains &' '[hehehe](u&rl)' '<p><a href="u&amp;rl">hehehe</a></p>'
try 'url contains +' '[hehehe](u+rl)' '<p><a href="u+rl">hehehe</a></p>'
try 'url contains "' '[hehehe](u"rl)' '<p><a href="u%22rl">hehehe</a></p>'
try 'url contains <' '[hehehe](u<rl)' '<p><a href="u&lt;rl">hehehe</a></p>'
try 'url contains whitespace' '[ha](r u)' '<p><a href="r%20u">ha</a></p>'
try 'url contains whitespace & title' \
'[hehehe](r u "there")' \
'<p><a href="r%20u" title="there">hehehe</a></p>'
try 'url contains escaped )' \
'[hehehe](u\))' \
'<p><a href="u)">hehehe</a></p>'
try 'image label contains <' \
'![he<he<he](url)' \
'<p><img src="url" alt="he&lt;he&lt;he" /></p>'
try 'image label contains >' \
'![he>he>he](url)' \
'<p><img src="url" alt="he&gt;he&gt;he" /></p>'
try 'sloppy context link' \
'[heh]( url "how about it?" )' \
'<p><a href="url" title="how about it?">heh</a></p>'
try 'footnote urls formed properly' \
'[hehehe]: hohoho "ha ha"
[hehehe][]' \
'<p><a href="hohoho" title="ha ha">hehehe</a></p>'
try 'linky-like []s work' \
'[foo]' \
'<p>[foo]</p>'
try 'pseudo-protocol "id:"'\
'[foo](id:bar)' \
'<p><a id="bar">foo</a></p>'
try 'pseudo-protocol "class:"' \
'[foo](class:bar)' \
'<p><span class="bar">foo</span></p>'
try 'pseudo-protocol "abbr:"'\
'[foo](abbr:bar)' \
'<p><abbr title="bar">foo</abbr></p>'
try 'nested [][]s' \
'[[z](y)](x)' \
'<p><a href="x">[z](y)</a></p>'
try 'empty [][] tags' \
'[![][1]][2]
[1]: image1
[2]: image2' \
'<p><a href="image2"><img src="image1" alt="" /></a></p>'
try 'footnote cuddled up to text' \
'foo
[bar]:bar' \
'<p>foo</p>'
try 'mid-paragraph footnote' \
'talk talk talk talk
[bar]: bar
talk talk talk talk' \
'<p>talk talk talk talk
talk talk talk talk</p>'
try 'mid-blockquote footnote' \
'>blockquote!
[footnote]: here!
>blockquote!' \
'<blockquote><p>blockquote!
blockquote!</p></blockquote>'
try 'end-blockquote footnote' \
'>blockquote!
>blockquote!
[footnote]: here!' \
'<blockquote><p>blockquote!
blockquote!</p></blockquote>'
try 'start-blockquote footnote' \
'[footnote]: here!
>blockquote!
>blockquote!' \
'<blockquote><p>blockquote!
blockquote!</p></blockquote>'
try '[text] (text) not a link' \
'[test] (me)' \
'<p>[test] (me)</p>'
exit $rc

View File

@@ -0,0 +1,31 @@
./echo "embedded images"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'image with size extension' \
'![picture](pic =200x200)' \
'<p><img src="pic" height="200" width="200" alt="picture" /></p>'
exit $rc

View File

@@ -0,0 +1,178 @@
./echo "lists"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'two separated items' \
' * A
* B' \
'<ul>
<li><p>A</p></li>
<li><p>B</p></li>
</ul>'
try 'two adjacent items' \
' * A
* B' \
'<ul>
<li>A</li>
<li>B</li>
</ul>'
try 'two adjacent items, then space' \
' * A
* B
space, the final frontier' \
'<ul>
<li>A</li>
<li>B</li>
</ul>
<p>space, the final frontier</p>'
try 'nested lists (1)' \
' * 1. Sub (list)
2. Two (items)
3. Here' \
'<ul>
<li><ol>
<li>Sub (list)</li>
<li>Two (items)</li>
<li>Here</li>
</ol>
</li>
</ul>'
try 'nested lists (2)' \
' * A (list)
1. Sub (list)
2. Two (items)
3. Here
Here
* B (list)' \
'<ul>
<li><p>A (list)</p>
<ol>
<li>Sub (list)</li>
<li>Two (items)</li>
<li>Here</li>
</ol>
<p> Here</p></li>
<li>B (list)</li>
</ul>'
try 'list inside blockquote' \
'>A (list)
>
>1. Sub (list)
>2. Two (items)
>3. Here' \
'<blockquote><p>A (list)</p>
<ol>
<li>Sub (list)</li>
<li>Two (items)</li>
<li>Here</li>
</ol>
</blockquote>'
try 'blockquote inside list' \
' * A (list)
> quote
> me
dont quote me' \
'<ul>
<li><p>A (list)</p>
<blockquote><p>quote
me</p></blockquote>
<p>dont quote me</p></li>
</ul>'
try 'empty list' \
'
-
-
' \
'<ul>
<li></li>
<li></li>
</ul>'
try 'blockquote inside a list' \
' * This is a list item.
> This is a quote insde a list item. ' \
'<ul>
<li><p> This is a list item.</p>
<blockquote><p>This is a quote insde a list item.</p></blockquote></li>
</ul>'
if ./markdown -V | grep DL_TAG >/dev/null; then
try 'dl followed by non-dl' \
'=a=
test
2. here' \
'<dl>
<dt>a</dt>
<dd>test</dd>
</dl>
<ol>
<li>here</li>
</ol>'
try 'non-dl followed by dl' \
'1. hello
=sailor=
hi!' \
'<ol>
<li>hello</li>
</ol>
<dl>
<dt>sailor</dt>
<dd>hi!</dd>
</dl>'
fi
exit $rc

View File

@@ -0,0 +1,57 @@
./echo "deeply nested lists"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
LIST='
* top-level list ( list 1)
+ second-level list (list 2)
* first item third-level list (list 3)
+ * second item, third-level list, first item. (list 4)
* second item, third-level list, second item.
* top-level list again.'
RSLT='<ul>
<li>top-level list ( list 1)
<ul>
<li>second-level list (list 2)
<ul>
<li>first item third-level list (list 3)</li>
</ul>
</li>
<li><ul>
<li>second item, third-level list, first item. (list 4)</li>
<li>second item, third-level list, second item.</li>
</ul>
</li>
</ul>
</li>
<li>top-level list again.</li>
</ul>'
try 'thrice-nested lists' "$LIST" "$RSLT"
exit $rc

View File

@@ -0,0 +1,33 @@
./echo "misc"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'single paragraph' 'AAA' '<p>AAA</p>'
try '< -> &lt;' '<' '<p>&lt;</p>'
try '`>` -> <code>&gt;</code>' '`>`' '<p><code>&gt;</code></p>'
try '`` ` `` -> <code>`</code>' '`` ` ``' '<p><code>`</code></p>'
exit $rc

View File

@@ -0,0 +1,74 @@
./echo "pandoc headers"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
HEADER='% title
% author(s)
% date'
if ./markdown -V | grep HEADER > /dev/null; then
try 'valid header' "$HEADER" ''
try -F0x0100 'valid header with -F0x0100' "$HEADER" '<p>% title
% author(s)
% date</p>'
try 'invalid header' \
'% title
% author(s)
a pony!' \
'<p>% title
% author(s)
a pony!</p>'
try 'offset header' \
'
% title
% author(s)
% date' \
'<p>% title
% author(s)
% date</p>'
try 'indented header' \
' % title
% author(s)
% date' \
'<p> % title
% author(s)
% date</p>'
else
try 'ignore headers' "$HEADER" '<p>% title
% author(s)
% date</p>'
fi
exit $rc

View File

@@ -0,0 +1,38 @@
./echo "paragraph blocking"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'paragraph followed by code' \
'a
b' \
'<p>a</p>
<pre><code>b
</code></pre>'
try 'single-line paragraph' 'a' '<p>a</p>'
exit $rc

View File

@@ -0,0 +1,31 @@
./echo "paranoia"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try -fsafelink 'bogus url (-fsafelink)' '[test](bad:protocol)' '<p>[test](bad:protocol)</p>'
try -fnosafelink 'bogus url (-fnosafelink)' '[test](bad:protocol)' '<p><a href="bad:protocol">test</a></p>'
exit $rc

View File

@@ -0,0 +1,66 @@
./echo "markup peculiarities"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'list followed by header .......... ' \
"
- AAA
- BBB
-" \
'<ul>
<li>AAA
<h2>&ndash; BBB</h2></li>
</ul>'
try 'ul with mixed item prefixes' \
'
- A
1. B' \
'<ul>
<li>A</li>
<li>B</li>
</ul>'
try 'ol with mixed item prefixes' \
'
1. A
- B
' \
'<ol>
<li>A</li>
<li>B</li>
</ol>'
try 'forcing a <br/>' 'this
is' '<p>this<br/>
is</p>'
try 'trimming single spaces' 'this ' '<p>this</p>'
try -fnohtml 'markdown <br/> with -fnohtml' 'foo
is' '<p>foo<br/>
is</p>'
exit $rc

View File

@@ -0,0 +1,35 @@
./echo "pseudo-protocols"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try '[](id:) links' '[foo](id:bar)' '<p><a id="bar">foo</a></p>'
try -fnoext '[](id:) links with -fnoext' '[foo](id:bar)' '<p>[foo](id:bar)</p>'
try '[](class:) links' '[foo](class:bar)' '<p><span class="bar">foo</span></p>'
try -fnoext '[](class:) links with -fnoext' '[foo](class:bar)' '<p>[foo](class:bar)</p>'
try '[](raw:) links' '[foo](raw:bar)' '<p>bar</p>'
try -fnoext '[](raw:) links with -fnoext' '[foo](raw:bar)' '<p>[foo](raw:bar)</p>'
exit $rc

View File

@@ -0,0 +1,34 @@
./echo "footnotes inside reparse sections"
rc=0
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
./echo -n " $1" '..................................' | ./cols 36
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo " ok"
else
./echo " FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try 'footnote inside [] section' \
'[![foo][]](bar)
[foo]: bar2' \
'<p><a href="bar"><img src="bar2" alt="foo" /></a></p>'
exit $rc

View File

@@ -0,0 +1,104 @@
./echo "Bugs & misfeatures reported by Mike Schiraldi"
rc=0
MARKDOWN_FLAGS=
try() {
unset FLAGS
case "$1" in
-*) FLAGS=$1
shift ;;
esac
S=`./echo -n "$1" '..................................' | ./cols 34`
./echo -n " $S "
Q=`./echo "$2" | ./markdown $FLAGS`
if [ "$3" = "$Q" ]; then
./echo "ok"
else
./echo "FAILED"
./echo "wanted: $3"
./echo "got : $Q"
rc=1
fi
}
try -fnohtml 'breaks with -fnohtml' 'foo
bar' '<p>foo<br/>
bar</p>'
try 'links with trailing \)' \
'[foo](http://en.wikipedia.org/wiki/Link_(film\))' \
'<p><a href="http://en.wikipedia.org/wiki/Link_(film)">foo</a></p>'
try -fautolink '(url) with -fautolink' \
'(http://tsfr.org)' \
'<p>(<a href="http://tsfr.org">http://tsfr.org</a>)</p>'
try 'single #' \
'#' \
'<p>#</p>'
try -frelax '* processing with -frelax' \
'2*4 = 8 * 1 = 2**3' \
'<p>2*4 = 8 * 1 = 2**3</p>'
try -fnopants '[]() with a single quote mark' \
'[Poe'"'"'s law](http://rationalwiki.com/wiki/Poe'"'"'s_Law)' \
'<p><a href="http://rationalwiki.com/wiki/Poe'"'"'s_Law">Poe'"'"'s law</a></p>'
try -fautolink 'autolink url with escaped spaces' \
'http://\(here\ I\ am\)' \
'<p><a href="http://(here%20I%20am)">http://(here I am)</a></p>'
try -fautolink 'autolink café_racer' \
'http://en.wikipedia.org/wiki/café_racer' \
'<p><a href="http://en.wikipedia.org/wiki/caf%C3%A9_racer">http://en.wikipedia.org/wiki/caf%C3%A9_racer</a></p>'
try -fautolink 'autolink url with arguments' \
'http://foo.bar?a&b=c' \
'<p><a href="http://foo.bar?a&amp;b=c">http://foo.bar?a&amp;b=c</a></p>'
try '\( escapes in []()' \
'[foo](http://a.com/\(foo\))' \
'<p><a href="http://a.com/(foo)">foo</a></p>'
try -fautolink 'autolink url with escaped ()' \
'http://a.com/\(foo\)' \
'<p><a href="http://a.com/(foo)">http://a.com/(foo)</a></p>'
try -fautolink 'autolink url with escaped \' \
'http://a.com/\\\)' \
'<p><a href="http://a.com/\)">http://a.com/\)</a></p>'
try -fautolink 'autolink url with -' \
'http://experts-exchange.com' \
'<p><a href="http://experts-exchange.com">http://experts-exchange.com</a></p>'
try -fautolink 'autolink url with +' \
'http://www67.wolframalpha.com/input/?i=how+old+was+jfk+jr+when+jfk+died' \
'<p><a href="http://www67.wolframalpha.com/input/?i=how+old+was+jfk+jr+when+jfk+died">http://www67.wolframalpha.com/input/?i=how+old+was+jfk+jr+when+jfk+died</a></p>'
try -fautolink 'autolink url with &' \
'http://foo.bar?a&b=c' \
'<p><a href="http://foo.bar?a&amp;b=c">http://foo.bar?a&amp;b=c</a></p>'
try -fautolink 'autolink url with ,' \
'http://www.spiegel.de/international/europe/0,1518,626171,00.html' \
'<p><a href="http://www.spiegel.de/international/europe/0,1518,626171,00.html">http://www.spiegel.de/international/europe/0,1518,626171,00.html</a></p>'
try -fautolink 'autolink url with : & ;' \
'http://www.biblegateway.com/passage/?search=Matthew%205:29-30;&version=31;' \
'<p><a href="http://www.biblegateway.com/passage/?search=Matthew%205:29-30;&amp;version=31;">http://www.biblegateway.com/passage/?search=Matthew%205:29-30;&amp;version=31;</a></p>'
Q="'"
try -fautolink 'security hole with \" in []()' \
'[XSS](/ "\"=\"\"onmouseover='$Q'alert(String.fromCharCode(88,83,83))'$Q'")' \
'<p><a href="/" title="\&quot;=\&quot;\&quot;onmouseover='$Q'alert(String.fromCharCode(88,83,83))'$Q'">XSS</a></p>'
exit $rc

Some files were not shown because too many files have changed in this diff Show More