package freenet.clients.http; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import freenet.client.filter.PushingTagReplacerCallback; import freenet.l10n.NodeL10n; import freenet.node.DarknetPeerNode; import freenet.node.Node; import freenet.node.SecurityLevels; import freenet.pluginmanager.FredPluginL10n; import freenet.support.HTMLNode; import freenet.support.Logger; import freenet.support.api.HTTPRequest; /** Simple class to output standard heads and tail for web interface pages. */ public final class PageMaker { public enum THEME { BOXED("boxed", "Boxed (Top menu)", "", false, false), BOXED_CLASSIC("boxed-classic", "Boxed (Classic menu)", "", false, false), BOXED_DROPDOWN("boxed-dropdown", "Boxed (Dropdown menu)", "", false, false), BOXED_DYNAMIC("boxed-classic", "Boxed (Dynamic menu)", "", false, false), BOXED_STATIC("boxed-static", "Boxed (Static menu)", "", false, false), CLEAN("clean", "Clean", "Mr. Proper", false, false), CLEAN_CLASSIC("clean-classic", "Clean (Classic menu)", "Clean theme with a classic menu.", false, false), CLEAN_DROPDOWN("clean-dropdown", "Clean (Dropdown menu)", "Clean theme with a dropdown menu.", false, false), CLEAN_STATIC("clean-static", "Clean (Static menu)", "Clean theme with a static menu.", false, false), CLEAN_TOP("clean-top", "Clean (Top menu)", "Clean theme with a static top menu.", false, false), GRAYANDBLUE("grayandblue", "Gray And Blue (Classic menu)", "", false, false), GRAYANDBLUE_DYNAMIC("grayandblue-dynamic", "Gray And Blue (Dynamic menu)", "", false, false), GRAYANDBLUE_DROPDOWN("grayandblue-dropdown", "Gray And Blue (Dropdown menu)", "", false, false), GRAYANDBLUE_STATIC("grayandblue-static", "Gray And Blue (Static menu)", "", false, false), GRAYANDBLUE_TOP("grayandblue-top", "Gray And Blue (Top menu)", "", false, false), SKY("sky", "Sky (Top menu)", "", false, false), SKY_CLASSIC("sky-classic", "Sky (Classic menu)", "", false, false), SKY_DROPDOWN("sky-dropdown", "Sky (Dropdown menu)", "", false, false), SKY_DYNAMIC("sky-dynamic", "Sky (Dynamic menu)", "", false, false), SKY_STATIC("sky-static", "Sky (Static menu)", "", false, false), MINIMALBLUE("minimalblue", "Minimal Blue", "A minimalistic theme in blue", false, false), MINIMALISTIC("minimalist", "Minimalistic", "A very minimalistic theme based on Google's designs", true, true), RABBIT_HOLE("rabbit-hole", "Into the Rabbit Hole", "Simple and clean theme", false, false); public static final String[] possibleValues = { BOXED.code, BOXED_CLASSIC.code, BOXED_DROPDOWN.code, BOXED_DYNAMIC.code, BOXED_STATIC.code, CLEAN.code, CLEAN_CLASSIC.code, CLEAN_DROPDOWN.code, CLEAN_STATIC.code, CLEAN_TOP.code, GRAYANDBLUE.code, GRAYANDBLUE_DYNAMIC.code, GRAYANDBLUE_DROPDOWN.code, GRAYANDBLUE_STATIC.code, GRAYANDBLUE_TOP.code, SKY.code, SKY_CLASSIC.code, SKY_DROPDOWN.code, SKY_DYNAMIC.code, SKY_STATIC.code, MINIMALBLUE.code, MINIMALISTIC.code, RABBIT_HOLE.code }; public final String code; // the internal name public final String name; // the name in "human form" public final String description; // description /** * If true, the activelinks will appear on the welcome page, whether * the user has enabled them or not. */ public final boolean forceActivelinks; /** * If true, the "Fetch a key" infobox will appear above the bookmarks * infobox on the welcome page. */ public final boolean fetchKeyBoxAboveBookmarks; private THEME(String code, String name, String description) { this(code, name, description, false, false); } private THEME(String code, String name, String description, boolean forceActivelinks, boolean fetchKeyBoxAboveBookmarks) { this.code = code; this.name = name; this.description = description; this.forceActivelinks = forceActivelinks; this.fetchKeyBoxAboveBookmarks = fetchKeyBoxAboveBookmarks; } public static THEME themeFromName(String cssName) { for(THEME t : THEME.values()) { if(t.code.equalsIgnoreCase(cssName) || t.name.equalsIgnoreCase(cssName)) { return t; } } return getDefault(); } public static THEME getDefault() { return THEME.SKY_STATIC; } } public static final int MODE_SIMPLE = 1; public static final int MODE_ADVANCED = 2; /** Parameter for simple/advanced mode switch. */ private static final String MODE_SWITCH_PARAMETER = "fproxyAdvancedMode"; private THEME theme; private String override; private final Node node; private List<SubMenu> menuList = new ArrayList<SubMenu>(); private Map<String, SubMenu> subMenus = new HashMap<String, SubMenu>(); private static class SubMenu { /** Name of the submenu */ private final String navigationLinkText; /** Link if the user clicks on the submenu itself */ private final String defaultNavigationLink; /** Tooltip */ private final String defaultNavigationLinkTitle; private final FredPluginL10n plugin; private final List<String> navigationLinkTexts = new ArrayList<String>(); private final List<String> navigationLinkTextsNonFull = new ArrayList<String>(); private final Map<String, String> navigationLinkTitles = new HashMap<String, String>(); private final Map<String, String> navigationLinks = new HashMap<String, String>(); private final Map<String, LinkEnabledCallback> navigationLinkCallbacks = new HashMap<String, LinkEnabledCallback>(); private final Map<String, FredPluginL10n> navigationLinkL10n = new HashMap<String, FredPluginL10n>(); public SubMenu(String link, String name, String title, FredPluginL10n plugin) { this.navigationLinkText = name; this.defaultNavigationLink = link; this.defaultNavigationLinkTitle = title; this.plugin = plugin; } public void addNavigationLink(String path, String name, String title, boolean fullOnly, LinkEnabledCallback cb, FredPluginL10n l10n) { navigationLinkTexts.add(name); if(!fullOnly) navigationLinkTextsNonFull.add(name); navigationLinkTitles.put(name, title); navigationLinks.put(name, path); if(cb != null) navigationLinkCallbacks.put(name, cb); if (l10n != null) navigationLinkL10n.put(name, l10n); } /** Remove a link from this sub-menu. */ public void removeNavigationLink(String name) { navigationLinkTexts.remove(name); navigationLinkTextsNonFull.remove(name); navigationLinkTitles.remove(name); navigationLinks.remove(name); navigationLinkL10n.remove(name); //Should this be here? If so, why not remove from navigationLinkCallbacks too } } protected PageMaker(THEME t, Node n) { setTheme(t); this.node = n; } void setOverride(String pointTo) { this.override = pointTo; } public void setTheme(THEME theme2) { if (theme2 == null) { this.theme = THEME.getDefault(); } else { URL themeurl = getClass().getResource("staticfiles/themes/" + theme2.code + "/theme.css"); if (themeurl == null) this.theme = THEME.getDefault(); else this.theme = theme2; } } public synchronized void addNavigationCategory(String link, String name, String title, FredPluginL10n plugin) { SubMenu menu = new SubMenu(link, name, title, plugin); subMenus.put(name, menu); menuList.add(menu); } /** * Add a navigation category to the menu at a given offset. * @param menuOffset The position of the link in FProxy's menu. 0 = left. */ public synchronized void addNavigationCategory(String link, String name, String title, FredPluginL10n plugin, int menuOffset) { SubMenu menu = new SubMenu(link, name, title, plugin); subMenus.put(name, menu); menuList.add(menuOffset, menu); } public synchronized void removeNavigationCategory(String name) { SubMenu menu = subMenus.remove(name); if (menu == null) { Logger.error(this, "can't remove navigation category, name="+name); return; } menuList.remove(menu); } public synchronized void addNavigationLink(String menutext, String path, String name, String title, boolean fullOnly, LinkEnabledCallback cb, FredPluginL10n l10n) { SubMenu menu = subMenus.get(menutext); if(menu == null) throw new NullPointerException("there is no menu named "+menutext); menu.addNavigationLink(path, name, title, fullOnly, cb, l10n); } /** Remove a navigation link from a sub-menu. Applies globally, do not use this to customise * menus when sending one page! */ public synchronized void removeNavigationLink(String menutext, String name) { SubMenu menu = subMenus.get(menutext); // The menu may have already been removed. if(menu != null) menu.removeNavigationLink(name); } public HTMLNode createBackLink(ToadletContext toadletContext, String name) { String referer = toadletContext.getHeaders().get("referer"); if (referer != null) { return new HTMLNode("a", new String[] { "href", "title" }, new String[] { referer, name }, name); } return new HTMLNode("a", new String[] { "href", "title" }, new String[] { "javascript:back()", name }, name); } /** * Generates an FProxy template page suitable for adding content to. * * @param title * Title of the page. * @param ctx * ToadletContext to use to render the page. * @return A template PageNode. */ public PageNode getPageNode(String title, ToadletContext ctx) { return getPageNode(title, true, ctx); } /** * Generates an FProxy template page with optional navigation bar suitable * for adding content to. * * @param title * Title of the page. * @param renderNavigationLinks * Whether to render navigation links. * @param ctx * ToadletContext to use to render the page. * @return A template PageNode. * @deprecated Use * {@link #getPageNode(String, ToadletContext, RenderParameters)} * instead */ @Deprecated public PageNode getPageNode(String title, boolean renderNavigationLinks, ToadletContext ctx) { return getPageNode(title, renderNavigationLinks, true, ctx); } /** * Generates an FProxy template page with optional navigation bar and status * information suitable for adding content to. * * @param title * Title of the page. * @param renderNavigationLinks * Whether to render navigation links. * @param renderStatus * Whether to render the status display. * @param ctx * ToadletContext to use to render the page. * @return A template PageNode. * @deprecated Use * {@link #getPageNode(String, ToadletContext, RenderParameters)} * instead */ @Deprecated public PageNode getPageNode(String title, boolean renderNavigationLinks, boolean renderStatus, ToadletContext ctx) { return getPageNode(title, ctx, new RenderParameters().renderNavigationLinks(renderNavigationLinks).renderStatus(renderStatus).renderModeSwitch(true)); } /** * Generates an FProxy template page suitable for adding content to. * * @param title * Title of the page. * @param ctx * ToadletContext to use to render the page. Can be null, e.g. if the HTML is not * being generated as part of a toadlet request, for example if it's using the old * FredPluginHTTP interface. * @param renderParameters * Parameters for inclusion or omission of certain page elements * @return A template PageNode. */ public PageNode getPageNode(String title, ToadletContext ctx, RenderParameters renderParameters) { boolean fullAccess = ctx == null ? false : ctx.isAllowedFullAccess(); HTMLNode pageNode = new HTMLNode.HTMLDoctype("html", "-//W3C//DTD XHTML 1.1//EN"); HTMLNode htmlNode = pageNode.addChild("html", "xml:lang", NodeL10n.getBase().getSelectedLanguage().isoCode); HTMLNode headNode = htmlNode.addChild("head"); headNode.addChild("meta", new String[] { "http-equiv", "content" }, new String[] { "Content-Type", "text/html; charset=utf-8" }); headNode.addChild("title", title + " - Freenet"); //To make something only rendered when javascript is on, then add the jsonly class to it headNode.addChild("noscript").addChild("style"," .jsonly {display:none;}"); if(override != null) headNode.addChild(getOverrideContent()); else headNode.addChild("link", new String[] { "rel", "href", "type", "title" }, new String[] { "stylesheet", "/static/themes/" + theme.code + "/theme.css", "text/css", theme.code }); boolean sendAllThemes = ctx != null && ctx.getContainer().sendAllThemes(); if(sendAllThemes) { for (THEME t: THEME.values()) { String themeName = t.code; headNode.addChild("link", new String[] { "rel", "href", "type", "media", "title" }, new String[] { "alternate stylesheet", "/static/themes/" + themeName + "/theme.css", "text/css", "screen", themeName }); } } boolean webPushingEnabled = ctx != null && ctx.getContainer().isFProxyJavascriptEnabled() && ctx.getContainer().isFProxyWebPushingEnabled(); // Add the generated javascript, if it and pushing is enabled if (webPushingEnabled) headNode.addChild("script", new String[] { "type", "language", "src" }, new String[] { "text/javascript", "javascript", "/static/freenetjs/freenetjs.nocache.js" }); Toadlet t; if (ctx != null) { t = ctx.activeToadlet(); t = t.showAsToadlet(ctx); } else t = null; String activePath = ""; if(t != null) activePath = t.path(); HTMLNode bodyNode = htmlNode.addChild("body", new String[] { "class", "id" }, new String[] { "fproxy-page", filterCSSIdentifier("page-"+activePath) }); //Add a hidden input that has the request's id if(webPushingEnabled) bodyNode.addChild("input",new String[]{"type","name","value","id"},new String[]{"hidden","requestId",ctx.getUniqueId(),"requestId"}); // Add the client-side localization only when pushing is enabled if (webPushingEnabled) { bodyNode.addChild("script", new String[] { "type", "language" }, new String[] { "text/javascript", "javascript" }).addChild("%", PushingTagReplacerCallback.getClientSideLocalizationScript()); } HTMLNode pageDiv = bodyNode.addChild("div", "id", "page"); HTMLNode topBarDiv = pageDiv.addChild("div", "id", "topbar"); if (renderParameters.isRenderStatus() && fullAccess) { final HTMLNode statusBarDiv = pageDiv.addChild("div", "id", "statusbar-container").addChild("div", "id", "statusbar"); if (node != null && node.clientCore != null) { final HTMLNode alerts = ctx.getAlertManager().createSummary(true); if (alerts != null) { statusBarDiv.addChild(alerts).addAttribute("id", "statusbar-alerts"); statusBarDiv.addChild("div", "class", "separator", "\u00a0"); } } statusBarDiv.addChild("div", "id", "statusbar-language").addChild("a", "href", "/config/node#l10n", NodeL10n.getBase().getSelectedLanguage().fullName); if (node.clientCore != null && ctx != null && renderParameters.isRenderModeSwitch()) { boolean isAdvancedMode = ctx.isAdvancedModeEnabled(); String uri = ctx.getUri().getQuery(); Map<String, List<String>> parameters = HTTPRequestImpl.parseUriParameters(uri, true); List<String> newModeSwitchValues = new ArrayList<String>(); newModeSwitchValues.add(String.valueOf(isAdvancedMode ? MODE_SIMPLE : MODE_ADVANCED)); /* overwrite any previously existing parameter value. */ parameters.put(MODE_SWITCH_PARAMETER, newModeSwitchValues); statusBarDiv.addChild("div", "class", "separator", "\u00a0"); final HTMLNode switchMode = statusBarDiv.addChild("div", "id", "statusbar-switchmode"); switchMode.addAttribute("class", isAdvancedMode ? "simple" : "advanced"); switchMode.addChild("a", "href", "?" + HTTPRequestImpl.createQueryString(parameters, false), isAdvancedMode ? NodeL10n.getBase().getString("StatusBar.switchToSimpleMode") : NodeL10n.getBase().getString("StatusBar.switchToAdvancedMode")); } if (node != null && node.clientCore != null) { statusBarDiv.addChild("div", "class", "separator", "\u00a0"); final HTMLNode secLevels = statusBarDiv.addChild("div", "id", "statusbar-seclevels", NodeL10n.getBase().getString("SecurityLevels.statusBarPrefix")); final HTMLNode network = secLevels.addChild("a", "href", "/seclevels/", SecurityLevels.localisedName(node.securityLevels.getNetworkThreatLevel()) + "\u00a0"); network.addAttribute("title", NodeL10n.getBase().getString("SecurityLevels.networkThreatLevelShort")); network.addAttribute("class", node.securityLevels.getNetworkThreatLevel().toString().toLowerCase()); final HTMLNode physical = secLevels.addChild("a", "href", "/seclevels/", SecurityLevels.localisedName(node.securityLevels.getPhysicalThreatLevel())); physical.addAttribute("title", NodeL10n.getBase().getString("SecurityLevels.physicalThreatLevelShort")); physical.addAttribute("class", node.securityLevels.getPhysicalThreatLevel().toString().toLowerCase()); statusBarDiv.addChild("div", "class", "separator", "\u00a0"); final int connectedPeers = node.peers.countConnectedPeers(); int darknetTotal = 0; for(DarknetPeerNode n : node.peers.getDarknetPeers()) { if(n == null) continue; if(n.isDisabled()) continue; darknetTotal++; } final int connectedDarknetPeers = node.peers.countConnectedDarknetPeers(); final int totalPeers = (node.getOpennet() == null) ? (darknetTotal > 0 ? darknetTotal : Integer.MAX_VALUE) : node.getOpennet().getNumberOfConnectedPeersToAimIncludingDarknet(); final double connectedRatio = ((double)connectedPeers) / (double)totalPeers; final String additionalClass; // If we use Opennet, we color the bar by the ratio of connected nodes if(connectedPeers > connectedDarknetPeers) { if (connectedRatio < 0.3D || connectedPeers < 3) { additionalClass = "very-few-peers"; } else if (connectedRatio < 0.5D) { additionalClass = "few-peers"; } else if (connectedRatio < 0.75D) { additionalClass = "avg-peers"; } else { additionalClass = "full-peers"; } } else { // If we are darknet only, we color by absolute connected peers if (connectedDarknetPeers < 3) { additionalClass = "very-few-peers"; } else if (connectedDarknetPeers < 5) { additionalClass = "few-peers"; } else if (connectedDarknetPeers < 10) { additionalClass = "avg-peers"; } else { additionalClass = "full-peers"; } } HTMLNode progressBar = statusBarDiv.addChild("div", "class", "progressbar"); progressBar.addChild("div", new String[] { "class", "style" }, new String[] { "progressbar-done progressbar-peers " + additionalClass, "width: " + Math.min(100,Math.floor(100*connectedRatio)) + "%;" }); progressBar.addChild("div", new String[] { "class", "title" }, new String[] { "progress_fraction_finalized", NodeL10n.getBase().getString("StatusBar.connectedPeers", new String[]{"X", "Y"}, new String[]{Integer.toString(node.peers.countConnectedDarknetPeers()), Integer.toString(node.peers.countConnectedOpennetPeers())}) }, Integer.toString(connectedPeers) + ((totalPeers != Integer.MAX_VALUE) ? " / " + Integer.toString(totalPeers) : "")); } } topBarDiv.addChild("h1", title); if (renderParameters.isRenderNavigationLinks()) { SubMenu selected = null; // Render the full menu. HTMLNode navbarDiv = pageDiv.addChild("div", "id", "navbar"); HTMLNode navbarUl = navbarDiv.addChild("ul", "id", "navlist"); synchronized (this) { for (SubMenu menu : menuList) { HTMLNode subnavlist = new HTMLNode("ul"); boolean isSelected = false; boolean nonEmpty = false; for (String navigationLink : fullAccess ? menu.navigationLinkTexts : menu.navigationLinkTextsNonFull) { LinkEnabledCallback cb = menu.navigationLinkCallbacks.get(navigationLink); if(cb != null && !cb.isEnabled(ctx)) continue; nonEmpty = true; String navigationTitle = menu.navigationLinkTitles.get(navigationLink); String navigationPath = menu.navigationLinks.get(navigationLink); HTMLNode sublistItem; if(activePath.equals(navigationPath)) { sublistItem = subnavlist.addChild("li", "class", "submenuitem-selected"); isSelected = true; } else { sublistItem = subnavlist.addChild("li", "class", "submenuitem-not-selected"); } FredPluginL10n l10n = menu.navigationLinkL10n.get(navigationLink); if(l10n == null) l10n = menu.plugin; if(l10n != null) { // From a plugin. Include the plugin name in the id. sublistItem.addAttribute("id", getPluginL10nCSSIdentifier(l10n, navigationTitle)); if(navigationTitle != null) { String newNavigationTitle = l10n.getString(navigationTitle); if(newNavigationTitle == null) { Logger.error(this, "Plugin '"+l10n+"' did return null in getString(key)!"); } else { navigationTitle = newNavigationTitle; } } if(navigationLink != null) { String newNavigationLink = l10n.getString(navigationLink); if(newNavigationLink == null) { Logger.error(this, "Plugin '"+l10n+"' did return null in getString(key)!"); } else { navigationLink = newNavigationLink; } } } else { // Not from a plugin. Add the localization key as id. sublistItem.addAttribute("id", filterCSSIdentifier(navigationTitle)); if(navigationTitle != null) navigationTitle = NodeL10n.getBase().getString(navigationTitle); if(navigationLink != null) navigationLink = NodeL10n.getBase().getString(navigationLink); } if(navigationTitle != null) sublistItem.addChild("a", new String[] { "href", "title" }, new String[] { navigationPath, navigationTitle }, navigationLink); else sublistItem.addChild("a", "href", navigationPath, navigationLink); } if(nonEmpty) { HTMLNode listItem; if(isSelected) { selected = menu; subnavlist.addAttribute("class", "subnavlist-selected"); listItem = new HTMLNode("li", "class", "navlist-selected"); } else { subnavlist.addAttribute("class", "subnavlist"); listItem = new HTMLNode("li", "class", "navlist-not-selected"); } String menuItemTitle = menu.defaultNavigationLinkTitle; String text = menu.navigationLinkText; if(menu.plugin == null) { // Not from a plugin. Add the localization key as id. listItem.addAttribute("id", filterCSSIdentifier(menuItemTitle)); menuItemTitle = NodeL10n.getBase().getString(menuItemTitle); text = NodeL10n.getBase().getString(text); } else { /* * From a plugin. Include the plugin name in the id. * * Note that a plugin could misbehave and fail to register its * menu with proper localization keys. */ listItem.addAttribute("id", getPluginL10nCSSIdentifier(menu.plugin, text)); String newTitle = menu.plugin.getString(menuItemTitle); if(newTitle == null) { Logger.error(this, "Plugin '"+menu.plugin+"' did return null in getString(key)!"); } else { menuItemTitle = newTitle; } String newText = menu.plugin.getString(text); if(newText == null) { Logger.error(this, "Plugin '"+menu.plugin+"' did return null in getString(key)!"); } else { text = newText; } } listItem.addChild("a", new String[] { "href", "title" }, new String[] { menu.defaultNavigationLink, menuItemTitle }, text); listItem.addChild(subnavlist); navbarUl.addChild(listItem); } } } // Some themes want the selected submenu separately. if(selected != null) { HTMLNode div = new HTMLNode("div", "id", "selected-subnavbar"); HTMLNode subnavlist = div.addChild("ul", "id", "selected-subnavbar-list"); boolean nonEmpty = false; for (String navigationLink : fullAccess ? selected.navigationLinkTexts : selected.navigationLinkTextsNonFull) { LinkEnabledCallback cb = selected.navigationLinkCallbacks.get(navigationLink); if(cb != null && !cb.isEnabled(ctx)) continue; nonEmpty = true; String navigationTitle = selected.navigationLinkTitles.get(navigationLink); String navigationPath = selected.navigationLinks.get(navigationLink); HTMLNode sublistItem; if(activePath.equals(navigationPath)) { sublistItem = subnavlist.addChild("li", "class", "submenuitem-selected"); } else { sublistItem = subnavlist.addChild("li", "class", "submenuitem-not-selected"); } FredPluginL10n l10n = selected.navigationLinkL10n.get(navigationLink); if (l10n == null) l10n = selected.plugin; if(l10n != null) { if(navigationTitle != null) navigationTitle = l10n.getString(navigationTitle); if(navigationLink != null) navigationLink = l10n.getString(navigationLink); } else { if(navigationTitle != null) navigationTitle = NodeL10n.getBase().getString(navigationTitle); if(navigationLink != null) navigationLink = NodeL10n.getBase().getString(navigationLink); } if(navigationTitle != null) sublistItem.addChild("a", new String[] { "href", "title" }, new String[] { navigationPath, navigationTitle }, navigationLink); else sublistItem.addChild("a", "href", navigationPath, navigationLink); } if(nonEmpty) pageDiv.addChild(div); } } HTMLNode contentDiv = pageDiv.addChild("div", "id", "content"); return new PageNode(pageNode, headNode, contentDiv); } /** * Create a CSS identifier incorporating both a class name and a localization key. * @param plugin plugin localization instance (used for class name) * @param key localization key * @return valid CSS identifier. */ // TODO: Less-stupid name. public static String getPluginL10nCSSIdentifier(FredPluginL10n plugin, String key) { return filterCSSIdentifier(plugin.getClass().getName()+'-'+key); } /** * Filters a given string so that it will be a valid CSS identifier. It replaces all characters that are not * a dash, underscore, or alphanumeric with an underscore. If the first character is a dash and the second * character is not a letter or underscore, replaces the second character with an underscore. This filter is * overly strict as it does not allow non-ASCII characters or escapes. If the given string is below two * characters in length, it appends underscores until it is not. * @param input string to filter * @return a filtered string guaranteed to be a syntactically valid CSS identifier. * @link http://www.w3.org/TR/CSS21/syndata.html#tokenization * @link http://www.w3.org/TR/CSS21/grammar.html#scanner * @link http://stackoverflow.com/questions/448981/ */ public static String filterCSSIdentifier(String input) { while (input.length() < 2) input = input.concat("_"); return input.replaceFirst("^-[^_a-zA-Z]", "-_").replaceAll("[^-_a-zA-Z0-9]", "_"); } public THEME getTheme() { return this.theme; } public InfoboxNode getInfobox(String header) { return getInfobox(header, null, false); } public InfoboxNode getInfobox(HTMLNode header) { return getInfobox(header, null, false); } public InfoboxNode getInfobox(String category, String header) { return getInfobox(category, header, null, false); } public HTMLNode getInfobox(String category, String header, HTMLNode parent) { return getInfobox(category, header, parent, null, false); } public InfoboxNode getInfobox(String category, HTMLNode header) { return getInfobox(category, header, null, false); } public InfoboxNode getInfobox(String header, String title, boolean isUnique) { if (header == null) throw new NullPointerException(); return getInfobox(new HTMLNode("#", header), title, isUnique); } public InfoboxNode getInfobox(HTMLNode header, String title, boolean isUnique) { if (header == null) throw new NullPointerException(); return getInfobox(null, header, title, isUnique); } public InfoboxNode getInfobox(String category, String header, String title, boolean isUnique) { if (header == null) throw new NullPointerException(); return getInfobox(category, new HTMLNode("#", header), title, isUnique); } /** Create an infobox, attach it to the given parent, and return the content node. */ public HTMLNode getInfobox(String category, String header, HTMLNode parent, String title, boolean isUnique) { InfoboxNode node = getInfobox(category, header, title, isUnique); parent.addChild(node.outer); return node.content; } /** * Returns an infobox with the given style and header. * * @param category * The CSS styles, separated by a space (' ') * @param header * The header HTML node * @return The infobox */ public InfoboxNode getInfobox(String category, HTMLNode header, String title, boolean isUnique) { if (header == null) throw new NullPointerException(); StringBuffer classes = new StringBuffer("infobox"); if(category != null) { classes.append(" "); classes.append(category); } if(title != null && !isUnique) { classes.append(" "); classes.append(title); } HTMLNode infobox = new HTMLNode("div", "class", classes.toString()); if(title != null && isUnique) { infobox.addAttribute("id", title); } infobox.addChild("div", "class", "infobox-header").addChild(header); return new InfoboxNode(infobox, infobox.addChild("div", "class", "infobox-content")); } private HTMLNode getOverrideContent() { return new HTMLNode("link", new String[] { "rel", "href", "type", "media", "title" }, new String[] { "stylesheet", override, "text/css", "screen", "custom" }); } public boolean advancedMode(HTTPRequest req, ToadletContainer container) { return parseMode(req, container) >= MODE_ADVANCED; } /** Call this before getPageNode(), so the menus reflect the advanced mode setting. */ public int parseMode(HTTPRequest req, ToadletContainer container) { int mode = container.isAdvancedModeEnabled() ? MODE_ADVANCED : MODE_SIMPLE; if(req.isParameterSet(MODE_SWITCH_PARAMETER)) { mode = req.getIntParam(MODE_SWITCH_PARAMETER, mode); if(mode == MODE_ADVANCED) container.setAdvancedMode(true); else container.setAdvancedMode(false); } return mode; } /** * Bundles parameters that are used to create the page node. The default for * the render parameters is to include all optional render tasks. Individual * tasks may be enabled or disabled by calling the appropriate methods which * returns a new {@link RenderParameters} object as {@link RenderParameters} * are immutable. * * @see PageMaker#getPageNode(String, ToadletContext, RenderParameters) * @author <a href="mailto:bombe@pterodactylus.net">David ?Bombe? Roden</a> */ public static class RenderParameters { /** Whether to include navigation links in the page. */ private final boolean renderNavigationLinks; /** Whether to include the status bar in the page. */ private final boolean renderStatus; /** Whether to include the mode switch in the page. */ private final boolean renderModeSwitch; /** * Creates default render parameters that include all elements. */ public RenderParameters() { this(true, true, true); } /** * Creates render parameters. * * @param renderNavigationLinks * {@code true} to include navigation links in the page * @param renderStatus * {@code true} to include the status bar in the page * @param renderModeSwitch * {@code true} to include the mode switch in the status bar */ private RenderParameters(boolean renderNavigationLinks, boolean renderStatus, boolean renderModeSwitch) { this.renderNavigationLinks = renderNavigationLinks; this.renderStatus = renderStatus; this.renderModeSwitch = renderModeSwitch; } // // ACCESSORS // /** * Returns whether the navigation links should be included in the page. * * @return {@code true} if the navigation links should be included in * the page, {@code false} otherwise */ public boolean isRenderNavigationLinks() { return renderNavigationLinks; } /** * Returns a new {@link RenderParameters} object that renders the * navigation links according to the given parameter. * * @param renderNavigationLinks * {@code true} to render the navigation links, {@code false} * otherwise * @return A new {@link RenderParameters} object */ public RenderParameters renderNavigationLinks(boolean renderNavigationLinks) { return new RenderParameters(renderNavigationLinks, renderStatus, renderModeSwitch); } /** * Returns whether the status bar should be included in the page. * * @return {@code true} if the status bar should be included in the * page, {@code false} otherwise */ public boolean isRenderStatus() { return renderStatus; } /** * Returns a new {@link RenderParameters} object that renders the status * bar according to the given parameter. * * @param renderStatus * {@code true} to render the status bar, {@code false} * otherwise * @return A new {@link RenderParameters} object */ public RenderParameters renderStatus(boolean renderStatus) { return new RenderParameters(renderNavigationLinks, renderStatus, renderModeSwitch); } /** * Returns whether the mode switch should be included in the page. * * @return {@code true} if the mode switch should be included in the * page, {@code false} otherwise */ public boolean isRenderModeSwitch() { return renderModeSwitch; } /** * Returns a new {@link RenderParameters} object that renders the mode * switch according to the given parameter. * * @param renderModeSwitch * {@code true} to render the mode switch, {@code false} * otherwise * @return A new {@link RenderParameters} object */ public RenderParameters renderModeSwitch(boolean renderModeSwitch) { return new RenderParameters(renderNavigationLinks, renderStatus, renderModeSwitch); } } }