Delete RSParser.
This commit is contained in:
parent
a983544539
commit
ce28c3cd88
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "RSParser"
|
||||
BuildableName = "RSParser"
|
||||
BlueprintName = "RSParser"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "RSParserTests"
|
||||
BuildableName = "RSParserTests"
|
||||
BlueprintName = "RSParserTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "RSParser"
|
||||
BuildableName = "RSParser"
|
||||
BlueprintName = "RSParser"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -1,54 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "RSParserTests"
|
||||
BuildableName = "RSParserTests"
|
||||
BlueprintName = "RSParserTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Brent Simmons
|
||||
|
||||
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, sublicense, 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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,40 +0,0 @@
|
|||
// swift-tools-version:5.10
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "RSParser",
|
||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
||||
products: [
|
||||
.library(
|
||||
name: "RSParser",
|
||||
type: .dynamic,
|
||||
targets: ["RSParser"]),
|
||||
.library(
|
||||
name: "RSParserObjC",
|
||||
type: .dynamic,
|
||||
targets: ["RSParserObjC"]),
|
||||
],
|
||||
dependencies: [
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "RSParser",
|
||||
dependencies: ["RSParserObjC"],
|
||||
path: "Sources/Swift",
|
||||
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
|
||||
),
|
||||
.target(
|
||||
name: "RSParserObjC",
|
||||
dependencies: [],
|
||||
path: "Sources/ObjC",
|
||||
cSettings: [
|
||||
.headerSearchPath("include")
|
||||
]),
|
||||
.testTarget(
|
||||
name: "RSParserTests",
|
||||
dependencies: ["RSParser"],
|
||||
exclude: ["Info.plist"],
|
||||
resources: [.copy("Resources")]),
|
||||
]
|
||||
)
|
|
@ -1,75 +0,0 @@
|
|||
# RSParser
|
||||
|
||||
This framework was developed for [NetNewsWire](https://github.com/brentsimmons/NetNewsWire) and is made available here for developers who just need the parsing code. It has no dependencies that aren’t provided by the system.
|
||||
|
||||
_Update 6 Feb. 2018_: RSParser is now a CocoaPod, with the much-appreciated help of [Silver Fox](https://github.com/dcilia). (We _think_ it worked, anyway. Looked like it did.)
|
||||
|
||||
## What’s inside
|
||||
|
||||
This framework includes parsers for:
|
||||
|
||||
* [RSS](http://cyber.harvard.edu/rss/rss.html), [Atom](https://tools.ietf.org/html/rfc4287), [JSON Feed](https://jsonfeed.org/), and [RSS-in-JSON](https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md)
|
||||
* [OPML](http://dev.opml.org/)
|
||||
* Internet dates
|
||||
* HTML metadata and links
|
||||
* HTML entities
|
||||
|
||||
It also includes Objective-C wrappers for libXML2’s XML SAX and HTML SAX parsers. You can write your own parsers on top of these.
|
||||
|
||||
This framework builds for macOS. It *could* be made to build for iOS also, but I haven’t gotten around to it yet.
|
||||
|
||||
## How to parse feeds
|
||||
|
||||
To get the type of a feed, even with partial data, call `FeedParser.feedType(parserData)`, which will return a `FeedType`.
|
||||
|
||||
To parse a feed, call `FeedParser.parse(parserData)`, which will return a [ParsedFeed](Feeds/ParsedFeed.swift). Also see related structs: `ParsedAuthor`, `ParsedItem`, `ParsedAttachment`, and `ParsedHub`.
|
||||
|
||||
You do *not* need to know the type of feed when calling `FeedParser.parse` — it will figure it out and use the correct concrete parser.
|
||||
|
||||
However, if you do want to use a concrete parser directly, see [RSSInJSONParser](Feeds/JSON/RSSInJSONParser.swift), [JSONFeedParser](Feeds/JSON/JSONFeedParser.swift), [RSSParser](Feeds/XML/RSSParser.swift), and [AtomParser](Feeds/XML/AtomParser.swift).
|
||||
|
||||
(Note: if you want to write a feed reader app, please do! You have my blessing and encouragement. Let me know when it’s shipping so I can check it out.)
|
||||
|
||||
## How to parse OPML
|
||||
|
||||
Call `+[RSOPMLParser parseOPMLWithParserData:error:]`, which returns an `RSOPMLDocument`. See related objects: `RSOPMLItem`, `RSOPMLAttributes`, `RSOPMLFeedSpecifier`, and `RSOPMLError`.
|
||||
|
||||
## How to parse dates
|
||||
|
||||
Call `RSDateWithString` or `RSDateWithBytes` (see `RSDateParser`). These handle the common internet date formats. You don’t need to know which format.
|
||||
|
||||
## How to parse HTML
|
||||
|
||||
To get an array of `<a href=…` links from from an HTML document, call `+[RSHTMLLinkParser htmlLinksWithParserData:]`. It returns an array of `RSHTMLLink`.
|
||||
|
||||
To parse the metadata in an HTML document, call `+[RSHTMLMetadataParser HTMLMetadataWithParserData:]`. It returns an `RSHTMLMetadata` object.
|
||||
|
||||
To write your own HTML parser, see `RSSAXHTMLParser`. The two parsers above can serve as examples.
|
||||
|
||||
## How to parse HTML entities
|
||||
|
||||
When you have a string with things like `—` and `ë` and you want to turn those into the correct characters, call `-[NSString rsparser_stringByDecodingHTMLEntities]`. (See `NSString+RSParser.h`.)
|
||||
|
||||
## How to parse XML
|
||||
|
||||
If you need to parse some XML that isn’t RSS, Atom, or OPML, you can use `RSSAXParser`. Don’t subclass it — instead, create an `RSSAXParserDelegate`. See `RSRSSParser`, `RSAtomParser`, and `RSOPMLParser` as examples.
|
||||
|
||||
### Why use libXML2’s SAX API?
|
||||
|
||||
SAX is kind of a pain because of all the state you have to manage.
|
||||
|
||||
An alternative is to use `NSXMLParser`, which is event-driven like SAX. However, `RSSAXParser` was written to avoid allocating Objective-C objects except when absolutely needed. You’ll note use of things like `memcp` and `strncmp`.
|
||||
|
||||
Normally I avoid this kind of thing *strenuously*. I prefer to work at the highest level possible.
|
||||
|
||||
But my more-than-a-decade of experience parsing XML has led me to this solution, which — last time I checked, which was, admittedly, a few years ago — was not only fastest but also uses the least memory. (The two things are related, of course: creating objects is bad for performance, so this code attempts to do the minimum possible.)
|
||||
|
||||
All that low-level stuff is encapsulated, however. If you just want to parse one of the popular feed formats, see `FeedParser`, which makes it easy and Swift-y.
|
||||
|
||||
## Thread safety
|
||||
|
||||
Everything here is thread-safe.
|
||||
|
||||
Everything’s pretty fast, too, so you probably could just use the main thread/queue. But it’s totally a-okay to use a non-serial background queue.
|
||||
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
//
|
||||
// FeedParser.h
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 7/12/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@class RSParsedFeed;
|
||||
@class RSXMLData;
|
||||
|
||||
|
||||
@protocol FeedParser <NSObject>
|
||||
|
||||
+ (BOOL)canParseFeed:(RSXMLData * _Nonnull)xmlData;
|
||||
|
||||
- (nonnull instancetype)initWithXMLData:(RSXMLData * _Nonnull)xmlData;
|
||||
|
||||
- (nullable RSParsedFeed *)parseFeed:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
|
||||
@end
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// NSData+RSParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/24/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
|
||||
@interface NSData (RSParser)
|
||||
|
||||
- (BOOL)isProbablyHTML;
|
||||
- (BOOL)isProbablyXML;
|
||||
- (BOOL)isProbablyJSON;
|
||||
|
||||
- (BOOL)isProbablyJSONFeed;
|
||||
- (BOOL)isProbablyRSSInJSON;
|
||||
- (BOOL)isProbablyRSS;
|
||||
- (BOOL)isProbablyAtom;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
//
|
||||
// NSData+RSParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/24/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSData+RSParser.h"
|
||||
|
||||
|
||||
|
||||
|
||||
/* TODO: find real-world cases where the isProbably* cases fail when they should succeed, and add them to tests.*/
|
||||
|
||||
static BOOL bytesAreProbablyHTML(const char *bytes, NSUInteger numberOfBytes);
|
||||
static BOOL bytesAreProbablyXML(const char *bytes, NSUInteger numberOfBytes);
|
||||
static BOOL bytesStartWithStringIgnoringWhitespace(const char *string, const char *bytes, NSUInteger numberOfBytes);
|
||||
static BOOL didFindString(const char *string, const char *bytes, NSUInteger numberOfBytes);
|
||||
static BOOL bytesStartWithRSS(const char *bytes, NSUInteger numberOfBytes);
|
||||
static BOOL bytesStartWithRDF(const char *bytes, NSUInteger numberOfBytes);
|
||||
static BOOL bytesStartWithAtom(const char *bytes, NSUInteger numberOfBytes);
|
||||
|
||||
@implementation NSData (RSParser)
|
||||
|
||||
- (BOOL)isProbablyHTML {
|
||||
|
||||
return bytesAreProbablyHTML(self.bytes, self.length);
|
||||
}
|
||||
|
||||
- (BOOL)isProbablyXML {
|
||||
|
||||
return bytesAreProbablyXML(self.bytes, self.length);
|
||||
}
|
||||
|
||||
- (BOOL)isProbablyJSON {
|
||||
|
||||
return bytesStartWithStringIgnoringWhitespace("{", self.bytes, self.length);
|
||||
}
|
||||
|
||||
- (BOOL)isProbablyJSONFeed {
|
||||
|
||||
if (![self isProbablyJSON]) {
|
||||
return NO;
|
||||
}
|
||||
return didFindString("://jsonfeed.org/version/", self.bytes, self.length) || didFindString(":\\/\\/jsonfeed.org\\/version\\/", self.bytes, self.length);
|
||||
}
|
||||
|
||||
- (BOOL)isProbablyRSSInJSON {
|
||||
|
||||
if (![self isProbablyJSON]) {
|
||||
return NO;
|
||||
}
|
||||
const char *bytes = self.bytes;
|
||||
NSUInteger length = self.length;
|
||||
return didFindString("rss", bytes, length) && didFindString("channel", bytes, length) && didFindString("item", bytes, length);
|
||||
}
|
||||
|
||||
- (BOOL)isProbablyRSS {
|
||||
|
||||
if (didFindString("<rss", self.bytes, self.length) || didFindString("<rdf:RDF", self.bytes, self.length)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// At this writing (7 Dec. 2017), https://www.natashatherobot.com/feed/ is missing an opening <rss> tag, but it should be parsed anyway. It does have some other distinct RSS markers we can find.
|
||||
return (didFindString("<channel>", self.bytes, self.length) && didFindString("<pubDate>", self.bytes, self.length));
|
||||
}
|
||||
|
||||
- (BOOL)isProbablyAtom {
|
||||
|
||||
return didFindString("<feed", self.bytes, self.length);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static BOOL didFindString(const char *string, const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
char *foundString = strnstr(bytes, string, numberOfBytes);
|
||||
return foundString != NULL;
|
||||
}
|
||||
|
||||
static BOOL bytesStartWithStringIgnoringWhitespace(const char *string, const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
NSUInteger i = 0;
|
||||
for (i = 0; i < numberOfBytes; i++) {
|
||||
|
||||
const char ch = bytes[i];
|
||||
if (ch == ' ' || ch == '\r' || ch == '\n' || ch == '\t') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == string[0]) {
|
||||
return strnstr(bytes, string, numberOfBytes) == bytes + i;
|
||||
}
|
||||
|
||||
// Allow for a BOM of up to four bytes. ASSUMPTION: BOM will only be at the start of the data.
|
||||
if (i < 4) continue;
|
||||
|
||||
break;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
static BOOL bytesAreProbablyHTML(const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
if (didFindString("<html", bytes, numberOfBytes)) {
|
||||
return YES;
|
||||
}
|
||||
if (didFindString("<HTML", bytes, numberOfBytes)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (didFindString("<body", bytes, numberOfBytes)) {
|
||||
return YES;
|
||||
}
|
||||
if (didFindString("<meta", bytes, numberOfBytes)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (didFindString("<", bytes, numberOfBytes)) {
|
||||
if (didFindString("doctype html", bytes, numberOfBytes)) {
|
||||
return YES;
|
||||
}
|
||||
if (didFindString("DOCTYPE html", bytes, numberOfBytes)) {
|
||||
return YES;
|
||||
}
|
||||
if (didFindString("DOCTYPE HTML", bytes, numberOfBytes)) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
static BOOL bytesAreProbablyXML(const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
return bytesStartWithStringIgnoringWhitespace("<?xml", bytes, numberOfBytes);
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// NSString+RSParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 9/25/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSString (RSParser)
|
||||
|
||||
- (NSString *)rsparser_stringByDecodingHTMLEntities;
|
||||
|
||||
/// Returns a copy of \c self with <, >, and & entity-encoded.
|
||||
@property (readonly, copy) NSString *rsparser_stringByEncodingRequiredEntities;
|
||||
|
||||
- (NSString *)rsparser_md5Hash;
|
||||
|
||||
- (BOOL)rsparser_contains:(NSString *)s;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,348 +0,0 @@
|
|||
//
|
||||
// NSString+RSParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 9/25/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSString+RSParser.h"
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
|
||||
|
||||
|
||||
|
||||
@interface NSScanner (RSParser)
|
||||
|
||||
- (BOOL)rs_scanEntityValue:(NSString * _Nullable * _Nullable)decodedEntity;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation NSString (RSParser)
|
||||
|
||||
- (BOOL)rsparser_contains:(NSString *)s {
|
||||
|
||||
return [self rangeOfString:s].location != NSNotFound;
|
||||
}
|
||||
|
||||
- (NSString *)rsparser_stringByDecodingHTMLEntities {
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
NSScanner *scanner = [[NSScanner alloc] initWithString:self];
|
||||
scanner.charactersToBeSkipped = nil;
|
||||
NSMutableString *result = [[NSMutableString alloc] init];
|
||||
|
||||
while (true) {
|
||||
|
||||
NSString *scannedString = nil;
|
||||
if ([scanner scanUpToString:@"&" intoString:&scannedString]) {
|
||||
[result appendString:scannedString];
|
||||
}
|
||||
if (scanner.isAtEnd) {
|
||||
break;
|
||||
}
|
||||
NSUInteger savedScanLocation = scanner.scanLocation;
|
||||
|
||||
NSString *decodedEntity = nil;
|
||||
if ([scanner rs_scanEntityValue:&decodedEntity]) {
|
||||
[result appendString:decodedEntity];
|
||||
}
|
||||
else {
|
||||
[result appendString:@"&"];
|
||||
scanner.scanLocation = savedScanLocation + 1;
|
||||
}
|
||||
|
||||
if (scanner.isAtEnd) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ([self isEqualToString:result]) {
|
||||
return self;
|
||||
}
|
||||
return [result copy];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static NSDictionary *RSEntitiesDictionary(void);
|
||||
static NSString *RSParserStringWithValue(uint32_t value);
|
||||
|
||||
- (NSString * _Nullable)rs_stringByDecodingEntity {
|
||||
|
||||
// self may or may not have outer & and ; characters.
|
||||
|
||||
NSMutableString *s = [self mutableCopy];
|
||||
|
||||
if ([s hasPrefix:@"&"]) {
|
||||
[s deleteCharactersInRange:NSMakeRange(0, 1)];
|
||||
}
|
||||
if ([s hasSuffix:@";"]) {
|
||||
[s deleteCharactersInRange:NSMakeRange(s.length - 1, 1)];
|
||||
}
|
||||
|
||||
NSDictionary *entitiesDictionary = RSEntitiesDictionary();
|
||||
|
||||
NSString *decodedEntity = entitiesDictionary[self];
|
||||
if (decodedEntity) {
|
||||
return decodedEntity;
|
||||
}
|
||||
|
||||
if ([s hasPrefix:@"#x"] || [s hasPrefix:@"#X"]) { // Hex
|
||||
NSScanner *scanner = [[NSScanner alloc] initWithString:s];
|
||||
scanner.charactersToBeSkipped = [NSCharacterSet characterSetWithCharactersInString:@"#xX"];
|
||||
unsigned int hexValue = 0;
|
||||
if ([scanner scanHexInt:&hexValue]) {
|
||||
return RSParserStringWithValue((uint32_t)hexValue);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
else if ([s hasPrefix:@"#"]) {
|
||||
[s deleteCharactersInRange:NSMakeRange(0, 1)];
|
||||
NSInteger value = s.integerValue;
|
||||
if (value < 1) {
|
||||
return nil;
|
||||
}
|
||||
return RSParserStringWithValue((uint32_t)value);
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)rsparser_stringByEncodingRequiredEntities {
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
|
||||
for (NSUInteger i = 0; i < self.length; ++i) {
|
||||
unichar c = [self characterAtIndex:i];
|
||||
|
||||
switch (c) {
|
||||
case '<':
|
||||
[result appendString:@"<"];
|
||||
break;
|
||||
case '>':
|
||||
[result appendString:@">"];
|
||||
break;
|
||||
case '&':
|
||||
[result appendString:@"&"];
|
||||
break;
|
||||
default:
|
||||
[result appendFormat:@"%C", c];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
- (NSData *)_rsparser_md5HashData {
|
||||
|
||||
NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding];
|
||||
unsigned char hash[CC_MD5_DIGEST_LENGTH];
|
||||
CC_MD5(data.bytes, (CC_LONG)data.length, hash);
|
||||
|
||||
return [NSData dataWithBytes:(const void *)hash length:CC_MD5_DIGEST_LENGTH];
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
- (NSString *)rsparser_md5Hash {
|
||||
|
||||
NSData *md5Data = [self _rsparser_md5HashData];
|
||||
const Byte *bytes = md5Data.bytes;
|
||||
return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSScanner (RSParser)
|
||||
|
||||
- (BOOL)rs_scanEntityValue:(NSString * _Nullable * _Nullable)decodedEntity {
|
||||
|
||||
NSString *s = self.string;
|
||||
NSUInteger initialScanLocation = self.scanLocation;
|
||||
static NSUInteger maxEntityLength = 20; // It’s probably smaller, but this is just for sanity.
|
||||
|
||||
while (true) {
|
||||
|
||||
unichar ch = [s characterAtIndex:self.scanLocation];
|
||||
if ([NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:ch]) {
|
||||
break;
|
||||
}
|
||||
if (ch == ';') {
|
||||
if (!decodedEntity) {
|
||||
return YES;
|
||||
}
|
||||
NSString *rawEntity = [s substringWithRange:NSMakeRange(initialScanLocation + 1, (self.scanLocation - initialScanLocation) - 1)];
|
||||
*decodedEntity = [rawEntity rs_stringByDecodingEntity];
|
||||
self.scanLocation = self.scanLocation + 1;
|
||||
return *decodedEntity != nil;
|
||||
}
|
||||
|
||||
self.scanLocation = self.scanLocation + 1;
|
||||
if (self.scanLocation - initialScanLocation > maxEntityLength) {
|
||||
break;
|
||||
}
|
||||
if (self.isAtEnd) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSString *RSParserStringWithValue(uint32_t value) {
|
||||
// From WebCore's HTMLEntityParser
|
||||
static const uint32_t windowsLatin1ExtensionArray[32] = {
|
||||
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, // 80-87
|
||||
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, // 88-8F
|
||||
0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, // 90-97
|
||||
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178 // 98-9F
|
||||
};
|
||||
|
||||
if ((value & ~0x1Fu) == 0x80u) { // value >= 128 && value < 160
|
||||
value = windowsLatin1ExtensionArray[value - 0x80];
|
||||
}
|
||||
|
||||
value = CFSwapInt32HostToLittle(value);
|
||||
|
||||
return [[NSString alloc] initWithBytes:&value length:sizeof(value) encoding:NSUTF32LittleEndianStringEncoding];
|
||||
}
|
||||
|
||||
static NSDictionary *RSEntitiesDictionary(void) {
|
||||
|
||||
static NSDictionary *entitiesDictionary = nil;
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
|
||||
entitiesDictionary = @{
|
||||
// Named entities
|
||||
@"AElig": @"Æ",
|
||||
@"Aacute": @"Á",
|
||||
@"Acirc": @"Â",
|
||||
@"Agrave": @"À",
|
||||
@"Aring": @"Å",
|
||||
@"Atilde": @"Ã",
|
||||
@"Auml": @"Ä",
|
||||
@"Ccedil": @"Ç",
|
||||
@"Dstrok": @"Ð",
|
||||
@"ETH": @"Ð",
|
||||
@"Eacute": @"É",
|
||||
@"Ecirc": @"Ê",
|
||||
@"Egrave": @"È",
|
||||
@"Euml": @"Ë",
|
||||
@"Iacute": @"Í",
|
||||
@"Icirc": @"Î",
|
||||
@"Igrave": @"Ì",
|
||||
@"Iuml": @"Ï",
|
||||
@"Ntilde": @"Ñ",
|
||||
@"Oacute": @"Ó",
|
||||
@"Ocirc": @"Ô",
|
||||
@"Ograve": @"Ò",
|
||||
@"Oslash": @"Ø",
|
||||
@"Otilde": @"Õ",
|
||||
@"Ouml": @"Ö",
|
||||
@"Pi": @"Π",
|
||||
@"THORN": @"Þ",
|
||||
@"Uacute": @"Ú",
|
||||
@"Ucirc": @"Û",
|
||||
@"Ugrave": @"Ù",
|
||||
@"Uuml": @"Ü",
|
||||
@"Yacute": @"Y",
|
||||
@"aacute": @"á",
|
||||
@"acirc": @"â",
|
||||
@"acute": @"´",
|
||||
@"aelig": @"æ",
|
||||
@"agrave": @"à",
|
||||
@"amp": @"&",
|
||||
@"apos": @"'",
|
||||
@"aring": @"å",
|
||||
@"atilde": @"ã",
|
||||
@"auml": @"ä",
|
||||
@"brkbar": @"¦",
|
||||
@"brvbar": @"¦",
|
||||
@"ccedil": @"ç",
|
||||
@"cedil": @"¸",
|
||||
@"cent": @"¢",
|
||||
@"copy": @"©",
|
||||
@"curren": @"¤",
|
||||
@"deg": @"°",
|
||||
@"die": @"¨",
|
||||
@"divide": @"÷",
|
||||
@"eacute": @"é",
|
||||
@"ecirc": @"ê",
|
||||
@"egrave": @"è",
|
||||
@"eth": @"ð",
|
||||
@"euml": @"ë",
|
||||
@"euro": @"€",
|
||||
@"frac12": @"½",
|
||||
@"frac14": @"¼",
|
||||
@"frac34": @"¾",
|
||||
@"gt": @">",
|
||||
@"hearts": @"♥",
|
||||
@"hellip": @"…",
|
||||
@"iacute": @"í",
|
||||
@"icirc": @"î",
|
||||
@"iexcl": @"¡",
|
||||
@"igrave": @"ì",
|
||||
@"iquest": @"¿",
|
||||
@"iuml": @"ï",
|
||||
@"laquo": @"«",
|
||||
@"ldquo": @"“",
|
||||
@"lsquo": @"‘",
|
||||
@"lt": @"<",
|
||||
@"macr": @"¯",
|
||||
@"mdash": @"—",
|
||||
@"micro": @"µ",
|
||||
@"middot": @"·",
|
||||
@"ndash": @"–",
|
||||
@"not": @"¬",
|
||||
@"ntilde": @"ñ",
|
||||
@"oacute": @"ó",
|
||||
@"ocirc": @"ô",
|
||||
@"ograve": @"ò",
|
||||
@"ordf": @"ª",
|
||||
@"ordm": @"º",
|
||||
@"oslash": @"ø",
|
||||
@"otilde": @"õ",
|
||||
@"ouml": @"ö",
|
||||
@"para": @"¶",
|
||||
@"pi": @"π",
|
||||
@"plusmn": @"±",
|
||||
@"pound": @"£",
|
||||
@"quot": @"\"",
|
||||
@"raquo": @"»",
|
||||
@"rdquo": @"”",
|
||||
@"reg": @"®",
|
||||
@"rsquo": @"’",
|
||||
@"sect": @"§",
|
||||
@"shy": RSParserStringWithValue(173),
|
||||
@"sup1": @"¹",
|
||||
@"sup2": @"²",
|
||||
@"sup3": @"³",
|
||||
@"szlig": @"ß",
|
||||
@"thorn": @"þ",
|
||||
@"times": @"×",
|
||||
@"trade": @"™",
|
||||
@"uacute": @"ú",
|
||||
@"ucirc": @"û",
|
||||
@"ugrave": @"ù",
|
||||
@"uml": @"¨",
|
||||
@"uuml": @"ü",
|
||||
@"yacute": @"y",
|
||||
@"yen": @"¥",
|
||||
@"yuml": @"ÿ",
|
||||
@"infin": @"∞",
|
||||
@"nbsp": RSParserStringWithValue(160)
|
||||
};
|
||||
});
|
||||
|
||||
return entitiesDictionary;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// ParserData.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 10/4/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ParserData : NSObject
|
||||
|
||||
@property (nonatomic, readonly) NSString *url;
|
||||
@property (nonatomic, readonly) NSData *data;
|
||||
|
||||
- (instancetype)initWithURL:(NSString *)url data:(NSData *)data;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// ParserData.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 10/4/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ParserData.h"
|
||||
|
||||
@implementation ParserData
|
||||
|
||||
- (instancetype)initWithURL:(NSString *)url data:(NSData *)data {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_url = url;
|
||||
_data = data;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,18 +0,0 @@
|
|||
//
|
||||
// RSAtomParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 1/15/15.
|
||||
// Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@class ParserData;
|
||||
@class RSParsedFeed;
|
||||
|
||||
@interface RSAtomParser : NSObject
|
||||
|
||||
+ (RSParsedFeed *)parseFeedWithData:(ParserData *)parserData;
|
||||
|
||||
@end
|
|
@ -1,679 +0,0 @@
|
|||
//
|
||||
// RSAtomParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 1/15/15.
|
||||
// Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "RSAtomParser.h"
|
||||
#import "RSSAXParser.h"
|
||||
#import "RSParsedFeed.h"
|
||||
#import "RSParsedArticle.h"
|
||||
#import "NSString+RSParser.h"
|
||||
#import "RSDateParser.h"
|
||||
#import "ParserData.h"
|
||||
#import "RSParsedEnclosure.h"
|
||||
#import "RSParsedAuthor.h"
|
||||
|
||||
#import <libxml/xmlstring.h>
|
||||
|
||||
@interface RSAtomParser () <RSSAXParserDelegate>
|
||||
|
||||
@property (nonatomic) NSData *feedData;
|
||||
@property (nonatomic) NSString *urlString;
|
||||
@property (nonatomic) BOOL endFeedFound;
|
||||
@property (nonatomic) BOOL parsingXHTML;
|
||||
@property (nonatomic) BOOL parsingSource;
|
||||
@property (nonatomic) BOOL parsingArticle;
|
||||
@property (nonatomic) BOOL parsingAuthor;
|
||||
@property (nonatomic) NSMutableArray *attributesStack;
|
||||
@property (nonatomic, readonly) NSDictionary *currentAttributes;
|
||||
@property (nonatomic) NSMutableString *xhtmlString;
|
||||
@property (nonatomic) NSString *link;
|
||||
@property (nonatomic) NSString *title;
|
||||
@property (nonatomic) NSMutableArray *articles;
|
||||
@property (nonatomic) NSDate *dateParsed;
|
||||
@property (nonatomic) RSSAXParser *parser;
|
||||
@property (nonatomic, readonly) RSParsedArticle *currentArticle;
|
||||
@property (nonatomic) RSParsedAuthor *currentAuthor;
|
||||
@property (nonatomic, readonly) NSDate *currentDate;
|
||||
@property (nonatomic) NSString *language;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSAtomParser
|
||||
|
||||
#pragma mark - Class Methods
|
||||
|
||||
+ (RSParsedFeed *)parseFeedWithData:(ParserData *)parserData {
|
||||
|
||||
RSAtomParser *parser = [[[self class] alloc] initWithParserData:parserData];
|
||||
return [parser parseFeed];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithParserData:(ParserData *)parserData {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_feedData = parserData.data;
|
||||
_urlString = parserData.url;
|
||||
_parser = [[RSSAXParser alloc] initWithDelegate:self];
|
||||
_attributesStack = [NSMutableArray new];
|
||||
_articles = [NSMutableArray new];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - API
|
||||
|
||||
- (RSParsedFeed *)parseFeed {
|
||||
|
||||
[self parse];
|
||||
|
||||
RSParsedFeed *parsedFeed = [[RSParsedFeed alloc] initWithURLString:self.urlString title:self.title link:self.link language:self.language articles:self.articles];
|
||||
|
||||
return parsedFeed;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Constants
|
||||
|
||||
static NSString *kTypeKey = @"type";
|
||||
static NSString *kXHTMLType = @"xhtml";
|
||||
static NSString *kRelKey = @"rel";
|
||||
static NSString *kAlternateValue = @"alternate";
|
||||
static NSString *kHrefKey = @"href";
|
||||
static NSString *kXMLKey = @"xml";
|
||||
static NSString *kBaseKey = @"base";
|
||||
static NSString *kLangKey = @"lang";
|
||||
static NSString *kXMLBaseKey = @"xml:base";
|
||||
static NSString *kXMLLangKey = @"xml:lang";
|
||||
static NSString *kTextHTMLValue = @"text/html";
|
||||
static NSString *kRelatedValue = @"related";
|
||||
static NSString *kEnclosureValue = @"enclosure";
|
||||
static NSString *kShortURLValue = @"shorturl";
|
||||
static NSString *kHTMLValue = @"html";
|
||||
static NSString *kEnValue = @"en";
|
||||
static NSString *kTextValue = @"text";
|
||||
static NSString *kSelfValue = @"self";
|
||||
static NSString *kLengthKey = @"length";
|
||||
static NSString *kTitleKey = @"title";
|
||||
|
||||
static const char *kID = "id";
|
||||
static const NSInteger kIDLength = 3;
|
||||
|
||||
static const char *kTitle = "title";
|
||||
static const NSInteger kTitleLength = 6;
|
||||
|
||||
static const char *kContent = "content";
|
||||
static const NSInteger kContentLength = 8;
|
||||
|
||||
static const char *kSummary = "summary";
|
||||
static const NSInteger kSummaryLength = 8;
|
||||
|
||||
static const char *kLink = "link";
|
||||
static const NSInteger kLinkLength = 5;
|
||||
|
||||
static const char *kPublished = "published";
|
||||
static const NSInteger kPublishedLength = 10;
|
||||
|
||||
static const char *kIssued = "issued";
|
||||
static const NSInteger kIssuedLength = 7;
|
||||
|
||||
static const char *kUpdated = "updated";
|
||||
static const NSInteger kUpdatedLength = 8;
|
||||
|
||||
static const char *kModified = "modified";
|
||||
static const NSInteger kModifiedLength = 9;
|
||||
|
||||
static const char *kAuthor = "author";
|
||||
static const NSInteger kAuthorLength = 7;
|
||||
|
||||
static const char *kName = "name";
|
||||
static const NSInteger kNameLength = 5;
|
||||
|
||||
static const char *kEmail = "email";
|
||||
static const NSInteger kEmailLength = 6;
|
||||
|
||||
static const char *kURI = "uri";
|
||||
static const NSInteger kURILength = 4;
|
||||
|
||||
static const char *kEntry = "entry";
|
||||
static const NSInteger kEntryLength = 6;
|
||||
|
||||
static const char *kSource = "source";
|
||||
static const NSInteger kSourceLength = 7;
|
||||
|
||||
static const char *kFeed = "feed";
|
||||
static const NSInteger kFeedLength = 5;
|
||||
|
||||
static const char *kType = "type";
|
||||
static const NSInteger kTypeLength = 5;
|
||||
|
||||
static const char *kRel = "rel";
|
||||
static const NSInteger kRelLength = 4;
|
||||
|
||||
static const char *kAlternate = "alternate";
|
||||
static const NSInteger kAlternateLength = 10;
|
||||
|
||||
static const char *kHref = "href";
|
||||
static const NSInteger kHrefLength = 5;
|
||||
|
||||
static const char *kXML = "xml";
|
||||
static const NSInteger kXMLLength = 4;
|
||||
|
||||
static const char *kBase = "base";
|
||||
static const NSInteger kBaseLength = 5;
|
||||
|
||||
static const char *kLang = "lang";
|
||||
static const NSInteger kLangLength = 5;
|
||||
|
||||
static const char *kTextHTML = "text/html";
|
||||
static const NSInteger kTextHTMLLength = 10;
|
||||
|
||||
static const char *kRelated = "related";
|
||||
static const NSInteger kRelatedLength = 8;
|
||||
|
||||
static const char *kShortURL = "shorturl";
|
||||
static const NSInteger kShortURLLength = 9;
|
||||
|
||||
static const char *kHTML = "html";
|
||||
static const NSInteger kHTMLLength = 5;
|
||||
|
||||
static const char *kEn = "en";
|
||||
static const NSInteger kEnLength = 3;
|
||||
|
||||
static const char *kText = "text";
|
||||
static const NSInteger kTextLength = 5;
|
||||
|
||||
static const char *kSelf = "self";
|
||||
static const NSInteger kSelfLength = 5;
|
||||
|
||||
static const char *kEnclosure = "enclosure";
|
||||
static const NSInteger kEnclosureLength = 10;
|
||||
|
||||
static const char *kLength = "length";
|
||||
static const NSInteger kLengthLength = 7;
|
||||
|
||||
#pragma mark - Parsing
|
||||
|
||||
- (void)parse {
|
||||
|
||||
self.dateParsed = [NSDate date];
|
||||
|
||||
@autoreleasepool {
|
||||
[self.parser parseData:self.feedData];
|
||||
[self.parser finishParsing];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)addArticle {
|
||||
|
||||
RSParsedArticle *article = [[RSParsedArticle alloc] initWithFeedURL:self.urlString];
|
||||
article.dateParsed = self.dateParsed;
|
||||
|
||||
[self.articles addObject:article];
|
||||
}
|
||||
|
||||
|
||||
- (RSParsedArticle *)currentArticle {
|
||||
|
||||
return self.articles.lastObject;
|
||||
}
|
||||
|
||||
|
||||
- (NSDictionary *)currentAttributes {
|
||||
|
||||
return self.attributesStack.lastObject;
|
||||
}
|
||||
|
||||
|
||||
- (NSDate *)currentDate {
|
||||
|
||||
return RSDateWithBytes(self.parser.currentCharacters.bytes, self.parser.currentCharacters.length);
|
||||
}
|
||||
|
||||
|
||||
- (void)addFeedLink {
|
||||
|
||||
if (self.link && self.link.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *related = self.currentAttributes[kRelKey];
|
||||
if (related == kAlternateValue) {
|
||||
self.link = self.currentAttributes[kHrefKey];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)addFeedTitle {
|
||||
|
||||
if (self.title.length < 1) {
|
||||
self.title = [self currentString];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addFeedLanguage {
|
||||
|
||||
if (self.language.length < 0) {
|
||||
self.language = self.currentAttributes[kXMLLangKey]
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addLink {
|
||||
|
||||
NSDictionary *attributes = self.currentAttributes;
|
||||
|
||||
NSString *urlString = attributes[kHrefKey];
|
||||
if (urlString.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
RSParsedArticle *article = self.currentArticle;
|
||||
|
||||
NSString *rel = attributes[kRelKey];
|
||||
if (rel.length < 1) {
|
||||
rel = kAlternateValue;
|
||||
}
|
||||
|
||||
if (rel == kRelatedValue) {
|
||||
if (!article.link) {
|
||||
article.link = urlString;
|
||||
}
|
||||
}
|
||||
else if (rel == kAlternateValue) {
|
||||
if (!article.permalink) {
|
||||
article.permalink = urlString;
|
||||
}
|
||||
}
|
||||
else if (rel == kEnclosureValue) {
|
||||
RSParsedEnclosure *enclosure = [self enclosureWithURLString:urlString attributes:attributes];
|
||||
[article addEnclosure:enclosure];
|
||||
}
|
||||
}
|
||||
|
||||
- (RSParsedEnclosure *)enclosureWithURLString:(NSString *)urlString attributes:(NSDictionary *)attributes {
|
||||
|
||||
RSParsedEnclosure *enclosure = [[RSParsedEnclosure alloc] init];
|
||||
enclosure.url = urlString;
|
||||
enclosure.title = attributes[kTitleKey];
|
||||
enclosure.mimeType = attributes[kTypeKey];
|
||||
enclosure.length = [attributes[kLengthKey] integerValue];
|
||||
|
||||
return enclosure;
|
||||
}
|
||||
|
||||
- (void)addContent {
|
||||
|
||||
self.currentArticle.body = [self currentString];
|
||||
}
|
||||
|
||||
|
||||
- (void)addSummary {
|
||||
|
||||
if (!self.currentArticle.body) {
|
||||
self.currentArticle.body = [self currentString];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)currentString {
|
||||
|
||||
return self.parser.currentStringWithTrimmedWhitespace;
|
||||
}
|
||||
|
||||
|
||||
- (void)addArticleElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix {
|
||||
|
||||
if (prefix) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kID, kIDLength)) {
|
||||
self.currentArticle.guid = [self currentString];
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) {
|
||||
self.currentArticle.title = [self currentString];
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kContent, kContentLength)) {
|
||||
[self addContent];
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kSummary, kSummaryLength)) {
|
||||
[self addSummary];
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kLink, kLinkLength)) {
|
||||
[self addLink];
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kPublished, kPublishedLength)) {
|
||||
self.currentArticle.datePublished = self.currentDate;
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kUpdated, kUpdatedLength)) {
|
||||
self.currentArticle.dateModified = self.currentDate;
|
||||
}
|
||||
|
||||
// Atom 0.3 dates
|
||||
else if (RSSAXEqualTags(localName, kIssued, kIssuedLength)) {
|
||||
if (!self.currentArticle.datePublished) {
|
||||
self.currentArticle.datePublished = self.currentDate;
|
||||
}
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kModified, kModifiedLength)) {
|
||||
if (!self.currentArticle.dateModified) {
|
||||
self.currentArticle.dateModified = self.currentDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)addXHTMLTag:(const xmlChar *)localName {
|
||||
|
||||
if (!localName) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self.xhtmlString appendString:@"<"];
|
||||
[self.xhtmlString appendString:[NSString stringWithUTF8String:(const char *)localName]];
|
||||
|
||||
if (self.currentAttributes.count < 1) {
|
||||
[self.xhtmlString appendString:@">"];
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *oneKey in self.currentAttributes) {
|
||||
|
||||
[self.xhtmlString appendString:@" "];
|
||||
|
||||
NSString *oneValue = self.currentAttributes[oneKey];
|
||||
[self.xhtmlString appendString:oneKey];
|
||||
|
||||
[self.xhtmlString appendString:@"=\""];
|
||||
|
||||
oneValue = [oneValue stringByReplacingOccurrencesOfString:@"\"" withString:@"""];
|
||||
[self.xhtmlString appendString:oneValue];
|
||||
|
||||
[self.xhtmlString appendString:@"\""];
|
||||
}
|
||||
|
||||
[self.xhtmlString appendString:@">"];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - RSSAXParserDelegate
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes {
|
||||
|
||||
if (self.endFeedFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *xmlAttributes = [self.parser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
|
||||
if (!xmlAttributes) {
|
||||
xmlAttributes = [NSDictionary dictionary];
|
||||
}
|
||||
[self.attributesStack addObject:xmlAttributes];
|
||||
|
||||
if (self.parsingXHTML) {
|
||||
[self addXHTMLTag:localName];
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kEntry, kEntryLength)) {
|
||||
self.parsingArticle = YES;
|
||||
[self addArticle];
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) {
|
||||
self.parsingAuthor = YES;
|
||||
self.currentAuthor = [[RSParsedAuthor alloc] init];
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kSource, kSourceLength)) {
|
||||
self.parsingSource = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isContentTag = RSSAXEqualTags(localName, kContent, kContentLength);
|
||||
BOOL isSummaryTag = RSSAXEqualTags(localName, kSummary, kSummaryLength);
|
||||
if (self.parsingArticle && (isContentTag || isSummaryTag)) {
|
||||
|
||||
if (isContentTag) {
|
||||
self.currentArticle.language = xmlAttributes[kXMLLangKey];
|
||||
}
|
||||
|
||||
NSString *contentType = xmlAttributes[kTypeKey];
|
||||
if ([contentType isEqualToString:kXHTMLType]) {
|
||||
self.parsingXHTML = YES;
|
||||
self.xhtmlString = [NSMutableString stringWithString:@""];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!self.parsingArticle && RSSAXEqualTags(localName, kLink, kLinkLength)) {
|
||||
[self addFeedLink];
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kFeed, kFeedLength)) {
|
||||
[self addFeedLanguage];
|
||||
}
|
||||
|
||||
[self.parser beginStoringCharacters];
|
||||
}
|
||||
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri {
|
||||
|
||||
if (RSSAXEqualTags(localName, kFeed, kFeedLength)) {
|
||||
self.endFeedFound = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.endFeedFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.parsingXHTML) {
|
||||
|
||||
BOOL isContentTag = RSSAXEqualTags(localName, kContent, kContentLength);
|
||||
BOOL isSummaryTag = RSSAXEqualTags(localName, kSummary, kSummaryLength);
|
||||
|
||||
if (self.parsingArticle && (isContentTag || isSummaryTag)) {
|
||||
|
||||
if (isContentTag) {
|
||||
self.currentArticle.body = [self.xhtmlString copy];
|
||||
}
|
||||
|
||||
else if (isSummaryTag) {
|
||||
if (self.currentArticle.body.length < 1) {
|
||||
self.currentArticle.body = [self.xhtmlString copy];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isContentTag || isSummaryTag) {
|
||||
self.parsingXHTML = NO;
|
||||
}
|
||||
|
||||
[self.xhtmlString appendString:@"</"];
|
||||
[self.xhtmlString appendString:[NSString stringWithUTF8String:(const char *)localName]];
|
||||
[self.xhtmlString appendString:@">"];
|
||||
}
|
||||
|
||||
else if (self.parsingAuthor) {
|
||||
|
||||
if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) {
|
||||
self.parsingAuthor = NO;
|
||||
RSParsedAuthor *author = self.currentAuthor;
|
||||
if (author.name || author.emailAddress || author.url) {
|
||||
[self.currentArticle addAuthor:author];
|
||||
}
|
||||
self.currentAuthor = nil;
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kName, kNameLength)) {
|
||||
self.currentAuthor.name = [self currentString];
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kEmail, kEmailLength)) {
|
||||
self.currentAuthor.emailAddress = [self currentString];
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kURI, kURILength)) {
|
||||
self.currentAuthor.url = [self currentString];
|
||||
}
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kEntry, kEntryLength)) {
|
||||
self.parsingArticle = NO;
|
||||
}
|
||||
|
||||
else if (self.parsingArticle && !self.parsingSource) {
|
||||
[self addArticleElement:localName prefix:prefix];
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kSource, kSourceLength)) {
|
||||
self.parsingSource = NO;
|
||||
}
|
||||
|
||||
else if (!self.parsingArticle && !self.parsingSource && RSSAXEqualTags(localName, kTitle, kTitleLength)) {
|
||||
[self addFeedTitle];
|
||||
}
|
||||
|
||||
[self.attributesStack removeLastObject];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix {
|
||||
|
||||
if (prefix && RSSAXEqualTags(prefix, kXML, kXMLLength)) {
|
||||
|
||||
if (RSSAXEqualTags(name, kBase, kBaseLength)) {
|
||||
return kXMLBaseKey;
|
||||
}
|
||||
if (RSSAXEqualTags(name, kLang, kLangLength)) {
|
||||
return kXMLLangKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kRel, kRelLength)) {
|
||||
return kRelKey;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kType, kTypeLength)) {
|
||||
return kTypeKey;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kHref, kHrefLength)) {
|
||||
return kHrefKey;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kAlternate, kAlternateLength)) {
|
||||
return kAlternateValue;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kLength, kLengthLength)) {
|
||||
return kLengthKey;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kTitle, kTitleLength)) {
|
||||
return kTitleKey;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
|
||||
|
||||
return memcmp(bytes1, bytes2, length) == 0;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
|
||||
|
||||
static const NSUInteger alternateLength = kAlternateLength - 1;
|
||||
static const NSUInteger textHTMLLength = kTextHTMLLength - 1;
|
||||
static const NSUInteger relatedLength = kRelatedLength - 1;
|
||||
static const NSUInteger shortURLLength = kShortURLLength - 1;
|
||||
static const NSUInteger htmlLength = kHTMLLength - 1;
|
||||
static const NSUInteger enLength = kEnLength - 1;
|
||||
static const NSUInteger textLength = kTextLength - 1;
|
||||
static const NSUInteger selfLength = kSelfLength - 1;
|
||||
static const NSUInteger enclosureLength = kEnclosureLength - 1;
|
||||
|
||||
if (length == alternateLength && equalBytes(bytes, kAlternate, alternateLength)) {
|
||||
return kAlternateValue;
|
||||
}
|
||||
|
||||
if (length == enclosureLength && equalBytes(bytes, kEnclosure, enclosureLength)) {
|
||||
return kEnclosureValue;
|
||||
}
|
||||
|
||||
if (length == textHTMLLength && equalBytes(bytes, kTextHTML, textHTMLLength)) {
|
||||
return kTextHTMLValue;
|
||||
}
|
||||
|
||||
if (length == relatedLength && equalBytes(bytes, kRelated, relatedLength)) {
|
||||
return kRelatedValue;
|
||||
}
|
||||
|
||||
if (length == shortURLLength && equalBytes(bytes, kShortURL, shortURLLength)) {
|
||||
return kShortURLValue;
|
||||
}
|
||||
|
||||
if (length == htmlLength && equalBytes(bytes, kHTML, htmlLength)) {
|
||||
return kHTMLValue;
|
||||
}
|
||||
|
||||
if (length == enLength && equalBytes(bytes, kEn, enLength)) {
|
||||
return kEnValue;
|
||||
}
|
||||
|
||||
if (length == textLength && equalBytes(bytes, kText, textLength)) {
|
||||
return kTextValue;
|
||||
}
|
||||
|
||||
if (length == selfLength && equalBytes(bytes, kSelf, selfLength)) {
|
||||
return kSelfValue;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLCharactersFound:(const unsigned char *)characters length:(NSUInteger)length {
|
||||
|
||||
if (self.parsingXHTML) {
|
||||
NSString *s = [[NSString alloc] initWithBytesNoCopy:(void *)characters length:length encoding:NSUTF8StringEncoding freeWhenDone:NO];
|
||||
if (s == nil) {
|
||||
return;
|
||||
}
|
||||
// libxml decodes all entities; we need to re-encode certain characters
|
||||
// (<, >, and &) when inside XHTML text content.
|
||||
[self.xhtmlString appendString:s.rsparser_stringByEncodingRequiredEntities];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// RSDateParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/25/15.
|
||||
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
|
||||
// Common web dates -- RFC 822 and 8601 -- are handled here: the formats you find in JSON and XML feeds.
|
||||
// These may return nil. They may also return garbage, given bad input.
|
||||
|
||||
NSDate *RSDateWithString(NSString *dateString);
|
||||
|
||||
// If you're using a SAX parser, you have the bytes and don't need to convert to a string first.
|
||||
// It's faster and uses less memory.
|
||||
// (Assumes bytes are UTF-8 or ASCII. If you're using the libxml SAX parser, this will work.)
|
||||
|
||||
NSDate *RSDateWithBytes(const char *bytes, NSUInteger numberOfBytes);
|
||||
|
|
@ -1,461 +0,0 @@
|
|||
//
|
||||
// RSDateParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/25/15.
|
||||
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "RSDateParser.h"
|
||||
#import <time.h>
|
||||
|
||||
|
||||
typedef struct {
|
||||
const char *abbreviation;
|
||||
const NSInteger offsetHours;
|
||||
const NSInteger offsetMinutes;
|
||||
} RSTimeZoneAbbreviationAndOffset;
|
||||
|
||||
|
||||
#define kNumberOfTimeZones 96
|
||||
|
||||
static const RSTimeZoneAbbreviationAndOffset timeZoneTable[kNumberOfTimeZones] = {
|
||||
{"GMT", 0, 0}, //Most common at top, for performance
|
||||
{"PDT", -7, 0}, {"PST", -8, 0}, {"EST", -5, 0}, {"EDT", -4, 0},
|
||||
{"MDT", -6, 0}, {"MST", -7, 0}, {"CST", -6, 0}, {"CDT", -5, 0},
|
||||
{"ACT", -8, 0}, {"AFT", 4, 30}, {"AMT", 4, 0}, {"ART", -3, 0},
|
||||
{"AST", 3, 0}, {"AZT", 4, 0}, {"BIT", -12, 0}, {"BDT", 8, 0},
|
||||
{"ACST", 9, 30}, {"AEST", 10, 0}, {"AKST", -9, 0}, {"AMST", 5, 0},
|
||||
{"AWST", 8, 0}, {"AZOST", -1, 0}, {"BIOT", 6, 0}, {"BRT", -3, 0},
|
||||
{"BST", 6, 0}, {"BTT", 6, 0}, {"CAT", 2, 0}, {"CCT", 6, 30},
|
||||
{"CET", 1, 0}, {"CEST", 2, 0}, {"CHAST", 12, 45}, {"ChST", 10, 0},
|
||||
{"CIST", -8, 0}, {"CKT", -10, 0}, {"CLT", -4, 0}, {"CLST", -3, 0},
|
||||
{"COT", -5, 0}, {"COST", -4, 0}, {"CVT", -1, 0}, {"CXT", 7, 0},
|
||||
{"EAST", -6, 0}, {"EAT", 3, 0}, {"ECT", -4, 0}, {"EEST", 3, 0},
|
||||
{"EET", 2, 0}, {"FJT", 12, 0}, {"FKST", -4, 0}, {"GALT", -6, 0},
|
||||
{"GET", 4, 0}, {"GFT", -3, 0}, {"GILT", 7, 0}, {"GIT", -9, 0},
|
||||
{"GST", -2, 0}, {"GYT", -4, 0}, {"HAST", -10, 0}, {"HKT", 8, 0},
|
||||
{"HMT", 5, 0}, {"IRKT", 8, 0}, {"IRST", 3, 30}, {"IST", 2, 0},
|
||||
{"JST", 9, 0}, {"KRAT", 7, 0}, {"KST", 9, 0}, {"LHST", 10, 30},
|
||||
{"LINT", 14, 0}, {"MAGT", 11, 0}, {"MIT", -9, 30}, {"MSK", 3, 0},
|
||||
{"MUT", 4, 0}, {"NDT", -2, 30}, {"NFT", 11, 30}, {"NPT", 5, 45},
|
||||
{"NT", -3, 30}, {"OMST", 6, 0}, {"PETT", 12, 0}, {"PHOT", 13, 0},
|
||||
{"PKT", 5, 0}, {"RET", 4, 0}, {"SAMT", 4, 0}, {"SAST", 2, 0},
|
||||
{"SBT", 11, 0}, {"SCT", 4, 0}, {"SLT", 5, 30}, {"SST", 8, 0},
|
||||
{"TAHT", -10, 0}, {"THA", 7, 0}, {"UYT", -3, 0}, {"UYST", -2, 0},
|
||||
{"VET", -4, 30}, {"VLAT", 10, 0}, {"WAT", 1, 0}, {"WET", 0, 0},
|
||||
{"WEST", 1, 0}, {"YAKT", 9, 0}, {"YEKT", 5, 0}
|
||||
}; /*See http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations for list*/
|
||||
|
||||
|
||||
|
||||
#pragma mark - Parser
|
||||
|
||||
enum {
|
||||
RSJanuary = 1,
|
||||
RSFebruary,
|
||||
RSMarch,
|
||||
RSApril,
|
||||
RSMay,
|
||||
RSJune,
|
||||
RSJuly,
|
||||
RSAugust,
|
||||
RSSeptember,
|
||||
RSOctober,
|
||||
RSNovember,
|
||||
RSDecember
|
||||
};
|
||||
|
||||
static NSInteger nextMonthValue(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex, NSUInteger *finalIndex) {
|
||||
|
||||
/*Months are 1-based -- January is 1, Dec is 12.
|
||||
Lots of short-circuits here. Not strict. GIGO.*/
|
||||
|
||||
NSUInteger i;// = startingIndex;
|
||||
NSUInteger numberOfAlphaCharactersFound = 0;
|
||||
char monthCharacters[3] = {0, 0, 0};
|
||||
|
||||
for (i = startingIndex; i < numberOfBytes; i++) {
|
||||
|
||||
*finalIndex = i;
|
||||
char character = bytes[i];
|
||||
|
||||
BOOL isAlphaCharacter = (BOOL)isalpha(character);
|
||||
if (!isAlphaCharacter && numberOfAlphaCharactersFound < 1)
|
||||
continue;
|
||||
if (!isAlphaCharacter && numberOfAlphaCharactersFound > 0)
|
||||
break;
|
||||
|
||||
numberOfAlphaCharactersFound++;
|
||||
if (numberOfAlphaCharactersFound == 1) {
|
||||
if (character == 'F' || character == 'f')
|
||||
return RSFebruary;
|
||||
if (character == 'S' || character == 's')
|
||||
return RSSeptember;
|
||||
if (character == 'O' || character == 'o')
|
||||
return RSOctober;
|
||||
if (character == 'N' || character == 'n')
|
||||
return RSNovember;
|
||||
if (character == 'D' || character == 'd')
|
||||
return RSDecember;
|
||||
}
|
||||
|
||||
monthCharacters[numberOfAlphaCharactersFound - 1] = character;
|
||||
if (numberOfAlphaCharactersFound >=3)
|
||||
break;
|
||||
}
|
||||
|
||||
if (numberOfAlphaCharactersFound < 2)
|
||||
return NSNotFound;
|
||||
|
||||
if (monthCharacters[0] == 'J' || monthCharacters[0] == 'j') { //Jan, Jun, Jul
|
||||
if (monthCharacters[1] == 'a' || monthCharacters[1] == 'A')
|
||||
return RSJanuary;
|
||||
if (monthCharacters[1] == 'u' || monthCharacters[1] == 'U') {
|
||||
if (monthCharacters[2] == 'n' || monthCharacters[2] == 'N')
|
||||
return RSJune;
|
||||
return RSJuly;
|
||||
}
|
||||
return RSJanuary;
|
||||
}
|
||||
|
||||
if (monthCharacters[0] == 'M' || monthCharacters[0] == 'm') { //March, May
|
||||
if (monthCharacters[2] == 'y' || monthCharacters[2] == 'Y')
|
||||
return RSMay;
|
||||
return RSMarch;
|
||||
}
|
||||
|
||||
if (monthCharacters[0] == 'A' || monthCharacters[0] == 'a') { //April, August
|
||||
if (monthCharacters[1] == 'u' || monthCharacters[1] == 'U')
|
||||
return RSAugust;
|
||||
return RSApril;
|
||||
}
|
||||
|
||||
return RSJanuary; //should never get here
|
||||
}
|
||||
|
||||
|
||||
static NSInteger nextNumericValue(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex, NSUInteger maximumNumberOfDigits, NSUInteger *finalIndex) {
|
||||
|
||||
/*maximumNumberOfDigits has a maximum limit of 4 (for time zone offsets and years).
|
||||
*finalIndex will be the index of the last character looked at.*/
|
||||
|
||||
if (maximumNumberOfDigits > 4)
|
||||
maximumNumberOfDigits = 4;
|
||||
|
||||
NSUInteger i = 0;
|
||||
NSUInteger numberOfDigitsFound = 0;
|
||||
NSInteger digits[4] = {0, 0, 0, 0};
|
||||
|
||||
for (i = startingIndex; i < numberOfBytes; i++) {
|
||||
*finalIndex = i;
|
||||
BOOL isDigit = (BOOL)isdigit(bytes[i]);
|
||||
if (!isDigit && numberOfDigitsFound < 1)
|
||||
continue;
|
||||
if (!isDigit && numberOfDigitsFound > 0)
|
||||
break;
|
||||
digits[numberOfDigitsFound] = bytes[i] - 48; // '0' is 48
|
||||
numberOfDigitsFound++;
|
||||
if (numberOfDigitsFound >= maximumNumberOfDigits)
|
||||
break;
|
||||
}
|
||||
|
||||
if (numberOfDigitsFound < 1)
|
||||
return NSNotFound;
|
||||
if (numberOfDigitsFound == 1)
|
||||
return digits[0];
|
||||
if (numberOfDigitsFound == 2)
|
||||
return (digits[0] * 10) + digits[1];
|
||||
if (numberOfDigitsFound == 3)
|
||||
return (digits[0] * 100) + (digits[1] * 10) + digits[2];
|
||||
return (digits[0] * 1000) + (digits[1] * 100) + (digits[2] * 10) + digits[3];
|
||||
}
|
||||
|
||||
|
||||
static BOOL hasAtLeastOneAlphaCharacter(const char *s) {
|
||||
|
||||
NSUInteger length = strlen(s);
|
||||
NSUInteger i = 0;
|
||||
|
||||
for (i = 0; i < length; i++) {
|
||||
if (isalpha(s[i]))
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Time Zones and offsets
|
||||
|
||||
static NSInteger offsetInSecondsForTimeZoneAbbreviation(const char *abbreviation) {
|
||||
|
||||
/*Linear search should be fine. It's a C array, and short (under 100 items).
|
||||
Most common time zones are at the beginning of the array. (We can tweak this as needed.)*/
|
||||
|
||||
NSUInteger i;
|
||||
|
||||
for (i = 0; i < kNumberOfTimeZones; i++) {
|
||||
|
||||
RSTimeZoneAbbreviationAndOffset zone = timeZoneTable[i];
|
||||
if (strcmp(abbreviation, zone.abbreviation) == 0) {
|
||||
if (zone.offsetHours < 0)
|
||||
return (zone.offsetHours * 60 * 60) - (zone.offsetMinutes * 60);
|
||||
return (zone.offsetHours * 60 * 60) + (zone.offsetMinutes * 60);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static NSInteger offsetInSecondsForOffsetCharacters(const char *timeZoneCharacters) {
|
||||
|
||||
BOOL isPlus = timeZoneCharacters[0] == '+';
|
||||
NSUInteger finalIndex = 0;
|
||||
NSInteger hours = nextNumericValue(timeZoneCharacters, strlen(timeZoneCharacters), 0, 2, &finalIndex);
|
||||
NSInteger minutes = nextNumericValue(timeZoneCharacters, strlen(timeZoneCharacters), finalIndex + 1, 2, &finalIndex);
|
||||
|
||||
if (hours == NSNotFound)
|
||||
hours = 0;
|
||||
if (minutes == NSNotFound)
|
||||
minutes = 0;
|
||||
if (hours == 0 && minutes == 0)
|
||||
return 0;
|
||||
|
||||
NSInteger seconds = (hours * 60 * 60) + (minutes * 60);
|
||||
if (!isPlus)
|
||||
seconds = 0 - seconds;
|
||||
return seconds;
|
||||
}
|
||||
|
||||
|
||||
static const char *rs_GMT = "GMT";
|
||||
static const char *rs_UTC = "UTC";
|
||||
|
||||
static NSInteger parsedTimeZoneOffset(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex) {
|
||||
|
||||
/*Examples: GMT Z +0000 -0000 +07:00 -0700 PDT EST
|
||||
Parse into char[5] -- drop any colon characters. If numeric, calculate seconds from GMT.
|
||||
If alpha, special-case GMT and Z, otherwise look up in time zone list to get offset.*/
|
||||
|
||||
char timeZoneCharacters[6] = {0, 0, 0, 0, 0, 0}; //nil-terminated last character
|
||||
NSUInteger i = 0;
|
||||
NSUInteger numberOfCharactersFound = 0;
|
||||
|
||||
for (i = startingIndex; i < numberOfBytes; i++) {
|
||||
char ch = bytes[i];
|
||||
if (ch == ':' || ch == ' ')
|
||||
continue;
|
||||
if (isdigit(ch) || isalpha(ch) || ch == '+' || ch == '-') {
|
||||
numberOfCharactersFound++;
|
||||
timeZoneCharacters[numberOfCharactersFound - 1] = ch;
|
||||
}
|
||||
if (numberOfCharactersFound >= 5)
|
||||
break;
|
||||
}
|
||||
|
||||
if (numberOfCharactersFound < 1 || timeZoneCharacters[0] == 'Z' || timeZoneCharacters[0] == 'z')
|
||||
return 0;
|
||||
if (strcasestr(timeZoneCharacters, rs_GMT) != nil || strcasestr(timeZoneCharacters, rs_UTC))
|
||||
return 0;
|
||||
|
||||
if (hasAtLeastOneAlphaCharacter(timeZoneCharacters))
|
||||
return offsetInSecondsForTimeZoneAbbreviation(timeZoneCharacters);
|
||||
return offsetInSecondsForOffsetCharacters(timeZoneCharacters);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Date Creation
|
||||
|
||||
static NSDate *dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(NSInteger year, NSInteger month, NSInteger day, NSInteger hour, NSInteger minute, NSInteger second, NSInteger milliseconds, NSInteger timeZoneOffset) {
|
||||
|
||||
struct tm timeInfo;
|
||||
timeInfo.tm_sec = (int)second;
|
||||
timeInfo.tm_min = (int)minute;
|
||||
timeInfo.tm_hour = (int)hour;
|
||||
timeInfo.tm_mday = (int)day;
|
||||
timeInfo.tm_mon = (int)(month - 1); //It's 1-based coming in
|
||||
timeInfo.tm_year = (int)(year - 1900); //see time.h -- it's years since 1900
|
||||
timeInfo.tm_wday = -1;
|
||||
timeInfo.tm_yday = -1;
|
||||
timeInfo.tm_isdst = -1;
|
||||
timeInfo.tm_gmtoff = 0;//[timeZone secondsFromGMT];
|
||||
timeInfo.tm_zone = nil;
|
||||
|
||||
NSTimeInterval rawTime = (NSTimeInterval)(timegm(&timeInfo) - timeZoneOffset); //timegm instead of mktime (which uses local time zone)
|
||||
if (rawTime == (time_t)ULONG_MAX) {
|
||||
|
||||
/*NSCalendar is super-amazingly-slow (which is partly why RSDateParser exists), so this is used only when the date is far enough in the future (19 January 2038 03:14:08Z on 32-bit systems) that timegm fails. If profiling says that this is a performance issue, then you've got a weird app that needs to work with dates far in the future.*/
|
||||
|
||||
NSDateComponents *dateComponents = [NSDateComponents new];
|
||||
|
||||
dateComponents.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset];
|
||||
dateComponents.year = year;
|
||||
dateComponents.month = month;
|
||||
dateComponents.day = day;
|
||||
dateComponents.hour = hour;
|
||||
dateComponents.minute = minute;
|
||||
dateComponents.second = second + (milliseconds / 1000);
|
||||
|
||||
return [[NSCalendar autoupdatingCurrentCalendar] dateFromComponents:dateComponents];
|
||||
}
|
||||
|
||||
if (milliseconds > 0) {
|
||||
rawTime += ((float)milliseconds / 1000.0f);
|
||||
}
|
||||
|
||||
return [NSDate dateWithTimeIntervalSince1970:rawTime];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Standard Formats
|
||||
|
||||
static NSDate *RSParsePubDateWithBytes(const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
/*@"EEE',' dd MMM yyyy HH':'mm':'ss ZZZ"
|
||||
@"EEE, dd MMM yyyy HH:mm:ss zzz"
|
||||
@"dd MMM yyyy HH:mm zzz"
|
||||
@"dd MMM yyyy HH:mm ZZZ"
|
||||
@"EEE, dd MMM yyyy"
|
||||
@"EEE, dd MMM yyyy HH:mm zzz"
|
||||
etc.*/
|
||||
|
||||
NSUInteger finalIndex = 0;
|
||||
NSInteger day = 1;
|
||||
NSInteger month = RSJanuary;
|
||||
NSInteger year = 1970;
|
||||
NSInteger hour = 0;
|
||||
NSInteger minute = 0;
|
||||
NSInteger second = 0;
|
||||
NSInteger timeZoneOffset = 0;
|
||||
|
||||
day = nextNumericValue(bytes, numberOfBytes, 0, 2, &finalIndex);
|
||||
if (day < 1 || day == NSNotFound)
|
||||
day = 1;
|
||||
|
||||
month = nextMonthValue(bytes, numberOfBytes, finalIndex + 1, &finalIndex);
|
||||
year = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 4, &finalIndex);
|
||||
hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
|
||||
if (hour == NSNotFound)
|
||||
hour = 0;
|
||||
|
||||
minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
|
||||
if (minute == NSNotFound)
|
||||
minute = 0;
|
||||
|
||||
NSUInteger currentIndex = finalIndex + 1;
|
||||
|
||||
BOOL hasSeconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == ':');
|
||||
if (hasSeconds)
|
||||
second = nextNumericValue(bytes, numberOfBytes, currentIndex, 2, &finalIndex);
|
||||
|
||||
currentIndex = finalIndex + 1;
|
||||
BOOL hasTimeZone = (currentIndex < numberOfBytes) && (bytes[currentIndex] == ' ');
|
||||
if (hasTimeZone)
|
||||
timeZoneOffset = parsedTimeZoneOffset(bytes, numberOfBytes, currentIndex);
|
||||
|
||||
return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(year, month, day, hour, minute, second, 0, timeZoneOffset);
|
||||
}
|
||||
|
||||
|
||||
static NSDate *RSParseW3CWithBytes(const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
/*@"yyyy'-'MM'-'dd'T'HH':'mm':'ss"
|
||||
@"yyyy-MM-dd'T'HH:mm:sszzz"
|
||||
@"yyyy-MM-dd'T'HH:mm:ss'.'SSSzzz"
|
||||
etc.*/
|
||||
|
||||
NSUInteger finalIndex = 0;
|
||||
NSInteger day = 1;
|
||||
NSInteger month = RSJanuary;
|
||||
NSInteger year = 1970;
|
||||
NSInteger hour = 0;
|
||||
NSInteger minute = 0;
|
||||
NSInteger second = 0;
|
||||
NSInteger milliseconds = 0;
|
||||
NSInteger timeZoneOffset = 0;
|
||||
|
||||
year = nextNumericValue(bytes, numberOfBytes, 0, 4, &finalIndex);
|
||||
month = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
|
||||
day = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
|
||||
hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
|
||||
minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
|
||||
second = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
|
||||
|
||||
NSUInteger currentIndex = finalIndex + 1;
|
||||
BOOL hasMilliseconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == '.');
|
||||
if (hasMilliseconds) {
|
||||
milliseconds = nextNumericValue(bytes, numberOfBytes, currentIndex, 3, &finalIndex);
|
||||
currentIndex = finalIndex + 1;
|
||||
}
|
||||
|
||||
timeZoneOffset = parsedTimeZoneOffset(bytes, numberOfBytes, currentIndex);
|
||||
|
||||
return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(year, month, day, hour, minute, second, milliseconds, timeZoneOffset);
|
||||
}
|
||||
|
||||
|
||||
static BOOL dateIsPubDate(const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
NSUInteger i = 0;
|
||||
|
||||
for (i = 0; i < numberOfBytes; i++) {
|
||||
if (bytes[i] == ' ' || bytes[i] == ',')
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
static BOOL dateIsW3CDate(const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
// Something like 2010-11-17T08:40:07-05:00
|
||||
// But might be missing T character in the middle.
|
||||
// Looks for four digits in a row followed by a -.
|
||||
|
||||
for (NSUInteger i = 0; i < numberOfBytes; i++) {
|
||||
char ch = bytes[i];
|
||||
if (ch == ' ' || ch == '\r' || ch == '\n' || ch == '\t') {
|
||||
continue;
|
||||
}
|
||||
if (numberOfBytes - i < 5) {
|
||||
return NO;
|
||||
}
|
||||
return isdigit(ch) && isdigit(bytes[i + 1]) && isdigit(bytes[i + 2]) && isdigit(bytes[i + 3]) && bytes[i + 4] == '-';
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
static BOOL numberOfBytesIsOutsideReasonableRange(NSUInteger numberOfBytes) {
|
||||
return numberOfBytes < 6 || numberOfBytes > 150;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - API
|
||||
|
||||
NSDate *RSDateWithBytes(const char *bytes, NSUInteger numberOfBytes) {
|
||||
|
||||
if (numberOfBytesIsOutsideReasonableRange(numberOfBytes))
|
||||
return nil;
|
||||
|
||||
if (dateIsW3CDate(bytes, numberOfBytes)) {
|
||||
return RSParseW3CWithBytes(bytes, numberOfBytes);
|
||||
}
|
||||
if (dateIsPubDate(bytes, numberOfBytes))
|
||||
return RSParsePubDateWithBytes(bytes, numberOfBytes);
|
||||
|
||||
// Fallback, in case our detection fails.
|
||||
return RSParseW3CWithBytes(bytes, numberOfBytes);
|
||||
}
|
||||
|
||||
|
||||
NSDate *RSDateWithString(NSString *dateString) {
|
||||
|
||||
const char *utf8String = [dateString UTF8String];
|
||||
return RSDateWithBytes(utf8String, strlen(utf8String));
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
//
|
||||
// RSHTMLLinkParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 8/7/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/*Returns all <a href="some_url">some_text</a> as RSHTMLLink object array.*/
|
||||
|
||||
@class ParserData;
|
||||
@class RSHTMLLink;
|
||||
|
||||
@interface RSHTMLLinkParser : NSObject
|
||||
|
||||
+ (NSArray <RSHTMLLink *> *)htmlLinksWithParserData:(ParserData *)parserData;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface RSHTMLLink : NSObject
|
||||
|
||||
// Any of these, even urlString, may be nil, because HTML can be bad.
|
||||
|
||||
@property (nonatomic, nullable, readonly) NSString *urlString; //absolute
|
||||
@property (nonatomic, nullable, readonly) NSString *text;
|
||||
@property (nonatomic, nullable, readonly) NSString *title; //title attribute inside anchor tag
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,154 +0,0 @@
|
|||
//
|
||||
// RSHTMLLinkParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 8/7/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "RSHTMLLinkParser.h"
|
||||
#import "RSSAXHTMLParser.h"
|
||||
#import "RSSAXParser.h"
|
||||
#import "RSParserInternal.h"
|
||||
#import "ParserData.h"
|
||||
|
||||
#import <libxml/xmlstring.h>
|
||||
|
||||
|
||||
|
||||
@interface RSHTMLLinkParser() <RSSAXHTMLParserDelegate>
|
||||
|
||||
@property (nonatomic, readonly) NSMutableArray *links;
|
||||
@property (nonatomic, readonly) ParserData *parserData;
|
||||
@property (nonatomic, readonly) NSMutableArray *dictionaries;
|
||||
@property (nonatomic, readonly) NSURL *baseURL;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface RSHTMLLink()
|
||||
|
||||
@property (nonatomic, readwrite) NSString *urlString; //absolute
|
||||
@property (nonatomic, readwrite) NSString *text;
|
||||
@property (nonatomic, readwrite) NSString *title; //title attribute inside anchor tag
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSHTMLLinkParser
|
||||
|
||||
|
||||
#pragma mark - Class Methods
|
||||
|
||||
+ (NSArray *)htmlLinksWithParserData:(ParserData *)parserData {
|
||||
|
||||
RSHTMLLinkParser *parser = [[self alloc] initWithParserData:parserData];
|
||||
return parser.links;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithParserData:(ParserData *)parserData {
|
||||
|
||||
NSParameterAssert(parserData.data);
|
||||
NSParameterAssert(parserData.url);
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_links = [NSMutableArray new];
|
||||
_parserData = parserData;
|
||||
_dictionaries = [NSMutableArray new];
|
||||
_baseURL = [NSURL URLWithString:parserData.url];
|
||||
|
||||
[self parse];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Parse
|
||||
|
||||
- (void)parse {
|
||||
|
||||
RSSAXHTMLParser *parser = [[RSSAXHTMLParser alloc] initWithDelegate:self];
|
||||
[parser parseData:self.parserData.data];
|
||||
[parser finishParsing];
|
||||
}
|
||||
|
||||
|
||||
- (RSHTMLLink *)currentLink {
|
||||
|
||||
return self.links.lastObject;
|
||||
}
|
||||
|
||||
|
||||
static NSString *kHrefKey = @"href";
|
||||
|
||||
- (NSString *)urlStringFromDictionary:(NSDictionary *)d {
|
||||
|
||||
NSString *href = [d rsparser_objectForCaseInsensitiveKey:kHrefKey];
|
||||
if (!href) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSURL *absoluteURL = [NSURL URLWithString:href relativeToURL:self.baseURL];
|
||||
return absoluteURL.absoluteString;
|
||||
}
|
||||
|
||||
|
||||
static NSString *kTitleKey = @"title";
|
||||
|
||||
- (NSString *)titleFromDictionary:(NSDictionary *)d {
|
||||
|
||||
return [d rsparser_objectForCaseInsensitiveKey:kTitleKey];
|
||||
}
|
||||
|
||||
|
||||
- (void)handleLinkAttributes:(NSDictionary *)d {
|
||||
|
||||
RSHTMLLink *link = self.currentLink;
|
||||
link.urlString = [self urlStringFromDictionary:d];
|
||||
link.title = [self titleFromDictionary:d];
|
||||
}
|
||||
|
||||
|
||||
static const char *kAnchor = "a";
|
||||
static const NSInteger kAnchorLength = 2;
|
||||
|
||||
- (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLStartElement:(const xmlChar *)localName attributes:(const xmlChar **)attributes {
|
||||
|
||||
if (!RSSAXEqualTags(localName, kAnchor, kAnchorLength)) {
|
||||
return;
|
||||
}
|
||||
|
||||
RSHTMLLink *link = [RSHTMLLink new];
|
||||
[self.links addObject:link];
|
||||
|
||||
NSDictionary *d = [SAXParser attributesDictionary:attributes];
|
||||
if (!RSParserObjectIsEmpty(d)) {
|
||||
[self handleLinkAttributes:d];
|
||||
}
|
||||
|
||||
[SAXParser beginStoringCharacters];
|
||||
}
|
||||
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName {
|
||||
|
||||
if (!RSSAXEqualTags(localName, kAnchor, kAnchorLength)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.currentLink.text = SAXParser.currentStringWithTrimmedWhitespace;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RSHTMLLink
|
||||
|
||||
@end
|
|
@ -1,97 +0,0 @@
|
|||
//
|
||||
// RSHTMLMetadata.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/6/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
@import CoreGraphics;
|
||||
|
||||
@class RSHTMLMetadataFeedLink;
|
||||
@class RSHTMLMetadataAppleTouchIcon;
|
||||
@class RSHTMLMetadataFavicon;
|
||||
@class RSHTMLOpenGraphProperties;
|
||||
@class RSHTMLOpenGraphImage;
|
||||
@class RSHTMLTag;
|
||||
@class RSHTMLTwitterProperties;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RSHTMLMetadata : NSObject
|
||||
|
||||
- (instancetype)initWithURLString:(NSString *)urlString tags:(NSArray <RSHTMLTag *> *)tags;
|
||||
|
||||
@property (nonatomic, readonly) NSString *baseURLString;
|
||||
@property (nonatomic, readonly) NSArray <RSHTMLTag *> *tags;
|
||||
|
||||
@property (nonatomic, readonly) NSArray <NSString *> *faviconLinks DEPRECATED_MSG_ATTRIBUTE("Use the favicons property instead.");
|
||||
@property (nonatomic, readonly) NSArray <RSHTMLMetadataFavicon *> *favicons;
|
||||
@property (nonatomic, readonly) NSArray <RSHTMLMetadataAppleTouchIcon *> *appleTouchIcons;
|
||||
@property (nonatomic, readonly) NSArray <RSHTMLMetadataFeedLink *> *feedLinks;
|
||||
|
||||
@property (nonatomic, readonly) RSHTMLOpenGraphProperties *openGraphProperties;
|
||||
@property (nonatomic, readonly) RSHTMLTwitterProperties *twitterProperties;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface RSHTMLMetadataAppleTouchIcon : NSObject
|
||||
|
||||
@property (nonatomic, readonly) NSString *rel;
|
||||
@property (nonatomic, nullable, readonly) NSString *sizes;
|
||||
@property (nonatomic, readonly) CGSize size;
|
||||
@property (nonatomic, nullable, readonly) NSString *urlString; // Absolute.
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface RSHTMLMetadataFeedLink : NSObject
|
||||
|
||||
@property (nonatomic, nullable, readonly) NSString *title;
|
||||
@property (nonatomic, nullable, readonly) NSString *type;
|
||||
@property (nonatomic, nullable, readonly) NSString *urlString; // Absolute.
|
||||
|
||||
@end
|
||||
|
||||
@interface RSHTMLMetadataFavicon : NSObject
|
||||
|
||||
@property (nonatomic, nullable, readonly) NSString *type;
|
||||
@property (nonatomic, nullable, readonly) NSString *urlString;
|
||||
|
||||
@end
|
||||
|
||||
@interface RSHTMLOpenGraphProperties : NSObject
|
||||
|
||||
// TODO: the rest. At this writing (Nov. 26, 2017) I just care about og:image.
|
||||
// See http://ogp.me/
|
||||
|
||||
- (instancetype)initWithURLString:(NSString *)urlString tags:(NSArray <RSHTMLTag *> *)tags;
|
||||
|
||||
@property (nonatomic, readonly) NSArray <RSHTMLOpenGraphImage *> *images;
|
||||
|
||||
@end
|
||||
|
||||
@interface RSHTMLOpenGraphImage : NSObject
|
||||
|
||||
@property (nonatomic, nullable, readonly) NSString *url;
|
||||
@property (nonatomic, nullable, readonly) NSString *secureURL;
|
||||
@property (nonatomic, nullable, readonly) NSString *mimeType;
|
||||
@property (nonatomic, readonly) CGFloat width;
|
||||
@property (nonatomic, readonly) CGFloat height;
|
||||
@property (nonatomic, nullable, readonly) NSString *altText;
|
||||
|
||||
@end
|
||||
|
||||
@interface RSHTMLTwitterProperties : NSObject
|
||||
|
||||
// TODO: the rest. At this writing (Nov. 26, 2017) I just care about twitter:image:src.
|
||||
|
||||
- (instancetype)initWithURLString:(NSString *)urlString tags:(NSArray <RSHTMLTag *> *)tags;
|
||||
|
||||
@property (nonatomic, nullable, readonly) NSString *imageURL; // twitter:image:src
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,483 +0,0 @@
|
|||
//
|
||||
// RSHTMLMetadata.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/6/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSHTMLMetadata.h"
|
||||
#import "RSParserInternal.h"
|
||||
#import "RSHTMLTag.h"
|
||||
|
||||
|
||||
|
||||
static NSString *urlStringFromDictionary(NSDictionary *d);
|
||||
static NSString *absoluteURLStringWithRelativeURLString(NSString *relativeURLString, NSString *baseURLString);
|
||||
static NSString *absoluteURLStringWithDictionary(NSDictionary *d, NSString *baseURLString);
|
||||
static NSArray *objectsOfClassWithTags(Class class, NSArray *tags, NSString *baseURLString);
|
||||
static NSString *relValue(NSDictionary *d);
|
||||
static BOOL typeIsFeedType(NSString *type);
|
||||
|
||||
static NSString *kIconRelValue = @"icon";
|
||||
static NSString *kHrefKey = @"href";
|
||||
static NSString *kSrcKey = @"src";
|
||||
static NSString *kAppleTouchIconValue = @"apple-touch-icon";
|
||||
static NSString *kAppleTouchIconPrecomposedValue = @"apple-touch-icon-precomposed";
|
||||
static NSString *kSizesKey = @"sizes";
|
||||
static NSString *kTitleKey = @"title";
|
||||
static NSString *kRelKey = @"rel";
|
||||
static NSString *kAlternateKey = @"alternate";
|
||||
static NSString *kRSSSuffix = @"/rss+xml";
|
||||
static NSString *kAtomSuffix = @"/atom+xml";
|
||||
static NSString *kJSONSuffix = @"/json";
|
||||
static NSString *kTypeKey = @"type";
|
||||
|
||||
@interface RSHTMLMetadataAppleTouchIcon ()
|
||||
|
||||
- (instancetype)initWithTag:(RSHTMLTag *)tag baseURLString:(NSString *)baseURLString;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface RSHTMLMetadataFeedLink ()
|
||||
|
||||
- (instancetype)initWithTag:(RSHTMLTag *)tag baseURLString:(NSString *)baseURLString;
|
||||
|
||||
@end
|
||||
|
||||
@interface RSHTMLMetadataFavicon ()
|
||||
|
||||
- (instancetype)initWithTag:(RSHTMLTag *)tag baseURLString:(NSString *)baseURLString;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RSHTMLMetadata
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithURLString:(NSString *)urlString tags:(NSArray <RSHTMLTag *> *)tags {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_baseURLString = urlString;
|
||||
_tags = tags;
|
||||
|
||||
_favicons = [self resolvedFaviconLinks];
|
||||
|
||||
NSArray *appleTouchIconTags = [self appleTouchIconTags];
|
||||
_appleTouchIcons = objectsOfClassWithTags([RSHTMLMetadataAppleTouchIcon class], appleTouchIconTags, urlString);
|
||||
|
||||
NSArray *feedLinkTags = [self feedLinkTags];
|
||||
_feedLinks = objectsOfClassWithTags([RSHTMLMetadataFeedLink class], feedLinkTags, urlString);
|
||||
|
||||
_openGraphProperties = [[RSHTMLOpenGraphProperties alloc] initWithURLString:urlString tags:tags];
|
||||
_twitterProperties = [[RSHTMLTwitterProperties alloc] initWithURLString:urlString tags:tags];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSArray<RSHTMLTag *> *)linkTagsWithMatchingRel:(NSString *)valueToMatch {
|
||||
|
||||
// Case-insensitive; matches a whitespace-delimited word
|
||||
|
||||
NSMutableArray<RSHTMLTag *> *tags = [NSMutableArray array];
|
||||
|
||||
for (RSHTMLTag *tag in self.tags) {
|
||||
|
||||
if (tag.type != RSHTMLTagTypeLink || RSParserStringIsEmpty(urlStringFromDictionary(tag.attributes))) {
|
||||
continue;
|
||||
}
|
||||
NSString *oneRelValue = relValue(tag.attributes);
|
||||
if (oneRelValue) {
|
||||
NSArray *relValues = [oneRelValue componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
|
||||
for (NSString *relValue in relValues) {
|
||||
if ([relValue compare:valueToMatch options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
[tags addObject:tag];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
||||
- (NSArray<RSHTMLTag *> *)appleTouchIconTags {
|
||||
|
||||
NSMutableArray *tags = [NSMutableArray new];
|
||||
|
||||
for (RSHTMLTag *tag in self.tags) {
|
||||
|
||||
if (tag.type != RSHTMLTagTypeLink) {
|
||||
continue;
|
||||
}
|
||||
NSString *oneRelValue = relValue(tag.attributes).lowercaseString;
|
||||
if ([oneRelValue isEqualToString:kAppleTouchIconValue] || [oneRelValue isEqualToString:kAppleTouchIconPrecomposedValue]) {
|
||||
[tags addObject:tag];
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
||||
- (NSArray<RSHTMLTag *> *)feedLinkTags {
|
||||
|
||||
NSMutableArray *tags = [NSMutableArray new];
|
||||
|
||||
for (RSHTMLTag *tag in self.tags) {
|
||||
|
||||
if (tag.type != RSHTMLTagTypeLink) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSDictionary *oneDictionary = tag.attributes;
|
||||
NSString *oneRelValue = relValue(oneDictionary).lowercaseString;
|
||||
if (![oneRelValue isEqualToString:kAlternateKey]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *oneType = [oneDictionary rsparser_objectForCaseInsensitiveKey:kTypeKey];
|
||||
if (!typeIsFeedType(oneType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RSParserStringIsEmpty(urlStringFromDictionary(oneDictionary))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[tags addObject:tag];
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)faviconLinks {
|
||||
NSMutableArray *urls = [NSMutableArray array];
|
||||
|
||||
for (RSHTMLMetadataFavicon *favicon in self.favicons) {
|
||||
[urls addObject:favicon.urlString];
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
- (NSArray<RSHTMLMetadataFavicon *> *)resolvedFaviconLinks {
|
||||
NSArray<RSHTMLTag *> *tags = [self linkTagsWithMatchingRel:kIconRelValue];
|
||||
NSMutableArray *links = [NSMutableArray array];
|
||||
NSMutableSet<NSString *> *seenHrefs = [NSMutableSet setWithCapacity:tags.count];
|
||||
|
||||
for (RSHTMLTag *tag in tags) {
|
||||
RSHTMLMetadataFavicon *link = [[RSHTMLMetadataFavicon alloc] initWithTag:tag baseURLString:self.baseURLString];
|
||||
NSString *urlString = link.urlString;
|
||||
if (urlString == nil) {
|
||||
continue;
|
||||
}
|
||||
if (![seenHrefs containsObject:urlString]) {
|
||||
[links addObject:link];
|
||||
[seenHrefs addObject:urlString];
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static NSString *relValue(NSDictionary *d) {
|
||||
|
||||
return [d rsparser_objectForCaseInsensitiveKey:kRelKey];
|
||||
}
|
||||
|
||||
|
||||
static NSString *urlStringFromDictionary(NSDictionary *d) {
|
||||
|
||||
NSString *urlString = [d rsparser_objectForCaseInsensitiveKey:kHrefKey];
|
||||
if (urlString) {
|
||||
return urlString;
|
||||
}
|
||||
|
||||
return [d rsparser_objectForCaseInsensitiveKey:kSrcKey];
|
||||
}
|
||||
|
||||
|
||||
static NSString *absoluteURLStringWithRelativeURLString(NSString *relativeURLString, NSString *baseURLString) {
|
||||
|
||||
NSURL *url = [NSURL URLWithString:baseURLString];
|
||||
if (!url) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSURL *absoluteURL = [NSURL URLWithString:relativeURLString relativeToURL:url];
|
||||
return absoluteURL.absoluteURL.standardizedURL.absoluteString;
|
||||
}
|
||||
|
||||
|
||||
static NSString *absoluteURLStringWithDictionary(NSDictionary *d, NSString *baseURLString) {
|
||||
|
||||
NSString *urlString = urlStringFromDictionary(d);
|
||||
if (RSParserStringIsEmpty(urlString)) {
|
||||
return nil;
|
||||
}
|
||||
return absoluteURLStringWithRelativeURLString(urlString, baseURLString);
|
||||
}
|
||||
|
||||
|
||||
static NSArray *objectsOfClassWithTags(Class class, NSArray *tags, NSString *baseURLString) {
|
||||
|
||||
NSMutableArray *objects = [NSMutableArray new];
|
||||
|
||||
for (RSHTMLTag *tag in tags) {
|
||||
|
||||
id oneObject = [[class alloc] initWithTag:tag baseURLString:baseURLString];
|
||||
if (oneObject) {
|
||||
[objects addObject:oneObject];
|
||||
}
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
|
||||
static BOOL typeIsFeedType(NSString *type) {
|
||||
|
||||
type = type.lowercaseString;
|
||||
return [type hasSuffix:kRSSSuffix] || [type hasSuffix:kAtomSuffix] || [type hasSuffix:kJSONSuffix];
|
||||
}
|
||||
|
||||
|
||||
@implementation RSHTMLMetadataAppleTouchIcon
|
||||
|
||||
- (instancetype)initWithTag:(RSHTMLTag *)tag baseURLString:(NSString *)baseURLString {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *d = tag.attributes;
|
||||
_urlString = absoluteURLStringWithDictionary(d, baseURLString);
|
||||
_sizes = [d rsparser_objectForCaseInsensitiveKey:kSizesKey];
|
||||
_rel = [d rsparser_objectForCaseInsensitiveKey:kRelKey];
|
||||
|
||||
_size = CGSizeZero;
|
||||
if (_sizes) {
|
||||
NSArray *components = [_sizes componentsSeparatedByString:@"x"];
|
||||
if (components.count == 2) {
|
||||
CGFloat width = [components[0] floatValue];
|
||||
CGFloat height = [components[1] floatValue];
|
||||
_size = CGSizeMake(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSHTMLMetadataFeedLink
|
||||
|
||||
- (instancetype)initWithTag:(RSHTMLTag *)tag baseURLString:(NSString *)baseURLString {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *d = tag.attributes;
|
||||
_urlString = absoluteURLStringWithDictionary(d, baseURLString);
|
||||
_title = [d rsparser_objectForCaseInsensitiveKey:kTitleKey];
|
||||
_type = [d rsparser_objectForCaseInsensitiveKey:kTypeKey];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RSHTMLMetadataFavicon
|
||||
|
||||
- (instancetype)initWithTag:(RSHTMLTag *)tag baseURLString:(NSString *)baseURLString {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *d = tag.attributes;
|
||||
_urlString = absoluteURLStringWithDictionary(d, baseURLString);
|
||||
_type = [d rsparser_objectForCaseInsensitiveKey:kTypeKey];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface RSHTMLOpenGraphImage ()
|
||||
|
||||
@property (nonatomic, readwrite) NSString *url;
|
||||
@property (nonatomic, readwrite) NSString *secureURL;
|
||||
@property (nonatomic, readwrite) NSString *mimeType;
|
||||
@property (nonatomic, readwrite) CGFloat width;
|
||||
@property (nonatomic, readwrite) CGFloat height;
|
||||
@property (nonatomic, readwrite) NSString *altText;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RSHTMLOpenGraphImage
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@interface RSHTMLOpenGraphProperties ()
|
||||
|
||||
@property (nonatomic) NSMutableArray *ogImages;
|
||||
@end
|
||||
|
||||
@implementation RSHTMLOpenGraphProperties
|
||||
|
||||
- (instancetype)initWithURLString:(NSString *)urlString tags:(NSArray <RSHTMLTag *> *)tags {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_ogImages = [NSMutableArray new];
|
||||
|
||||
[self parseTags:tags];
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (RSHTMLOpenGraphImage *)currentImage {
|
||||
|
||||
return self.ogImages.lastObject;
|
||||
}
|
||||
|
||||
|
||||
- (RSHTMLOpenGraphImage *)pushImage {
|
||||
|
||||
RSHTMLOpenGraphImage *image = [RSHTMLOpenGraphImage new];
|
||||
[self.ogImages addObject:image];
|
||||
return image;
|
||||
}
|
||||
|
||||
- (RSHTMLOpenGraphImage *)ensureImage {
|
||||
|
||||
RSHTMLOpenGraphImage *image = [self currentImage];
|
||||
if (image != nil) {
|
||||
return image;
|
||||
}
|
||||
return [self pushImage];
|
||||
}
|
||||
|
||||
|
||||
- (NSArray *)images {
|
||||
|
||||
return self.ogImages;
|
||||
}
|
||||
|
||||
static NSString *ogPrefix = @"og:";
|
||||
static NSString *ogImage = @"og:image";
|
||||
static NSString *ogImageURL = @"og:image:url";
|
||||
static NSString *ogImageSecureURL = @"og:image:secure_url";
|
||||
static NSString *ogImageType = @"og:image:type";
|
||||
static NSString *ogImageWidth = @"og:image:width";
|
||||
static NSString *ogImageHeight = @"og:image:height";
|
||||
static NSString *ogImageAlt = @"og:image:alt";
|
||||
static NSString *ogPropertyKey = @"property";
|
||||
static NSString *ogContentKey = @"content";
|
||||
|
||||
- (void)parseTags:(NSArray *)tags {
|
||||
|
||||
for (RSHTMLTag *tag in tags) {
|
||||
|
||||
if (tag.type != RSHTMLTagTypeMeta) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *propertyName = tag.attributes[ogPropertyKey];
|
||||
if (!propertyName || ![propertyName hasPrefix:ogPrefix]) {
|
||||
continue;
|
||||
}
|
||||
NSString *content = tag.attributes[ogContentKey];
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([propertyName isEqualToString:ogImage]) {
|
||||
RSHTMLOpenGraphImage *image = [self currentImage];
|
||||
if (!image || image.url) { // Most likely case, since og:image will probably appear before other image attributes.
|
||||
image = [self pushImage];
|
||||
}
|
||||
image.url = content;
|
||||
}
|
||||
|
||||
else if ([propertyName isEqualToString:ogImageURL]) {
|
||||
[self ensureImage].url = content;
|
||||
}
|
||||
else if ([propertyName isEqualToString:ogImageSecureURL]) {
|
||||
[self ensureImage].secureURL = content;
|
||||
}
|
||||
else if ([propertyName isEqualToString:ogImageType]) {
|
||||
[self ensureImage].mimeType = content;
|
||||
}
|
||||
else if ([propertyName isEqualToString:ogImageAlt]) {
|
||||
[self ensureImage].altText = content;
|
||||
}
|
||||
else if ([propertyName isEqualToString:ogImageWidth]) {
|
||||
[self ensureImage].width = [content floatValue];
|
||||
}
|
||||
else if ([propertyName isEqualToString:ogImageHeight]) {
|
||||
[self ensureImage].height = [content floatValue];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RSHTMLTwitterProperties
|
||||
|
||||
static NSString *twitterNameKey = @"name";
|
||||
static NSString *twitterContentKey = @"content";
|
||||
static NSString *twitterImageSrc = @"twitter:image:src";
|
||||
|
||||
- (instancetype)initWithURLString:(NSString *)urlString tags:(NSArray <RSHTMLTag *> *)tags {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
for (RSHTMLTag *tag in tags) {
|
||||
|
||||
if (tag.type != RSHTMLTagTypeMeta) {
|
||||
continue;
|
||||
}
|
||||
NSString *name = tag.attributes[twitterNameKey];
|
||||
if (!name || ![name isEqualToString:twitterImageSrc]) {
|
||||
continue;
|
||||
}
|
||||
NSString *content = tag.attributes[twitterContentKey];
|
||||
if (!content || content.length < 1) {
|
||||
continue;
|
||||
}
|
||||
_imageURL = content;
|
||||
break;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
//
|
||||
// RSHTMLMetadataParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/6/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
|
||||
@class RSHTMLMetadata;
|
||||
@class ParserData;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RSHTMLMetadataParser : NSObject
|
||||
|
||||
+ (RSHTMLMetadata *)HTMLMetadataWithParserData:(ParserData *)parserData;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,151 +0,0 @@
|
|||
//
|
||||
// RSHTMLMetadataParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/6/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSHTMLMetadataParser.h"
|
||||
#import "RSHTMLMetadata.h"
|
||||
#import "RSSAXHTMLParser.h"
|
||||
#import "RSSAXHTMLParser.h"
|
||||
#import "RSSAXParser.h"
|
||||
#import "RSParserInternal.h"
|
||||
#import "ParserData.h"
|
||||
#import "RSHTMLTag.h"
|
||||
|
||||
#import <libxml/xmlstring.h>
|
||||
|
||||
|
||||
@interface RSHTMLMetadataParser () <RSSAXHTMLParserDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ParserData *parserData;
|
||||
@property (nonatomic, readwrite) RSHTMLMetadata *metadata;
|
||||
@property (nonatomic) NSMutableArray *tags;
|
||||
@property (nonatomic) BOOL didFinishParsing;
|
||||
@property (nonatomic) BOOL shouldScanPastHeadSection;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSHTMLMetadataParser
|
||||
|
||||
|
||||
#pragma mark - Class Methods
|
||||
|
||||
+ (RSHTMLMetadata *)HTMLMetadataWithParserData:(ParserData *)parserData {
|
||||
|
||||
RSHTMLMetadataParser *parser = [[self alloc] initWithParserData:parserData];
|
||||
return parser.metadata;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithParserData:(ParserData *)parserData {
|
||||
|
||||
NSParameterAssert(parserData.data);
|
||||
NSParameterAssert(parserData.url);
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_parserData = parserData;
|
||||
_tags = [NSMutableArray new];
|
||||
|
||||
// YouTube has a weird bug where, on some pages, it puts the feed link tag after the head section, in the body section.
|
||||
// This allows for a special case where we continue to scan after the head section.
|
||||
// (Yes, this match could yield false positives, but it’s harmless.)
|
||||
_shouldScanPastHeadSection = [parserData.url rangeOfString:@"youtube" options:NSCaseInsensitiveSearch].location != NSNotFound;
|
||||
|
||||
[self parse];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Parse
|
||||
|
||||
- (void)parse {
|
||||
|
||||
RSSAXHTMLParser *parser = [[RSSAXHTMLParser alloc] initWithDelegate:self];
|
||||
[parser parseData:self.parserData.data];
|
||||
[parser finishParsing];
|
||||
|
||||
self.metadata = [[RSHTMLMetadata alloc] initWithURLString:self.parserData.url tags:self.tags];
|
||||
}
|
||||
|
||||
|
||||
static NSString *kHrefKey = @"href";
|
||||
static NSString *kSrcKey = @"src";
|
||||
static NSString *kRelKey = @"rel";
|
||||
|
||||
- (NSString *)linkForDictionary:(NSDictionary *)d {
|
||||
|
||||
NSString *link = [d rsparser_objectForCaseInsensitiveKey:kHrefKey];
|
||||
if (link) {
|
||||
return link;
|
||||
}
|
||||
|
||||
return [d rsparser_objectForCaseInsensitiveKey:kSrcKey];
|
||||
}
|
||||
|
||||
- (void)handleLinkAttributes:(NSDictionary *)d {
|
||||
|
||||
if (RSParserStringIsEmpty([d rsparser_objectForCaseInsensitiveKey:kRelKey])) {
|
||||
return;
|
||||
}
|
||||
if (RSParserStringIsEmpty([self linkForDictionary:d])) {
|
||||
return;
|
||||
}
|
||||
|
||||
RSHTMLTag *tag = [RSHTMLTag linkTagWithAttributes:d];
|
||||
[self.tags addObject:tag];
|
||||
}
|
||||
|
||||
- (void)handleMetaAttributes:(NSDictionary *)d {
|
||||
|
||||
RSHTMLTag *tag = [RSHTMLTag metaTagWithAttributes:d];
|
||||
[self.tags addObject:tag];
|
||||
}
|
||||
|
||||
#pragma mark - RSSAXHTMLParserDelegate
|
||||
|
||||
static const char *kBody = "body";
|
||||
static const NSInteger kBodyLength = 5;
|
||||
static const char *kLink = "link";
|
||||
static const NSInteger kLinkLength = 5;
|
||||
static const char *kMeta = "meta";
|
||||
static const NSInteger kMetaLength = 5;
|
||||
|
||||
- (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLStartElement:(const xmlChar *)localName attributes:(const xmlChar **)attributes {
|
||||
|
||||
if (self.didFinishParsing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kBody, kBodyLength) && !self.shouldScanPastHeadSection) {
|
||||
self.didFinishParsing = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kLink, kLinkLength)) {
|
||||
NSDictionary *d = [SAXParser attributesDictionary:attributes];
|
||||
if (!RSParserObjectIsEmpty(d)) {
|
||||
[self handleLinkAttributes:d];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kMeta, kMetaLength)) {
|
||||
NSDictionary *d = [SAXParser attributesDictionary:attributes];
|
||||
if (!RSParserObjectIsEmpty(d)) {
|
||||
[self handleMetaAttributes:d];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// RSHTMLTag.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 11/26/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *RSHTMLTagNameLink; // @"link"
|
||||
extern NSString *RSHTMLTagNameMeta; // @"meta"
|
||||
|
||||
typedef NS_ENUM(NSInteger, RSHTMLTagType) {
|
||||
RSHTMLTagTypeLink,
|
||||
RSHTMLTagTypeMeta
|
||||
};
|
||||
|
||||
@interface RSHTMLTag : NSObject
|
||||
|
||||
- (instancetype)initWithType:(RSHTMLTagType)type attributes:(NSDictionary *)attributes;
|
||||
|
||||
+ (RSHTMLTag *)linkTagWithAttributes:(NSDictionary *)attributes;
|
||||
+ (RSHTMLTag *)metaTagWithAttributes:(NSDictionary *)attributes;
|
||||
|
||||
@property (nonatomic, readonly) RSHTMLTagType type;
|
||||
@property (nonatomic, readonly) NSDictionary *attributes;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,43 +0,0 @@
|
|||
//
|
||||
// RSHTMLTag.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 11/26/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSHTMLTag.h"
|
||||
|
||||
NSString *RSHTMLTagNameLink = @"link";
|
||||
NSString *RSHTMLTagNameMeta = @"meta";
|
||||
|
||||
@implementation RSHTMLTag
|
||||
|
||||
- (instancetype)initWithType:(RSHTMLTagType)type attributes:(NSDictionary *)attributes {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_type = type;
|
||||
_attributes = attributes;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (RSHTMLTag *)linkTagWithAttributes:(NSDictionary *)attributes {
|
||||
|
||||
return [[self alloc] initWithType:RSHTMLTagTypeLink attributes:attributes];
|
||||
}
|
||||
|
||||
+ (RSHTMLTag *)metaTagWithAttributes:(NSDictionary *)attributes {
|
||||
|
||||
return [[self alloc] initWithType:RSHTMLTagTypeMeta attributes:attributes];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<%@: %p> type: %ld attributes: %@", NSStringFromClass([self class]), self, (long)self.type, self.attributes];
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,36 +0,0 @@
|
|||
//
|
||||
// RSOPMLAttributes.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
// OPML allows for arbitrary attributes.
|
||||
// These are the common attributes in OPML files used as RSS subscription lists.
|
||||
|
||||
extern NSString *OPMLTextKey; //text
|
||||
extern NSString *OPMLTitleKey; //title
|
||||
extern NSString *OPMLDescriptionKey; //description
|
||||
extern NSString *OPMLTypeKey; //type
|
||||
extern NSString *OPMLVersionKey; //version
|
||||
extern NSString *OPMLHMTLURLKey; //htmlUrl
|
||||
extern NSString *OPMLXMLURLKey; //xmlUrl
|
||||
|
||||
|
||||
@interface NSDictionary (RSOPMLAttributes)
|
||||
|
||||
// A frequent error in OPML files is to mess up the capitalization,
|
||||
// so these do a case-insensitive lookup.
|
||||
|
||||
@property (nonatomic, readonly) NSString *opml_text;
|
||||
@property (nonatomic, readonly) NSString *opml_title;
|
||||
@property (nonatomic, readonly) NSString *opml_description;
|
||||
@property (nonatomic, readonly) NSString *opml_type;
|
||||
@property (nonatomic, readonly) NSString *opml_version;
|
||||
@property (nonatomic, readonly) NSString *opml_htmlUrl;
|
||||
@property (nonatomic, readonly) NSString *opml_xmlUrl;
|
||||
|
||||
@end
|
|
@ -1,68 +0,0 @@
|
|||
//
|
||||
// RSOPMLAttributes.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLAttributes.h"
|
||||
#import "RSParserInternal.h"
|
||||
|
||||
|
||||
|
||||
|
||||
NSString *OPMLTextKey = @"text";
|
||||
NSString *OPMLTitleKey = @"title";
|
||||
NSString *OPMLDescriptionKey = @"description";
|
||||
NSString *OPMLTypeKey = @"type";
|
||||
NSString *OPMLVersionKey = @"version";
|
||||
NSString *OPMLHMTLURLKey = @"htmlUrl";
|
||||
NSString *OPMLXMLURLKey = @"xmlUrl";
|
||||
|
||||
|
||||
@implementation NSDictionary (RSOPMLAttributes)
|
||||
|
||||
- (NSString *)opml_text {
|
||||
|
||||
return [self rsparser_objectForCaseInsensitiveKey:OPMLTextKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_title {
|
||||
|
||||
return [self rsparser_objectForCaseInsensitiveKey:OPMLTitleKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_description {
|
||||
|
||||
return [self rsparser_objectForCaseInsensitiveKey:OPMLDescriptionKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_type {
|
||||
|
||||
return [self rsparser_objectForCaseInsensitiveKey:OPMLTypeKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_version {
|
||||
|
||||
return [self rsparser_objectForCaseInsensitiveKey:OPMLVersionKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_htmlUrl {
|
||||
|
||||
return [self rsparser_objectForCaseInsensitiveKey:OPMLHMTLURLKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_xmlUrl {
|
||||
|
||||
return [self rsparser_objectForCaseInsensitiveKey:OPMLXMLURLKey];
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// RSOPMLDocument.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
#import "RSOPMLItem.h"
|
||||
|
||||
|
||||
|
||||
|
||||
@interface RSOPMLDocument : RSOPMLItem
|
||||
|
||||
@property (nonatomic) NSString *title;
|
||||
@property (nonatomic) NSString *url;
|
||||
|
||||
@end
|
|
@ -1,14 +0,0 @@
|
|||
//
|
||||
// RSOPMLDocument.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "RSOPMLDocument.h"
|
||||
|
||||
@implementation RSOPMLDocument
|
||||
|
||||
@end
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// RSOPMLError.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
extern NSString *RSOPMLErrorDomain;
|
||||
|
||||
|
||||
typedef NS_ENUM(NSInteger, RSOPMLErrorCode) {
|
||||
RSOPMLErrorCodeDataIsWrongFormat = 1024
|
||||
};
|
||||
|
||||
|
||||
NSError *RSOPMLWrongFormatError(NSString *fileName);
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// RSOPMLError.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLError.h"
|
||||
|
||||
NSString *RSOPMLErrorDomain = @"com.ranchero.OPML";
|
||||
|
||||
NSError *RSOPMLWrongFormatError(NSString *fileName) {
|
||||
|
||||
NSString *localizedDescriptionFormatString = NSLocalizedString(@"The file ‘%@’ can’t be parsed because it’s not an OPML file.", @"OPML wrong format");
|
||||
NSString *localizedDescription = [NSString stringWithFormat:localizedDescriptionFormatString, fileName];
|
||||
|
||||
NSString *localizedFailureString = NSLocalizedString(@"The file is not an OPML file.", @"OPML wrong format");
|
||||
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: localizedDescription, NSLocalizedFailureReasonErrorKey: localizedFailureString};
|
||||
|
||||
return [[NSError alloc] initWithDomain:RSOPMLErrorDomain code:RSOPMLErrorCodeDataIsWrongFormat userInfo:userInfo];
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
//
|
||||
// RSOPMLFeedSpecifier.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RSOPMLFeedSpecifier : NSObject
|
||||
|
||||
- (instancetype)initWithTitle:(NSString * _Nullable)title feedDescription:(NSString * _Nullable)feedDescription homePageURL:(NSString * _Nullable)homePageURL feedURL:(NSString *)feedURL;
|
||||
|
||||
@property (nonatomic, nullable, readonly) NSString *title;
|
||||
@property (nonatomic, nullable, readonly) NSString *feedDescription;
|
||||
@property (nonatomic, nullable, readonly) NSString *homePageURL;
|
||||
@property (nonatomic, readonly) NSString *feedURL;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,51 +0,0 @@
|
|||
//
|
||||
// RSOPMLFeedSpecifier.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLFeedSpecifier.h"
|
||||
#import "RSParserInternal.h"
|
||||
|
||||
|
||||
|
||||
@implementation RSOPMLFeedSpecifier
|
||||
|
||||
- (instancetype)initWithTitle:(NSString *)title feedDescription:(NSString *)feedDescription homePageURL:(NSString *)homePageURL feedURL:(NSString *)feedURL {
|
||||
|
||||
NSParameterAssert(!RSParserStringIsEmpty(feedURL));
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (RSParserStringIsEmpty(title)) {
|
||||
_title = nil;
|
||||
}
|
||||
else {
|
||||
_title = title;
|
||||
}
|
||||
|
||||
if (RSParserStringIsEmpty(feedDescription)) {
|
||||
_feedDescription = nil;
|
||||
}
|
||||
else {
|
||||
_feedDescription = feedDescription;
|
||||
}
|
||||
|
||||
if (RSParserStringIsEmpty(homePageURL)) {
|
||||
_homePageURL = nil;
|
||||
}
|
||||
else {
|
||||
_homePageURL = homePageURL;
|
||||
}
|
||||
|
||||
_feedURL = feedURL;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,30 +0,0 @@
|
|||
//
|
||||
// RSOPMLItem.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@class RSOPMLFeedSpecifier;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RSOPMLItem : NSObject
|
||||
|
||||
@property (nonatomic, nullable) NSDictionary *attributes;
|
||||
@property (nonatomic, nullable) NSArray <RSOPMLItem *> *children;
|
||||
|
||||
- (void)addChild:(RSOPMLItem *)child;
|
||||
|
||||
@property (nonatomic, nullable, readonly) RSOPMLFeedSpecifier *feedSpecifier;
|
||||
|
||||
@property (nonatomic, nullable, readonly) NSString *titleFromAttributes;
|
||||
@property (nonatomic, readonly) BOOL isFolder;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
//
|
||||
// RSOPMLItem.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLItem.h"
|
||||
#import "RSOPMLAttributes.h"
|
||||
#import "RSOPMLFeedSpecifier.h"
|
||||
#import "RSParserInternal.h"
|
||||
|
||||
|
||||
|
||||
@interface RSOPMLItem ()
|
||||
|
||||
@property (nonatomic) NSMutableArray *mutableChildren;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSOPMLItem
|
||||
|
||||
@synthesize children = _children;
|
||||
@synthesize feedSpecifier = _feedSpecifier;
|
||||
|
||||
|
||||
- (NSArray *)children {
|
||||
|
||||
return [self.mutableChildren copy];
|
||||
}
|
||||
|
||||
|
||||
- (void)setChildren:(NSArray *)children {
|
||||
|
||||
_children = children;
|
||||
self.mutableChildren = [_children mutableCopy];
|
||||
}
|
||||
|
||||
|
||||
- (void)addChild:(RSOPMLItem *)child {
|
||||
|
||||
if (!self.mutableChildren) {
|
||||
self.mutableChildren = [NSMutableArray new];
|
||||
}
|
||||
|
||||
[self.mutableChildren addObject:child];
|
||||
}
|
||||
|
||||
|
||||
- (RSOPMLFeedSpecifier *)feedSpecifier {
|
||||
|
||||
if (_feedSpecifier) {
|
||||
return _feedSpecifier;
|
||||
}
|
||||
|
||||
NSString *feedURL = self.attributes.opml_xmlUrl;
|
||||
if (RSParserObjectIsEmpty(feedURL)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_feedSpecifier = [[RSOPMLFeedSpecifier alloc] initWithTitle:self.titleFromAttributes feedDescription:self.attributes.opml_description homePageURL:self.attributes.opml_htmlUrl feedURL:feedURL];
|
||||
|
||||
return _feedSpecifier;
|
||||
}
|
||||
|
||||
- (NSString *)titleFromAttributes {
|
||||
|
||||
NSString *title = self.attributes.opml_title;
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
title = self.attributes.opml_text;
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isFolder {
|
||||
|
||||
return self.mutableChildren.count > 0;
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// RSOPMLParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 7/12/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
|
||||
@class ParserData;
|
||||
@class RSOPMLDocument;
|
||||
|
||||
typedef void (^OPMLParserCallback)(RSOPMLDocument *opmlDocument, NSError *error);
|
||||
|
||||
// Parses on background thread; calls back on main thread.
|
||||
void RSParseOPML(ParserData *parserData, OPMLParserCallback callback);
|
||||
|
||||
|
||||
@interface RSOPMLParser: NSObject
|
||||
|
||||
+ (RSOPMLDocument *)parseOPMLWithParserData:(ParserData *)parserData error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
|
@ -1,310 +0,0 @@
|
|||
//
|
||||
// RSOPMLParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 7/12/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLParser.h"
|
||||
#import "RSSAXParser.h"
|
||||
#import "RSOPMLItem.h"
|
||||
#import "RSOPMLDocument.h"
|
||||
#import "RSOPMLAttributes.h"
|
||||
#import "RSOPMLError.h"
|
||||
#import "RSOPMLParser.h"
|
||||
#import "ParserData.h"
|
||||
|
||||
#import <libxml/xmlstring.h>
|
||||
|
||||
|
||||
|
||||
@interface RSOPMLParser () <RSSAXParserDelegate>
|
||||
|
||||
@property (nonatomic, readwrite) RSOPMLDocument *OPMLDocument;
|
||||
@property (nonatomic, readwrite) NSError *error;
|
||||
@property (nonatomic) NSMutableArray *itemStack;
|
||||
|
||||
@end
|
||||
|
||||
void RSParseOPML(ParserData *parserData, OPMLParserCallback callback) {
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
|
||||
@autoreleasepool {
|
||||
NSError *error = nil;
|
||||
RSOPMLDocument *opmlDocument = [RSOPMLParser parseOPMLWithParserData:parserData error:&error];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
callback(opmlDocument, error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@implementation RSOPMLParser
|
||||
|
||||
#pragma mark - Class Methods
|
||||
|
||||
+ (RSOPMLDocument *)parseOPMLWithParserData:(ParserData *)parserData error:(NSError **)error {
|
||||
|
||||
RSOPMLParser *parser = [[RSOPMLParser alloc] initWithParserData:parserData];
|
||||
|
||||
RSOPMLDocument *document = parser.OPMLDocument;
|
||||
document.url = parserData.url;
|
||||
if (parser.error && error) {
|
||||
*error = parser.error;
|
||||
return nil;
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithParserData:(ParserData *)parserData {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
[self parse:parserData];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)parse:(ParserData *)parserData {
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
if (![self canParseData:parserData.data]) {
|
||||
|
||||
NSString *filename = nil;
|
||||
NSURL *url = [NSURL URLWithString:parserData.url];
|
||||
if (url && url.isFileURL) {
|
||||
filename = url.path.lastPathComponent;
|
||||
}
|
||||
if ([parserData.url hasPrefix:@"http"]) {
|
||||
filename = parserData.url;
|
||||
}
|
||||
if (!filename) {
|
||||
filename = parserData.url;
|
||||
}
|
||||
self.error = RSOPMLWrongFormatError(filename);
|
||||
return;
|
||||
}
|
||||
|
||||
RSSAXParser *parser = [[RSSAXParser alloc] initWithDelegate:self];
|
||||
|
||||
self.itemStack = [NSMutableArray new];
|
||||
self.OPMLDocument = [RSOPMLDocument new];
|
||||
[self pushItem:self.OPMLDocument];
|
||||
|
||||
[parser parseData:parserData.data];
|
||||
[parser finishParsing];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)canParseData:(NSData *)d {
|
||||
|
||||
// Check for <opml and <outline near the top.
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
NSString *s = [[NSString alloc] initWithBytesNoCopy:(void *)d.bytes length:d.length encoding:NSUTF8StringEncoding freeWhenDone:NO];
|
||||
if (!s) {
|
||||
NSDictionary *options = @{NSStringEncodingDetectionSuggestedEncodingsKey : @[@(NSUTF8StringEncoding)]};
|
||||
(void)[NSString stringEncodingForData:d encodingOptions:options convertedString:&s usedLossyConversion:nil];
|
||||
}
|
||||
if (!s) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
static const NSInteger numberOfCharactersToSearch = 4096;
|
||||
NSRange rangeToSearch = NSMakeRange(0, numberOfCharactersToSearch);
|
||||
if (s.length < numberOfCharactersToSearch) {
|
||||
rangeToSearch.length = s.length;
|
||||
}
|
||||
|
||||
NSRange opmlRange = [s rangeOfString:@"<opml" options:NSCaseInsensitiveSearch range:rangeToSearch];
|
||||
if (opmlRange.length < 1) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)pushItem:(RSOPMLItem *)item {
|
||||
|
||||
[self.itemStack addObject:item];
|
||||
}
|
||||
|
||||
|
||||
- (void)popItem {
|
||||
|
||||
NSAssert(self.itemStack.count > 0, nil);
|
||||
|
||||
/*If itemStack is empty, bad things are happening.
|
||||
But we still shouldn't crash in production.*/
|
||||
|
||||
if (self.itemStack.count > 0) {
|
||||
[self.itemStack removeLastObject];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (RSOPMLItem *)currentItem {
|
||||
|
||||
return self.itemStack.lastObject;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - RSSAXParserDelegate
|
||||
|
||||
static const char *kOutline = "outline";
|
||||
static const char kOutlineLength = 8;
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes {
|
||||
|
||||
if (RSSAXEqualTags(localName, kTitle, kTitleLength)) {
|
||||
[SAXParser beginStoringCharacters];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RSSAXEqualTags(localName, kOutline, kOutlineLength)) {
|
||||
return;
|
||||
}
|
||||
|
||||
RSOPMLItem *item = [RSOPMLItem new];
|
||||
item.attributes = [SAXParser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
|
||||
|
||||
[[self currentItem] addChild:item];
|
||||
[self pushItem:item];
|
||||
}
|
||||
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri {
|
||||
|
||||
if (RSSAXEqualTags(localName, kTitle, kTitleLength)) {
|
||||
RSOPMLItem* item = [self currentItem];
|
||||
if ([item isKindOfClass:[RSOPMLDocument class]]) {
|
||||
((RSOPMLDocument *)item).title = SAXParser.currentStringWithTrimmedWhitespace;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kOutline, kOutlineLength)) {
|
||||
[self popItem];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const char *kText = "text";
|
||||
static const NSInteger kTextLength = 5;
|
||||
|
||||
static const char *kTitle = "title";
|
||||
static const NSInteger kTitleLength = 6;
|
||||
|
||||
static const char *kDescription = "description";
|
||||
static const NSInteger kDescriptionLength = 12;
|
||||
|
||||
static const char *kType = "type";
|
||||
static const NSInteger kTypeLength = 5;
|
||||
|
||||
static const char *kVersion = "version";
|
||||
static const NSInteger kVersionLength = 8;
|
||||
|
||||
static const char *kHTMLURL = "htmlUrl";
|
||||
static const NSInteger kHTMLURLLength = 8;
|
||||
|
||||
static const char *kXMLURL = "xmlUrl";
|
||||
static const NSInteger kXMLURLLength = 7;
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix {
|
||||
|
||||
if (prefix) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t nameLength = strlen((const char *)name);
|
||||
|
||||
if (nameLength == kTextLength - 1) {
|
||||
if (RSSAXEqualTags(name, kText, kTextLength)) {
|
||||
return OPMLTextKey;
|
||||
}
|
||||
if (RSSAXEqualTags(name, kType, kTypeLength)) {
|
||||
return OPMLTypeKey;
|
||||
}
|
||||
}
|
||||
|
||||
else if (nameLength == kTitleLength - 1) {
|
||||
if (RSSAXEqualTags(name, kTitle, kTitleLength)) {
|
||||
return OPMLTitleKey;
|
||||
}
|
||||
}
|
||||
|
||||
else if (nameLength == kXMLURLLength - 1) {
|
||||
if (RSSAXEqualTags(name, kXMLURL, kXMLURLLength)) {
|
||||
return OPMLXMLURLKey;
|
||||
}
|
||||
}
|
||||
|
||||
else if (nameLength == kVersionLength - 1) {
|
||||
if (RSSAXEqualTags(name, kVersion, kVersionLength)) {
|
||||
return OPMLVersionKey;
|
||||
}
|
||||
if (RSSAXEqualTags(name, kHTMLURL, kHTMLURLLength)) {
|
||||
return OPMLHMTLURLKey;
|
||||
}
|
||||
}
|
||||
|
||||
else if (nameLength == kDescriptionLength - 1) {
|
||||
if (RSSAXEqualTags(name, kDescription, kDescriptionLength)) {
|
||||
return OPMLDescriptionKey;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
static const char *kRSSUppercase = "RSS";
|
||||
static const char *kRSSLowercase = "rss";
|
||||
static const NSUInteger kRSSLength = 3;
|
||||
static NSString *RSSUppercaseValue = @"RSS";
|
||||
static NSString *RSSLowercaseValue = @"rss";
|
||||
static NSString *emptyString = @"";
|
||||
|
||||
static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
|
||||
|
||||
return memcmp(bytes1, bytes2, length) == 0;
|
||||
}
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
|
||||
|
||||
|
||||
if (length < 1) {
|
||||
return emptyString;
|
||||
}
|
||||
|
||||
if (length == kRSSLength) {
|
||||
|
||||
if (equalBytes(bytes, kRSSUppercase, kRSSLength)) {
|
||||
return RSSUppercaseValue;
|
||||
}
|
||||
else if (equalBytes(bytes, kRSSLowercase, kRSSLength)) {
|
||||
return RSSLowercaseValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -1,37 +0,0 @@
|
|||
//
|
||||
// RSParsedArticle.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 12/6/14.
|
||||
// Copyright (c) 2014 Ranchero Software LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@class RSParsedEnclosure;
|
||||
@class RSParsedAuthor;
|
||||
|
||||
@interface RSParsedArticle : NSObject
|
||||
|
||||
- (nonnull instancetype)initWithFeedURL:(NSString * _Nonnull)feedURL;
|
||||
|
||||
@property (nonatomic, readonly, nonnull) NSString *feedURL;
|
||||
@property (nonatomic, nonnull) NSString *articleID; //guid, if present, or calculated from other attributes. Should be unique to the feed, but not necessarily unique across different feeds. (Not suitable for a database ID.)
|
||||
|
||||
@property (nonatomic, nullable) NSString *guid;
|
||||
@property (nonatomic, nullable) NSString *title;
|
||||
@property (nonatomic, nullable) NSString *body;
|
||||
@property (nonatomic, nullable) NSString *link;
|
||||
@property (nonatomic, nullable) NSString *permalink;
|
||||
@property (nonatomic, nullable) NSSet<RSParsedAuthor *> *authors;
|
||||
@property (nonatomic, nullable) NSSet<RSParsedEnclosure *> *enclosures;
|
||||
@property (nonatomic, nullable) NSDate *datePublished;
|
||||
@property (nonatomic, nullable) NSDate *dateModified;
|
||||
@property (nonatomic, nonnull) NSDate *dateParsed;
|
||||
@property (nonatomic, nullable) NSString *language;
|
||||
|
||||
- (void)addEnclosure:(RSParsedEnclosure *_Nonnull)enclosure;
|
||||
- (void)addAuthor:(RSParsedAuthor *_Nonnull)author;
|
||||
|
||||
@end
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
//
|
||||
// RSParsedArticle.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 12/6/14.
|
||||
// Copyright (c) 2014 Ranchero Software LLC. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "RSParsedArticle.h"
|
||||
#import "RSParserInternal.h"
|
||||
#import "NSString+RSParser.h"
|
||||
#import "RSParsedAuthor.h"
|
||||
#import "RSParsedEnclosure.h"
|
||||
|
||||
|
||||
|
||||
@implementation RSParsedArticle
|
||||
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithFeedURL:(NSString *)feedURL {
|
||||
|
||||
NSParameterAssert(feedURL != nil);
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_feedURL = feedURL;
|
||||
_dateParsed = [NSDate date];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Enclosures
|
||||
|
||||
- (void)addEnclosure:(RSParsedEnclosure *)enclosure {
|
||||
|
||||
if (self.enclosures) {
|
||||
self.enclosures = [self.enclosures setByAddingObject:enclosure];
|
||||
}
|
||||
else {
|
||||
self.enclosures = [NSSet setWithObject:enclosure];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Authors
|
||||
|
||||
- (void)addAuthor:(RSParsedAuthor *)author {
|
||||
|
||||
if (self.authors) {
|
||||
self.authors = [self.authors setByAddingObject:author];
|
||||
}
|
||||
else {
|
||||
self.authors = [NSSet setWithObject:author];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - articleID
|
||||
|
||||
- (NSString *)articleID {
|
||||
|
||||
if (self.guid) {
|
||||
return self.guid;
|
||||
}
|
||||
|
||||
if (!_articleID) {
|
||||
_articleID = [self calculatedArticleID];
|
||||
}
|
||||
|
||||
return _articleID;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)calculatedArticleID {
|
||||
|
||||
/*Concatenate a combination of properties when no guid. Then hash the result.
|
||||
In general, feeds should have guids. When they don't, re-runs are very likely,
|
||||
because there's no other 100% reliable way to determine identity.
|
||||
This is intended to create an ID unique inside a feed, but not globally unique.
|
||||
Not suitable for a database ID, in other words.*/
|
||||
|
||||
NSMutableString *s = [NSMutableString stringWithString:@""];
|
||||
|
||||
NSString *datePublishedTimeStampString = nil;
|
||||
if (self.datePublished) {
|
||||
datePublishedTimeStampString = [NSString stringWithFormat:@"%.0f", self.datePublished.timeIntervalSince1970];
|
||||
}
|
||||
|
||||
// Ideally we have a permalink and a pubDate. Either one would probably be a good guid, but together they should be rock-solid. (In theory. Feeds are buggy, though.)
|
||||
if (!RSParserStringIsEmpty(self.permalink) && datePublishedTimeStampString) {
|
||||
[s appendString:self.permalink];
|
||||
[s appendString:datePublishedTimeStampString];
|
||||
}
|
||||
|
||||
else if (!RSParserStringIsEmpty(self.link) && datePublishedTimeStampString) {
|
||||
[s appendString:self.link];
|
||||
[s appendString:datePublishedTimeStampString];
|
||||
}
|
||||
|
||||
else if (!RSParserStringIsEmpty(self.title) && datePublishedTimeStampString) {
|
||||
[s appendString:self.title];
|
||||
[s appendString:datePublishedTimeStampString];
|
||||
}
|
||||
|
||||
else if (datePublishedTimeStampString) {
|
||||
[s appendString:datePublishedTimeStampString];
|
||||
}
|
||||
|
||||
else if (!RSParserStringIsEmpty(self.permalink)) {
|
||||
[s appendString:self.permalink];
|
||||
}
|
||||
|
||||
else if (!RSParserStringIsEmpty(self.link)) {
|
||||
[s appendString:self.link];
|
||||
}
|
||||
|
||||
else if (!RSParserStringIsEmpty(self.title)) {
|
||||
[s appendString:self.title];
|
||||
}
|
||||
|
||||
else if (!RSParserStringIsEmpty(self.body)) {
|
||||
[s appendString:self.body];
|
||||
}
|
||||
|
||||
return [s rsparser_md5Hash];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// RSParsedAuthor.h
|
||||
// RSParserTests
|
||||
//
|
||||
// Created by Brent Simmons on 12/19/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@interface RSParsedAuthor : NSObject
|
||||
|
||||
@property (nonatomic, nullable) NSString *name;
|
||||
@property (nonatomic, nullable) NSString *emailAddress;
|
||||
@property (nonatomic, nullable) NSString *url;
|
||||
|
||||
+ (instancetype _Nonnull )authorWithSingleString:(NSString *_Nonnull)s; // Don’t know which property it is. Guess based on contents of the string. Common with RSS.
|
||||
|
||||
@end
|
|
@ -1,34 +0,0 @@
|
|||
//
|
||||
// RSParsedAuthor.m
|
||||
// RSParserTests
|
||||
//
|
||||
// Created by Brent Simmons on 12/19/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSString+RSParser.h"
|
||||
|
||||
#import "RSParsedAuthor.h"
|
||||
|
||||
@implementation RSParsedAuthor
|
||||
|
||||
+ (instancetype)authorWithSingleString:(NSString *)s {
|
||||
|
||||
// The author element in RSS is supposed to be email address — but often it’s a name, and sometimes a URL.
|
||||
|
||||
RSParsedAuthor *author = [[self alloc] init];
|
||||
|
||||
if ([s rsparser_contains:@"@"]) {
|
||||
author.emailAddress = s;
|
||||
}
|
||||
else if ([s.lowercaseString hasPrefix:@"http"]) {
|
||||
author.url = s;
|
||||
}
|
||||
else {
|
||||
author.name = s;
|
||||
}
|
||||
|
||||
return author;
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// RSParsedEnclosure.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 12/18/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RSParsedEnclosure : NSObject
|
||||
|
||||
@property (nonatomic) NSString *url;
|
||||
@property (nonatomic) NSInteger length;
|
||||
@property (nonatomic, nullable) NSString *mimeType;
|
||||
@property (nonatomic, nullable) NSString *title;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// RSParsedEnclosure.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 12/18/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSParsedEnclosure.h"
|
||||
|
||||
@implementation RSParsedEnclosure
|
||||
|
||||
@end
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// RSParsedFeed.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 7/12/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@class RSParsedArticle;
|
||||
|
||||
@interface RSParsedFeed : NSObject
|
||||
|
||||
- (nonnull instancetype)initWithURLString:(NSString * _Nonnull)urlString title:(NSString * _Nullable)title link:(NSString * _Nullable)link language:(NSString * _Nullable)language articles:(NSArray <RSParsedArticle *>* _Nonnull)articles;
|
||||
|
||||
@property (nonatomic, readonly, nonnull) NSString *urlString;
|
||||
@property (nonatomic, readonly, nullable) NSString *title;
|
||||
@property (nonatomic, readonly, nullable) NSString *link;
|
||||
@property (nonatomic, readonly, nullable) NSString *language;
|
||||
@property (nonatomic, readonly, nonnull) NSSet <RSParsedArticle *>*articles;
|
||||
|
||||
@end
|
|
@ -1,32 +0,0 @@
|
|||
//
|
||||
// RSParsedFeed.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 7/12/15.
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSParsedFeed.h"
|
||||
|
||||
|
||||
|
||||
@implementation RSParsedFeed
|
||||
|
||||
- (instancetype)initWithURLString:(NSString *)urlString title:(NSString *)title link:(NSString *)link language:(NSString *)language articles:(NSSet *)articles {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_urlString = urlString;
|
||||
_title = title;
|
||||
_link = link;
|
||||
_language = language;
|
||||
_articles = articles;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -1,24 +0,0 @@
|
|||
//
|
||||
// RSParserInternal.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 12/26/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
BOOL RSParserObjectIsEmpty(id _Nullable obj);
|
||||
BOOL RSParserStringIsEmpty(NSString * _Nullable s);
|
||||
|
||||
|
||||
@interface NSDictionary (RSParserInternal)
|
||||
|
||||
- (nullable id)rsparser_objectForCaseInsensitiveKey:(NSString *)key;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
//
|
||||
// RSParserInternal.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 12/26/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "RSParserInternal.h"
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
|
||||
|
||||
static BOOL RSParserIsNil(id obj) {
|
||||
|
||||
return obj == nil || obj == [NSNull null];
|
||||
}
|
||||
|
||||
BOOL RSParserObjectIsEmpty(id obj) {
|
||||
|
||||
if (RSParserIsNil(obj)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ([obj respondsToSelector:@selector(count)]) {
|
||||
return [obj count] < 1;
|
||||
}
|
||||
|
||||
if ([obj respondsToSelector:@selector(length)]) {
|
||||
return [obj length] < 1;
|
||||
}
|
||||
|
||||
return NO; /*Shouldn't get here very often.*/
|
||||
}
|
||||
|
||||
BOOL RSParserStringIsEmpty(NSString *s) {
|
||||
|
||||
return RSParserIsNil(s) || s.length < 1;
|
||||
}
|
||||
|
||||
|
||||
@implementation NSDictionary (RSParserInternal)
|
||||
|
||||
- (nullable id)rsparser_objectForCaseInsensitiveKey:(NSString *)key {
|
||||
|
||||
id obj = self[key];
|
||||
if (obj) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
for (NSString *oneKey in self.allKeys) {
|
||||
|
||||
if ([oneKey isKindOfClass:[NSString class]] && [key caseInsensitiveCompare:oneKey] == NSOrderedSame) {
|
||||
return self[oneKey];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// RSRSSParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 1/6/15.
|
||||
// Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@class ParserData;
|
||||
@class RSParsedFeed;
|
||||
|
||||
@interface RSRSSParser : NSObject
|
||||
|
||||
+ (RSParsedFeed *)parseFeedWithData:(ParserData *)parserData;
|
||||
|
||||
|
||||
@end
|
|
@ -1,523 +0,0 @@
|
|||
//
|
||||
// RSRSSParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 1/6/15.
|
||||
// Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSRSSParser.h"
|
||||
#import "RSSAXParser.h"
|
||||
#import "RSParsedFeed.h"
|
||||
#import "RSParsedArticle.h"
|
||||
#import "RSParserInternal.h"
|
||||
#import "NSString+RSParser.h"
|
||||
#import "RSDateParser.h"
|
||||
#import "ParserData.h"
|
||||
#import "RSParsedEnclosure.h"
|
||||
#import "RSParsedAuthor.h"
|
||||
|
||||
|
||||
|
||||
#import <libxml/xmlstring.h>
|
||||
|
||||
|
||||
@interface RSRSSParser () <RSSAXParserDelegate>
|
||||
|
||||
@property (nonatomic) NSData *feedData;
|
||||
@property (nonatomic) NSString *urlString;
|
||||
@property (nonatomic) NSDictionary *currentAttributes;
|
||||
@property (nonatomic) RSSAXParser *parser;
|
||||
@property (nonatomic) NSMutableArray *articles;
|
||||
@property (nonatomic) BOOL parsingArticle;
|
||||
@property (nonatomic) BOOL parsingAuthor;
|
||||
@property (nonatomic, readonly) RSParsedArticle *currentArticle;
|
||||
@property (nonatomic) BOOL parsingChannelImage;
|
||||
@property (nonatomic, readonly) NSDate *currentDate;
|
||||
@property (nonatomic) BOOL endRSSFound;
|
||||
@property (nonatomic) NSString *link;
|
||||
@property (nonatomic) NSString *title;
|
||||
@property (nonatomic) NSDate *dateParsed;
|
||||
@property (nonatomic) BOOL isRDF;
|
||||
@property (nonatomic) NSString *language;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSRSSParser
|
||||
|
||||
#pragma mark - Class Methods
|
||||
|
||||
+ (RSParsedFeed *)parseFeedWithData:(ParserData *)parserData {
|
||||
|
||||
RSRSSParser *parser = [[[self class] alloc] initWithParserData:parserData];
|
||||
return [parser parseFeed];
|
||||
}
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithParserData:(ParserData *)parserData {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_feedData = parserData.data;
|
||||
_urlString = parserData.url;
|
||||
_parser = [[RSSAXParser alloc] initWithDelegate:self];
|
||||
_articles = [NSMutableArray new];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - API
|
||||
|
||||
- (RSParsedFeed *)parseFeed {
|
||||
|
||||
[self parse];
|
||||
|
||||
RSParsedFeed *parsedFeed = [[RSParsedFeed alloc] initWithURLString:self.urlString title:self.title link:self.link language:self.language articles:self.articles];
|
||||
|
||||
return parsedFeed;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Constants
|
||||
|
||||
static NSString *kIsPermaLinkKey = @"isPermaLink";
|
||||
static NSString *kURLKey = @"url";
|
||||
static NSString *kLengthKey = @"length";
|
||||
static NSString *kTypeKey = @"type";
|
||||
static NSString *kFalseValue = @"false";
|
||||
static NSString *kTrueValue = @"true";
|
||||
static NSString *kContentEncodedKey = @"content:encoded";
|
||||
static NSString *kDCDateKey = @"dc:date";
|
||||
static NSString *kDCCreatorKey = @"dc:creator";
|
||||
static NSString *kRDFAboutKey = @"rdf:about";
|
||||
|
||||
static const char *kItem = "item";
|
||||
static const NSInteger kItemLength = 5;
|
||||
|
||||
static const char *kImage = "image";
|
||||
static const NSInteger kImageLength = 6;
|
||||
|
||||
static const char *kLink = "link";
|
||||
static const NSInteger kLinkLength = 5;
|
||||
|
||||
static const char *kTitle = "title";
|
||||
static const NSInteger kTitleLength = 6;
|
||||
|
||||
static const char *kDC = "dc";
|
||||
static const NSInteger kDCLength = 3;
|
||||
|
||||
static const char *kCreator = "creator";
|
||||
static const NSInteger kCreatorLength = 8;
|
||||
|
||||
static const char *kDate = "date";
|
||||
static const NSInteger kDateLength = 5;
|
||||
|
||||
static const char *kContent = "content";
|
||||
static const NSInteger kContentLength = 8;
|
||||
|
||||
static const char *kEncoded = "encoded";
|
||||
static const NSInteger kEncodedLength = 8;
|
||||
|
||||
static const char *kGuid = "guid";
|
||||
static const NSInteger kGuidLength = 5;
|
||||
|
||||
static const char *kPubDate = "pubDate";
|
||||
static const NSInteger kPubDateLength = 8;
|
||||
|
||||
static const char *kAuthor = "author";
|
||||
static const NSInteger kAuthorLength = 7;
|
||||
|
||||
static const char *kDescription = "description";
|
||||
static const NSInteger kDescriptionLength = 12;
|
||||
|
||||
static const char *kRSS = "rss";
|
||||
static const NSInteger kRSSLength = 4;
|
||||
|
||||
static const char *kURL = "url";
|
||||
static const NSInteger kURLLength = 4;
|
||||
|
||||
static const char *kLength = "length";
|
||||
static const NSInteger kLengthLength = 7;
|
||||
|
||||
static const char *kType = "type";
|
||||
static const NSInteger kTypeLength = 5;
|
||||
|
||||
static const char *kIsPermaLink = "isPermaLink";
|
||||
static const NSInteger kIsPermaLinkLength = 12;
|
||||
|
||||
static const char *kRDF = "rdf";
|
||||
static const NSInteger kRDFlength = 4;
|
||||
|
||||
static const char *kAbout = "about";
|
||||
static const NSInteger kAboutLength = 6;
|
||||
|
||||
static const char *kFalse = "false";
|
||||
static const NSInteger kFalseLength = 6;
|
||||
|
||||
static const char *kTrue = "true";
|
||||
static const NSInteger kTrueLength = 5;
|
||||
|
||||
static const char *kUppercaseRDF = "RDF";
|
||||
static const NSInteger kUppercaseRDFLength = 4;
|
||||
|
||||
static const char *kEnclosure = "enclosure";
|
||||
static const NSInteger kEnclosureLength = 10;
|
||||
|
||||
static const char *kLanguage = "language";
|
||||
static const NSInteger kLanguageLength = 9;
|
||||
|
||||
#pragma mark - Parsing
|
||||
|
||||
- (void)parse {
|
||||
|
||||
self.dateParsed = [NSDate date];
|
||||
|
||||
@autoreleasepool {
|
||||
[self.parser parseData:self.feedData];
|
||||
[self.parser finishParsing];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)addArticle {
|
||||
|
||||
RSParsedArticle *article = [[RSParsedArticle alloc] initWithFeedURL:self.urlString];
|
||||
article.dateParsed = self.dateParsed;
|
||||
|
||||
[self.articles addObject:article];
|
||||
}
|
||||
|
||||
|
||||
- (RSParsedArticle *)currentArticle {
|
||||
|
||||
return self.articles.lastObject;
|
||||
}
|
||||
|
||||
|
||||
- (void)addFeedElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix {
|
||||
|
||||
if (prefix != NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kLink, kLinkLength)) {
|
||||
if (!self.link) {
|
||||
self.link = [self currentString];
|
||||
}
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) {
|
||||
self.title = [self currentString];
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kLanguage, kLanguageLength)) {
|
||||
self.language = [self currentString];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addAuthorWithString:(NSString *)authorString {
|
||||
|
||||
if (RSParserStringIsEmpty(authorString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
RSParsedAuthor *author = [RSParsedAuthor authorWithSingleString:[self currentString]];
|
||||
[self.currentArticle addAuthor:author];
|
||||
}
|
||||
|
||||
- (void)addDCElement:(const xmlChar *)localName {
|
||||
|
||||
if (RSSAXEqualTags(localName, kCreator, kCreatorLength)) {
|
||||
[self addAuthorWithString:[self currentString]];
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kDate, kDateLength)) {
|
||||
self.currentArticle.datePublished = self.currentDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)addGuid {
|
||||
|
||||
NSString *guid = [self currentString];
|
||||
self.currentArticle.guid = guid;
|
||||
|
||||
NSString *isPermaLinkValue = [self.currentAttributes rsparser_objectForCaseInsensitiveKey:@"ispermalink"];
|
||||
if (!isPermaLinkValue || ![isPermaLinkValue isEqualToString:@"false"]) {
|
||||
if ([self stringIsProbablyAURLOrRelativePath:guid]) {
|
||||
self.currentArticle.permalink = [self urlString:guid];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addEnclosure {
|
||||
|
||||
NSDictionary *attributes = self.currentAttributes;
|
||||
NSString *url = attributes[kURLKey];
|
||||
if (!url || url.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
RSParsedEnclosure *enclosure = [[RSParsedEnclosure alloc] init];
|
||||
enclosure.url = url;
|
||||
enclosure.length = [attributes[kLengthKey] integerValue];
|
||||
enclosure.mimeType = attributes[kTypeKey];
|
||||
|
||||
[self.currentArticle addEnclosure:enclosure];
|
||||
}
|
||||
|
||||
- (BOOL)stringIsProbablyAURLOrRelativePath:(NSString *)s {
|
||||
|
||||
/*The RSS guid is defined as a permalink, except when it appears like this:
|
||||
<guid isPermaLink="false">some—identifier</guid>
|
||||
However, people often seem to think it’s *not* a permalink by default, even
|
||||
though it is. So we try to detect the situation where the value is not a URL string,
|
||||
and not even a relative path. This may need to evolve over time as we find
|
||||
feeds broken in different ways.*/
|
||||
|
||||
if (![s rsparser_contains:@"/"]) {
|
||||
// This seems to be just about the best possible check.
|
||||
// Bad guids are often just integers, for instance.
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([s.lowercaseString hasPrefix:@"tag:"]) { // A common non-URL guid form
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSString *)urlString:(NSString *)s {
|
||||
|
||||
/*Resolve against home page URL (if available) or feed URL.*/
|
||||
|
||||
if ([[s lowercaseString] hasPrefix:@"http"]) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (!self.link) {
|
||||
//TODO: get feed URL and use that to resolve URL.*/
|
||||
return s;
|
||||
}
|
||||
|
||||
NSURL *baseURL = [NSURL URLWithString:self.link];
|
||||
if (!baseURL) {
|
||||
return s;
|
||||
}
|
||||
|
||||
NSURL *resolvedURL = [NSURL URLWithString:s relativeToURL:baseURL];
|
||||
if (resolvedURL.absoluteString) {
|
||||
return resolvedURL.absoluteString;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)currentString {
|
||||
|
||||
return self.parser.currentStringWithTrimmedWhitespace;
|
||||
}
|
||||
|
||||
|
||||
- (void)addArticleElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix {
|
||||
|
||||
if (RSSAXEqualTags(prefix, kDC, kDCLength)) {
|
||||
|
||||
[self addDCElement:localName];
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(prefix, kContent, kContentLength) && RSSAXEqualTags(localName, kEncoded, kEncodedLength)) {
|
||||
NSString *s = [self currentString];
|
||||
if (!RSParserStringIsEmpty(s)) {
|
||||
self.currentArticle.body = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (prefix != NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kGuid, kGuidLength)) {
|
||||
[self addGuid];
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kPubDate, kPubDateLength)) {
|
||||
self.currentArticle.datePublished = self.currentDate;
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) {
|
||||
[self addAuthorWithString:[self currentString]];
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kLink, kLinkLength)) {
|
||||
self.currentArticle.link = [self urlString:[self currentString]];
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kDescription, kDescriptionLength)) {
|
||||
|
||||
if (!self.currentArticle.body) {
|
||||
self.currentArticle.body = [self currentString];
|
||||
}
|
||||
}
|
||||
else if (!self.parsingAuthor && RSSAXEqualTags(localName, kTitle, kTitleLength)) {
|
||||
NSString *articleTitle = [self currentString];
|
||||
if (articleTitle != nil) {
|
||||
self.currentArticle.title = articleTitle;
|
||||
}
|
||||
}
|
||||
else if (RSSAXEqualTags(localName, kEnclosure, kEnclosureLength)) {
|
||||
[self addEnclosure];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSDate *)currentDate {
|
||||
|
||||
return RSDateWithBytes(self.parser.currentCharacters.bytes, self.parser.currentCharacters.length);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - RSSAXParserDelegate
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes {
|
||||
|
||||
if (self.endRSSFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(localName, kUppercaseRDF, kUppercaseRDFLength)) {
|
||||
self.isRDF = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *xmlAttributes = nil;
|
||||
if ((self.isRDF && RSSAXEqualTags(localName, kItem, kItemLength)) || RSSAXEqualTags(localName, kGuid, kGuidLength) || RSSAXEqualTags(localName, kEnclosure, kEnclosureLength)) {
|
||||
xmlAttributes = [self.parser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
|
||||
}
|
||||
if (self.currentAttributes != xmlAttributes) {
|
||||
self.currentAttributes = xmlAttributes;
|
||||
}
|
||||
|
||||
if (!prefix && RSSAXEqualTags(localName, kItem, kItemLength)) {
|
||||
|
||||
[self addArticle];
|
||||
self.parsingArticle = YES;
|
||||
|
||||
if (self.isRDF && xmlAttributes && xmlAttributes[kRDFAboutKey]) { /*RSS 1.0 guid*/
|
||||
self.currentArticle.guid = xmlAttributes[kRDFAboutKey];
|
||||
self.currentArticle.permalink = self.currentArticle.guid;
|
||||
}
|
||||
}
|
||||
|
||||
else if (!prefix && RSSAXEqualTags(localName, kImage, kImageLength)) {
|
||||
self.parsingChannelImage = YES;
|
||||
}
|
||||
else if (!prefix && RSSAXEqualTags(localName, kAuthor, kAuthorLength)) {
|
||||
if (self.parsingArticle) {
|
||||
self.parsingAuthor = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!self.parsingChannelImage) {
|
||||
[self.parser beginStoringCharacters];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri {
|
||||
|
||||
if (self.endRSSFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.isRDF && RSSAXEqualTags(localName, kUppercaseRDF, kUppercaseRDFLength)) {
|
||||
self.endRSSFound = YES;
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kRSS, kRSSLength)) {
|
||||
self.endRSSFound = YES;
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kImage, kImageLength)) {
|
||||
self.parsingChannelImage = NO;
|
||||
}
|
||||
|
||||
else if (RSSAXEqualTags(localName, kItem, kItemLength)) {
|
||||
self.parsingArticle = NO;
|
||||
}
|
||||
|
||||
else if (self.parsingArticle) {
|
||||
[self addArticleElement:localName prefix:prefix];
|
||||
if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) {
|
||||
self.parsingAuthor = NO;
|
||||
}
|
||||
}
|
||||
|
||||
else if (!self.parsingChannelImage) {
|
||||
[self addFeedElement:localName prefix:prefix];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix {
|
||||
|
||||
if (RSSAXEqualTags(prefix, kRDF, kRDFlength)) {
|
||||
|
||||
if (RSSAXEqualTags(name, kAbout, kAboutLength)) {
|
||||
return kRDFAboutKey;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kIsPermaLink, kIsPermaLinkLength)) {
|
||||
return kIsPermaLinkKey;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kURL, kURLLength)) {
|
||||
return kURLKey;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kLength, kLengthLength)) {
|
||||
return kLengthKey;
|
||||
}
|
||||
|
||||
if (RSSAXEqualTags(name, kType, kTypeLength)) {
|
||||
return kTypeKey;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
|
||||
|
||||
return memcmp(bytes1, bytes2, length) == 0;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
|
||||
|
||||
static const NSUInteger falseLength = kFalseLength - 1;
|
||||
static const NSUInteger trueLength = kTrueLength - 1;
|
||||
|
||||
if (length == falseLength && equalBytes(bytes, kFalse, falseLength)) {
|
||||
return kFalseValue;
|
||||
}
|
||||
|
||||
if (length == trueLength && equalBytes(bytes, kTrue, trueLength)) {
|
||||
return kTrueValue;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -1,55 +0,0 @@
|
|||
//
|
||||
// RSSAXHTMLParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/6/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class RSSAXHTMLParser;
|
||||
|
||||
@protocol RSSAXHTMLParserDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
|
||||
- (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLStartElement:(const unsigned char *)localName attributes:(const unsigned char *_Nullable*_Nullable)attributes;
|
||||
|
||||
- (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLEndElement:(nullable const unsigned char *)localName;
|
||||
|
||||
// Length is guaranteed to be greater than 0.
|
||||
- (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLCharactersFound:(nullable const unsigned char *)characters length:(NSUInteger)length;
|
||||
|
||||
- (void)saxParserDidReachEndOfDocument:(RSSAXHTMLParser *)SAXParser; // If canceled, may not get called (but might).
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface RSSAXHTMLParser : NSObject
|
||||
|
||||
|
||||
- (instancetype)initWithDelegate:(id<RSSAXHTMLParserDelegate>)delegate;
|
||||
|
||||
- (void)parseData:(NSData *)data;
|
||||
- (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes;
|
||||
- (void)finishParsing;
|
||||
- (void)cancel;
|
||||
|
||||
@property (nullable, nonatomic, strong, readonly) NSData *currentCharacters; // nil if not storing characters. UTF-8 encoded.
|
||||
@property (nullable, nonatomic, strong, readonly) NSString *currentString; // Convenience to get string version of currentCharacters.
|
||||
@property (nullable, nonatomic, strong, readonly) NSString *currentStringWithTrimmedWhitespace;
|
||||
|
||||
- (void)beginStoringCharacters; // Delegate can call from XMLStartElement. Characters will be available in XMLEndElement as currentCharacters property. Storing characters is stopped after each XMLEndElement.
|
||||
|
||||
// Delegate can call from within XMLStartElement.
|
||||
|
||||
- (nullable NSDictionary *)attributesDictionary:(const unsigned char *_Nullable*_Nullable)attributes;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
//
|
||||
// RSSAXHTMLParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/6/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSSAXHTMLParser.h"
|
||||
#import "RSSAXParser.h"
|
||||
#import "RSParserInternal.h"
|
||||
|
||||
#import <libxml/tree.h>
|
||||
#import <libxml/xmlstring.h>
|
||||
#import <libxml/HTMLparser.h>
|
||||
|
||||
|
||||
|
||||
@interface RSSAXHTMLParser ()
|
||||
|
||||
@property (nonatomic) id<RSSAXHTMLParserDelegate> delegate;
|
||||
@property (nonatomic, assign) htmlParserCtxtPtr context;
|
||||
@property (nonatomic, assign) BOOL storingCharacters;
|
||||
@property (nonatomic) NSMutableData *characters;
|
||||
@property (nonatomic) BOOL delegateRespondsToStartElementMethod;
|
||||
@property (nonatomic) BOOL delegateRespondsToEndElementMethod;
|
||||
@property (nonatomic) BOOL delegateRespondsToCharactersFoundMethod;
|
||||
@property (nonatomic) BOOL delegateRespondsToEndOfDocumentMethod;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSSAXHTMLParser
|
||||
|
||||
|
||||
+ (void)initialize {
|
||||
|
||||
RSSAXInitLibXMLParser();
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithDelegate:(id<RSSAXHTMLParserDelegate>)delegate {
|
||||
|
||||
self = [super init];
|
||||
if (self == nil)
|
||||
return nil;
|
||||
|
||||
_delegate = delegate;
|
||||
|
||||
if ([_delegate respondsToSelector:@selector(saxParser:XMLStartElement:attributes:)]) {
|
||||
_delegateRespondsToStartElementMethod = YES;
|
||||
}
|
||||
if ([_delegate respondsToSelector:@selector(saxParser:XMLEndElement:)]) {
|
||||
_delegateRespondsToEndElementMethod = YES;
|
||||
}
|
||||
if ([_delegate respondsToSelector:@selector(saxParser:XMLCharactersFound:length:)]) {
|
||||
_delegateRespondsToCharactersFoundMethod = YES;
|
||||
}
|
||||
if ([_delegate respondsToSelector:@selector(saxParserDidReachEndOfDocument:)]) {
|
||||
_delegateRespondsToEndOfDocumentMethod = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Dealloc
|
||||
|
||||
- (void)dealloc {
|
||||
|
||||
if (_context != nil) {
|
||||
htmlFreeParserCtxt(_context);
|
||||
_context = nil;
|
||||
}
|
||||
_delegate = nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - API
|
||||
|
||||
static xmlSAXHandler saxHandlerStruct;
|
||||
|
||||
- (void)parseData:(NSData *)data {
|
||||
|
||||
[self parseBytes:data.bytes numberOfBytes:data.length];
|
||||
}
|
||||
|
||||
|
||||
- (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes {
|
||||
|
||||
if (self.context == nil) {
|
||||
|
||||
xmlCharEncoding characterEncoding = xmlDetectCharEncoding(bytes, (int)numberOfBytes);
|
||||
self.context = htmlCreatePushParserCtxt(&saxHandlerStruct, (__bridge void *)self, nil, 0, nil, characterEncoding);
|
||||
htmlCtxtUseOptions(self.context, XML_PARSE_RECOVER | XML_PARSE_NONET | HTML_PARSE_COMPACT);
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
htmlParseChunk(self.context, (const char *)bytes, (int)numberOfBytes, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)finishParsing {
|
||||
|
||||
NSAssert(self.context != nil, nil);
|
||||
if (self.context == nil)
|
||||
return;
|
||||
|
||||
@autoreleasepool {
|
||||
htmlParseChunk(self.context, nil, 0, 1);
|
||||
htmlFreeParserCtxt(self.context);
|
||||
self.context = nil;
|
||||
self.characters = nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)cancel {
|
||||
|
||||
@autoreleasepool {
|
||||
xmlStopParser(self.context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void)beginStoringCharacters {
|
||||
self.storingCharacters = YES;
|
||||
self.characters = [NSMutableData new];
|
||||
}
|
||||
|
||||
|
||||
- (void)endStoringCharacters {
|
||||
self.storingCharacters = NO;
|
||||
self.characters = nil;
|
||||
}
|
||||
|
||||
|
||||
- (NSData *)currentCharacters {
|
||||
|
||||
if (!self.storingCharacters) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self.characters;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)currentString {
|
||||
|
||||
NSData *d = self.currentCharacters;
|
||||
if (RSParserObjectIsEmpty(d)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)currentStringWithTrimmedWhitespace {
|
||||
|
||||
return [self.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Attributes Dictionary
|
||||
|
||||
- (NSDictionary *)attributesDictionary:(const xmlChar **)attributes {
|
||||
|
||||
if (!attributes) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableDictionary *d = [NSMutableDictionary new];
|
||||
|
||||
NSInteger ix = 0;
|
||||
NSString *currentKey = nil;
|
||||
while (true) {
|
||||
|
||||
const xmlChar *oneAttribute = attributes[ix];
|
||||
ix++;
|
||||
|
||||
if (!currentKey && !oneAttribute) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!currentKey) {
|
||||
currentKey = [NSString stringWithUTF8String:(const char *)oneAttribute];
|
||||
}
|
||||
else {
|
||||
NSString *value = nil;
|
||||
if (oneAttribute) {
|
||||
value = [NSString stringWithUTF8String:(const char *)oneAttribute];
|
||||
}
|
||||
|
||||
d[currentKey] = value ? value : @"";
|
||||
currentKey = nil;
|
||||
}
|
||||
}
|
||||
|
||||
return [d copy];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Callbacks
|
||||
|
||||
- (void)xmlEndDocument {
|
||||
|
||||
@autoreleasepool {
|
||||
if (self.delegateRespondsToEndOfDocumentMethod) {
|
||||
[self.delegate saxParserDidReachEndOfDocument:self];
|
||||
}
|
||||
|
||||
[self endStoringCharacters];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)xmlCharactersFound:(const xmlChar *)ch length:(NSUInteger)length {
|
||||
|
||||
if (length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
if (self.storingCharacters) {
|
||||
[self.characters appendBytes:(const void *)ch length:length];
|
||||
}
|
||||
|
||||
if (self.delegateRespondsToCharactersFoundMethod) {
|
||||
[self.delegate saxParser:self XMLCharactersFound:ch length:length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)xmlStartElement:(const xmlChar *)localName attributes:(const xmlChar **)attributes {
|
||||
|
||||
@autoreleasepool {
|
||||
if (self.delegateRespondsToStartElementMethod) {
|
||||
|
||||
[self.delegate saxParser:self XMLStartElement:localName attributes:attributes];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)xmlEndElement:(const xmlChar *)localName {
|
||||
|
||||
@autoreleasepool {
|
||||
if (self.delegateRespondsToEndElementMethod) {
|
||||
[self.delegate saxParser:self XMLEndElement:localName];
|
||||
}
|
||||
|
||||
[self endStoringCharacters];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static void startElementSAX(void *context, const xmlChar *localname, const xmlChar **attributes) {
|
||||
|
||||
[(__bridge RSSAXHTMLParser *)context xmlStartElement:localname attributes:attributes];
|
||||
}
|
||||
|
||||
|
||||
static void endElementSAX(void *context, const xmlChar *localname) {
|
||||
[(__bridge RSSAXHTMLParser *)context xmlEndElement:localname];
|
||||
}
|
||||
|
||||
|
||||
static void charactersFoundSAX(void *context, const xmlChar *ch, int len) {
|
||||
[(__bridge RSSAXHTMLParser *)context xmlCharactersFound:ch length:(NSUInteger)len];
|
||||
}
|
||||
|
||||
|
||||
static void endDocumentSAX(void *context) {
|
||||
[(__bridge RSSAXHTMLParser *)context xmlEndDocument];
|
||||
}
|
||||
|
||||
|
||||
static htmlSAXHandler saxHandlerStruct = {
|
||||
nil, /* internalSubset */
|
||||
nil, /* isStandalone */
|
||||
nil, /* hasInternalSubset */
|
||||
nil, /* hasExternalSubset */
|
||||
nil, /* resolveEntity */
|
||||
nil, /* getEntity */
|
||||
nil, /* entityDecl */
|
||||
nil, /* notationDecl */
|
||||
nil, /* attributeDecl */
|
||||
nil, /* elementDecl */
|
||||
nil, /* unparsedEntityDecl */
|
||||
nil, /* setDocumentLocator */
|
||||
nil, /* startDocument */
|
||||
endDocumentSAX, /* endDocument */
|
||||
startElementSAX, /* startElement*/
|
||||
endElementSAX, /* endElement */
|
||||
nil, /* reference */
|
||||
charactersFoundSAX, /* characters */
|
||||
nil, /* ignorableWhitespace */
|
||||
nil, /* processingInstruction */
|
||||
nil, /* comment */
|
||||
nil, /* warning */
|
||||
nil, /* error */
|
||||
nil, /* fatalError //: unused error() get all the errors */
|
||||
nil, /* getParameterEntity */
|
||||
nil, /* cdataBlock */
|
||||
nil, /* externalSubset */
|
||||
XML_SAX2_MAGIC,
|
||||
nil,
|
||||
nil, /* startElementNs */
|
||||
nil, /* endElementNs */
|
||||
nil /* serror */
|
||||
};
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
//
|
||||
// RSSAXParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/25/15.
|
||||
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
/*Thread-safe, not re-entrant.
|
||||
|
||||
Calls to the delegate will happen on the same thread where the parser runs.
|
||||
|
||||
This is a low-level streaming XML parser, a thin wrapper for libxml2's SAX parser. It doesn't do much Foundation-ifying quite on purpose -- because the goal is performance and low memory use.
|
||||
|
||||
This class is not meant to be sub-classed. Use the delegate methods.
|
||||
*/
|
||||
|
||||
|
||||
@class RSSAXParser;
|
||||
|
||||
@protocol RSSAXParserDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const unsigned char *)localName prefix:(const unsigned char *)prefix uri:(const unsigned char *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const unsigned char **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const unsigned char **)attributes;
|
||||
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const unsigned char *)localName prefix:(const unsigned char *)prefix uri:(const unsigned char *)uri;
|
||||
|
||||
// Length is guaranteed to be greater than 0.
|
||||
- (void)saxParser:(RSSAXParser *)SAXParser XMLCharactersFound:(const unsigned char *)characters length:(NSUInteger)length;
|
||||
|
||||
- (void)saxParserDidReachEndOfDocument:(RSSAXParser *)SAXParser; /*If canceled, may not get called (but might).*/
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const unsigned char *)name prefix:(const unsigned char *)prefix; /*Okay to return nil. Prefix may be nil.*/
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
void RSSAXInitLibXMLParser(void); // Needed by RSSAXHTMLParser.
|
||||
|
||||
/*For use by delegate.*/
|
||||
|
||||
BOOL RSSAXEqualTags(const unsigned char *localName, const char *tag, NSInteger tagLength);
|
||||
|
||||
|
||||
@interface RSSAXParser : NSObject
|
||||
|
||||
- (instancetype)initWithDelegate:(id<RSSAXParserDelegate>)delegate;
|
||||
|
||||
- (void)parseData:(NSData *)data;
|
||||
- (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes;
|
||||
- (void)finishParsing;
|
||||
- (void)cancel;
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *currentCharacters; /*nil if not storing characters. UTF-8 encoded.*/
|
||||
@property (nonatomic, strong, readonly) NSString *currentString; /*Convenience to get string version of currentCharacters.*/
|
||||
@property (nonatomic, strong, readonly) NSString *currentStringWithTrimmedWhitespace;
|
||||
|
||||
- (void)beginStoringCharacters; /*Delegate can call from XMLStartElement. Characters will be available in XMLEndElement as currentCharacters property. Storing characters is stopped after each XMLEndElement.*/
|
||||
|
||||
/*Delegate can call from within XMLStartElement. Returns nil if numberOfAttributes < 1.*/
|
||||
|
||||
- (NSDictionary *)attributesDictionary:(const unsigned char **)attributes numberOfAttributes:(NSInteger)numberOfAttributes;
|
||||
|
||||
@end
|
|
@ -1,353 +0,0 @@
|
|||
//
|
||||
// RSSAXParser.m
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 3/25/15.
|
||||
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSSAXParser.h"
|
||||
#import "RSParserInternal.h"
|
||||
|
||||
#import <libxml/parser.h>
|
||||
#import <libxml/tree.h>
|
||||
#import <libxml/xmlstring.h>
|
||||
|
||||
|
||||
|
||||
@interface RSSAXParser ()
|
||||
|
||||
@property (nonatomic, weak) id<RSSAXParserDelegate> delegate;
|
||||
@property (nonatomic, assign) xmlParserCtxtPtr context;
|
||||
@property (nonatomic, assign) BOOL storingCharacters;
|
||||
@property (nonatomic) NSMutableData *characters;
|
||||
@property (nonatomic) BOOL delegateRespondsToInternedStringMethod;
|
||||
@property (nonatomic) BOOL delegateRespondsToInternedStringForValueMethod;
|
||||
@property (nonatomic) BOOL delegateRespondsToStartElementMethod;
|
||||
@property (nonatomic) BOOL delegateRespondsToEndElementMethod;
|
||||
@property (nonatomic) BOOL delegateRespondsToCharactersFoundMethod;
|
||||
@property (nonatomic) BOOL delegateRespondsToEndOfDocumentMethod;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSSAXParser
|
||||
|
||||
+ (void)initialize {
|
||||
|
||||
RSSAXInitLibXMLParser();
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithDelegate:(id<RSSAXParserDelegate>)delegate {
|
||||
|
||||
self = [super init];
|
||||
if (self == nil)
|
||||
return nil;
|
||||
|
||||
_delegate = delegate;
|
||||
|
||||
if ([_delegate respondsToSelector:@selector(saxParser:internedStringForName:prefix:)]) {
|
||||
_delegateRespondsToInternedStringMethod = YES;
|
||||
}
|
||||
if ([_delegate respondsToSelector:@selector(saxParser:internedStringForValue:length:)]) {
|
||||
_delegateRespondsToInternedStringForValueMethod = YES;
|
||||
}
|
||||
if ([_delegate respondsToSelector:@selector(saxParser:XMLStartElement:prefix:uri:numberOfNamespaces:namespaces:numberOfAttributes:numberDefaulted:attributes:)]) {
|
||||
_delegateRespondsToStartElementMethod = YES;
|
||||
}
|
||||
if ([_delegate respondsToSelector:@selector(saxParser:XMLEndElement:prefix:uri:)]) {
|
||||
_delegateRespondsToEndElementMethod = YES;
|
||||
}
|
||||
if ([_delegate respondsToSelector:@selector(saxParser:XMLCharactersFound:length:)]) {
|
||||
_delegateRespondsToCharactersFoundMethod = YES;
|
||||
}
|
||||
if ([_delegate respondsToSelector:@selector(saxParserDidReachEndOfDocument:)]) {
|
||||
_delegateRespondsToEndOfDocumentMethod = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Dealloc
|
||||
|
||||
- (void)dealloc {
|
||||
if (_context != nil) {
|
||||
xmlFreeParserCtxt(_context);
|
||||
_context = nil;
|
||||
}
|
||||
_delegate = nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - API
|
||||
|
||||
static xmlSAXHandler saxHandlerStruct;
|
||||
|
||||
- (void)parseData:(NSData *)data {
|
||||
|
||||
[self parseBytes:data.bytes numberOfBytes:data.length];
|
||||
}
|
||||
|
||||
|
||||
- (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes {
|
||||
|
||||
if (self.context == nil) {
|
||||
|
||||
self.context = xmlCreatePushParserCtxt(&saxHandlerStruct, (__bridge void *)self, nil, 0, nil);
|
||||
xmlCtxtUseOptions(self.context, XML_PARSE_RECOVER | XML_PARSE_NOENT);
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
xmlParseChunk(self.context, (const char *)bytes, (int)numberOfBytes, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)finishParsing {
|
||||
|
||||
NSAssert(self.context != nil, nil);
|
||||
if (self.context == nil)
|
||||
return;
|
||||
|
||||
@autoreleasepool {
|
||||
xmlParseChunk(self.context, nil, 0, 1);
|
||||
xmlFreeParserCtxt(self.context);
|
||||
self.context = nil;
|
||||
self.characters = nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)cancel {
|
||||
|
||||
@autoreleasepool {
|
||||
xmlStopParser(self.context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)beginStoringCharacters {
|
||||
self.storingCharacters = YES;
|
||||
self.characters = [NSMutableData new];
|
||||
}
|
||||
|
||||
|
||||
- (void)endStoringCharacters {
|
||||
self.storingCharacters = NO;
|
||||
self.characters = nil;
|
||||
}
|
||||
|
||||
|
||||
- (NSData *)currentCharacters {
|
||||
|
||||
if (!self.storingCharacters) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self.characters;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)currentString {
|
||||
|
||||
NSData *d = self.currentCharacters;
|
||||
if (RSParserObjectIsEmpty(d)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)currentStringWithTrimmedWhitespace {
|
||||
|
||||
return [self.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Attributes Dictionary
|
||||
|
||||
- (NSDictionary *)attributesDictionary:(const xmlChar **)attributes numberOfAttributes:(NSInteger)numberOfAttributes {
|
||||
|
||||
if (numberOfAttributes < 1 || !attributes) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableDictionary *d = [NSMutableDictionary new];
|
||||
|
||||
@autoreleasepool {
|
||||
NSInteger i = 0, j = 0;
|
||||
for (i = 0, j = 0; i < numberOfAttributes; i++, j+=5) {
|
||||
|
||||
NSUInteger lenValue = (NSUInteger)(attributes[j + 4] - attributes[j + 3]);
|
||||
NSString *value = nil;
|
||||
|
||||
if (self.delegateRespondsToInternedStringForValueMethod) {
|
||||
value = [self.delegate saxParser:self internedStringForValue:(const void *)attributes[j + 3] length:lenValue];
|
||||
}
|
||||
if (!value) {
|
||||
value = [[NSString alloc] initWithBytes:(const void *)attributes[j + 3] length:lenValue encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
NSString *attributeName = nil;
|
||||
|
||||
if (self.delegateRespondsToInternedStringMethod) {
|
||||
attributeName = [self.delegate saxParser:self internedStringForName:(const xmlChar *)attributes[j] prefix:(const xmlChar *)attributes[j + 1]];
|
||||
}
|
||||
|
||||
if (!attributeName) {
|
||||
attributeName = [NSString stringWithUTF8String:(const char *)attributes[j]];
|
||||
if (attributes[j + 1]) {
|
||||
NSString *attributePrefix = [NSString stringWithUTF8String:(const char *)attributes[j + 1]];
|
||||
attributeName = [NSString stringWithFormat:@"%@:%@", attributePrefix, attributeName];
|
||||
}
|
||||
}
|
||||
|
||||
if (value && attributeName) {
|
||||
d[attributeName] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Equal Tags
|
||||
|
||||
BOOL RSSAXEqualTags(const xmlChar *localName, const char *tag, NSInteger tagLength) {
|
||||
|
||||
if (!localName) {
|
||||
return NO;
|
||||
}
|
||||
return !strncmp((const char *)localName, tag, (size_t)tagLength);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Callbacks
|
||||
|
||||
- (void)xmlEndDocument {
|
||||
|
||||
@autoreleasepool {
|
||||
if (self.delegateRespondsToEndOfDocumentMethod) {
|
||||
[self.delegate saxParserDidReachEndOfDocument:self];
|
||||
}
|
||||
|
||||
[self endStoringCharacters];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)xmlCharactersFound:(const xmlChar *)ch length:(NSUInteger)length {
|
||||
|
||||
if (length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
if (self.storingCharacters) {
|
||||
[self.characters appendBytes:(const void *)ch length:length];
|
||||
}
|
||||
|
||||
if (self.delegateRespondsToCharactersFoundMethod) {
|
||||
[self.delegate saxParser:self XMLCharactersFound:ch length:length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)xmlStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(int)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(int)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes {
|
||||
|
||||
@autoreleasepool {
|
||||
if (self.delegateRespondsToStartElementMethod) {
|
||||
|
||||
[self.delegate saxParser:self XMLStartElement:localName prefix:prefix uri:uri numberOfNamespaces:numberOfNamespaces namespaces:namespaces numberOfAttributes:numberOfAttributes numberDefaulted:numberDefaulted attributes:attributes];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)xmlEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri {
|
||||
|
||||
@autoreleasepool {
|
||||
if (self.delegateRespondsToEndElementMethod) {
|
||||
[self.delegate saxParser:self XMLEndElement:localName prefix:prefix uri:uri];
|
||||
}
|
||||
|
||||
[self endStoringCharacters];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static void startElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes) {
|
||||
|
||||
[(__bridge RSSAXParser *)context xmlStartElement:localname prefix:prefix uri:URI numberOfNamespaces:nb_namespaces namespaces:namespaces numberOfAttributes:nb_attributes numberDefaulted:nb_defaulted attributes:attributes];
|
||||
}
|
||||
|
||||
|
||||
static void endElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI) {
|
||||
[(__bridge RSSAXParser *)context xmlEndElement:localname prefix:prefix uri:URI];
|
||||
}
|
||||
|
||||
|
||||
static void charactersFoundSAX(void *context, const xmlChar *ch, int len) {
|
||||
[(__bridge RSSAXParser *)context xmlCharactersFound:ch length:(NSUInteger)len];
|
||||
}
|
||||
|
||||
|
||||
static void endDocumentSAX(void *context) {
|
||||
[(__bridge RSSAXParser *)context xmlEndDocument];
|
||||
}
|
||||
|
||||
|
||||
static xmlSAXHandler saxHandlerStruct = {
|
||||
nil, /* internalSubset */
|
||||
nil, /* isStandalone */
|
||||
nil, /* hasInternalSubset */
|
||||
nil, /* hasExternalSubset */
|
||||
nil, /* resolveEntity */
|
||||
nil, /* getEntity */
|
||||
nil, /* entityDecl */
|
||||
nil, /* notationDecl */
|
||||
nil, /* attributeDecl */
|
||||
nil, /* elementDecl */
|
||||
nil, /* unparsedEntityDecl */
|
||||
nil, /* setDocumentLocator */
|
||||
nil, /* startDocument */
|
||||
endDocumentSAX, /* endDocument */
|
||||
nil, /* startElement*/
|
||||
nil, /* endElement */
|
||||
nil, /* reference */
|
||||
charactersFoundSAX, /* characters */
|
||||
nil, /* ignorableWhitespace */
|
||||
nil, /* processingInstruction */
|
||||
nil, /* comment */
|
||||
nil, /* warning */
|
||||
nil, /* error */
|
||||
nil, /* fatalError //: unused error() get all the errors */
|
||||
nil, /* getParameterEntity */
|
||||
nil, /* cdataBlock */
|
||||
nil, /* externalSubset */
|
||||
XML_SAX2_MAGIC,
|
||||
nil,
|
||||
startElementSAX, /* startElementNs */
|
||||
endElementSAX, /* endElementNs */
|
||||
nil /* serror */
|
||||
};
|
||||
|
||||
|
||||
void RSSAXInitLibXMLParser(void) {
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
xmlInitParser();
|
||||
});
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
//
|
||||
// RSParser.h
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/20/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
|
||||
#import "../ParserData.h"
|
||||
#import "../RSDateParser.h"
|
||||
|
||||
// OPML
|
||||
|
||||
#import "../RSOPMLParser.h"
|
||||
#import "../RSOPMLDocument.h"
|
||||
#import "../RSOPMLItem.h"
|
||||
#import "../RSOPMLAttributes.h"
|
||||
#import "../RSOPMLFeedSpecifier.h"
|
||||
#import "../RSOPMLError.h"
|
||||
|
||||
// For writing your own XML parser.
|
||||
|
||||
#import "../RSSAXParser.h"
|
||||
|
||||
// You should use FeedParser (Swift) instead of these two specific parsers
|
||||
// and the objects they create.
|
||||
// But they’re available if you want them.
|
||||
|
||||
#import "../RSRSSParser.h"
|
||||
#import "../RSAtomParser.h"
|
||||
#import "../RSParsedFeed.h"
|
||||
#import "../RSParsedArticle.h"
|
||||
#import "../RSParsedEnclosure.h"
|
||||
#import "../RSParsedAuthor.h"
|
||||
|
||||
// HTML
|
||||
|
||||
#import "../RSHTMLMetadataParser.h"
|
||||
#import "../RSHTMLMetadata.h"
|
||||
#import "../RSHTMLLinkParser.h"
|
||||
#import "../RSSAXHTMLParser.h" // For writing your own HTML parser.
|
||||
#import "../RSHTMLTag.h"
|
||||
|
||||
// Utilities
|
||||
|
||||
#import "../NSData+RSParser.h"
|
||||
#import "../NSString+RSParser.h"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
//
|
||||
// Exports.swift
|
||||
//
|
||||
//
|
||||
// Created by Stuart Breckenridge on 29/7/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@_exported import RSParserObjC
|
|
@ -1,91 +0,0 @@
|
|||
//
|
||||
// FeedParser.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/20/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSParserObjC
|
||||
|
||||
// FeedParser handles RSS, Atom, JSON Feed, and RSS-in-JSON.
|
||||
// You don’t need to know the type of feed.
|
||||
|
||||
public typealias FeedParserCallback = (_ parsedFeed: ParsedFeed?, _ error: Error?) -> Void
|
||||
|
||||
public struct FeedParser {
|
||||
|
||||
private static let parseQueue = DispatchQueue(label: "FeedParser parse queue")
|
||||
|
||||
public static func canParse(_ parserData: ParserData) -> Bool {
|
||||
|
||||
let type = feedType(parserData)
|
||||
|
||||
switch type {
|
||||
case .jsonFeed, .rssInJSON, .rss, .atom:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public static func mightBeAbleToParseBasedOnPartialData(_ parserData: ParserData) -> Bool {
|
||||
|
||||
let type = feedType(parserData, isPartialData: true)
|
||||
|
||||
switch type {
|
||||
case .jsonFeed, .rssInJSON, .rss, .atom, .unknown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse(_ parserData: ParserData) throws -> ParsedFeed? {
|
||||
|
||||
// This is generally fast enough to call on the main thread —
|
||||
// but it’s probably a good idea to use a background queue if
|
||||
// you might be doing a lot of parsing. (Such as in a feed reader.)
|
||||
|
||||
do {
|
||||
let type = feedType(parserData)
|
||||
|
||||
switch type {
|
||||
|
||||
case .jsonFeed:
|
||||
return try JSONFeedParser.parse(parserData)
|
||||
|
||||
case .rssInJSON:
|
||||
return try RSSInJSONParser.parse(parserData)
|
||||
|
||||
case .rss:
|
||||
return RSSParser.parse(parserData)
|
||||
|
||||
case .atom:
|
||||
return AtomParser.parse(parserData)
|
||||
|
||||
case .unknown, .notAFeed:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
catch { throw error }
|
||||
}
|
||||
|
||||
public static func parse(_ parserData: ParserData, _ completion: @escaping FeedParserCallback) {
|
||||
|
||||
parseQueue.async {
|
||||
do {
|
||||
let parsedFeed = try parse(parserData)
|
||||
DispatchQueue.main.async {
|
||||
completion(parsedFeed, nil)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
//
|
||||
// FeedParserError.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/24/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct FeedParserError: Error {
|
||||
|
||||
public enum FeedParserErrorType {
|
||||
|
||||
case rssChannelNotFound
|
||||
case rssItemsNotFound
|
||||
case jsonFeedVersionNotFound
|
||||
case jsonFeedItemsNotFound
|
||||
case jsonFeedTitleNotFound
|
||||
case invalidJSON
|
||||
}
|
||||
|
||||
public let errorType: FeedParserErrorType
|
||||
|
||||
public init(_ errorType: FeedParserErrorType) {
|
||||
|
||||
self.errorType = errorType
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
//
|
||||
// FeedType.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/20/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if SWIFT_PACKAGE
|
||||
import RSParserObjC
|
||||
#endif
|
||||
|
||||
public enum FeedType {
|
||||
case rss
|
||||
case atom
|
||||
case jsonFeed
|
||||
case rssInJSON
|
||||
case unknown
|
||||
case notAFeed
|
||||
}
|
||||
|
||||
|
||||
private let minNumberOfBytesRequired = 128
|
||||
|
||||
public func feedType(_ parserData: ParserData, isPartialData: Bool = false) -> FeedType {
|
||||
|
||||
// Can call with partial data — while still downloading, for instance.
|
||||
// If there’s not enough data, return .unknown. Ask again when there’s more data.
|
||||
// If it’s definitely not a feed, return .notAFeed.
|
||||
//
|
||||
// This is fast enough to call on the main thread.
|
||||
|
||||
if parserData.data.count < minNumberOfBytesRequired {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
let nsdata = parserData.data as NSData
|
||||
|
||||
if nsdata.isProbablyJSONFeed() {
|
||||
return .jsonFeed
|
||||
}
|
||||
if nsdata.isProbablyRSSInJSON() {
|
||||
return .rssInJSON
|
||||
}
|
||||
if nsdata.isProbablyRSS() {
|
||||
return .rss
|
||||
}
|
||||
if nsdata.isProbablyAtom() {
|
||||
return .atom
|
||||
}
|
||||
|
||||
if isPartialData && nsdata.isProbablyJSON() {
|
||||
// Might not be able to detect a JSON Feed without all data.
|
||||
// Dr. Drang’s JSON Feed (see althis.json and allthis-partial.json in tests)
|
||||
// has, at this writing, the JSON version element at the end of the feed,
|
||||
// which is totally legal — but it means not being able to detect
|
||||
// that it’s a JSON Feed without all the data.
|
||||
// So this returns .unknown instead of .notAFeed.
|
||||
return .unknown
|
||||
}
|
||||
|
||||
return .notAFeed
|
||||
}
|
|
@ -1,250 +0,0 @@
|
|||
//
|
||||
// JSONFeedParser.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/25/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if SWIFT_PACKAGE
|
||||
import RSParserObjC
|
||||
#endif
|
||||
|
||||
// See https://jsonfeed.org/version/1.1
|
||||
|
||||
public struct JSONFeedParser {
|
||||
|
||||
struct Key {
|
||||
static let version = "version"
|
||||
static let items = "items"
|
||||
static let title = "title"
|
||||
static let homePageURL = "home_page_url"
|
||||
static let feedURL = "feed_url"
|
||||
static let feedDescription = "description"
|
||||
static let nextURL = "next_url"
|
||||
static let icon = "icon"
|
||||
static let favicon = "favicon"
|
||||
static let expired = "expired"
|
||||
static let author = "author"
|
||||
static let authors = "authors"
|
||||
static let name = "name"
|
||||
static let url = "url"
|
||||
static let avatar = "avatar"
|
||||
static let hubs = "hubs"
|
||||
static let type = "type"
|
||||
static let contentHTML = "content_html"
|
||||
static let contentText = "content_text"
|
||||
static let externalURL = "external_url"
|
||||
static let summary = "summary"
|
||||
static let image = "image"
|
||||
static let bannerImage = "banner_image"
|
||||
static let datePublished = "date_published"
|
||||
static let dateModified = "date_modified"
|
||||
static let tags = "tags"
|
||||
static let uniqueID = "id"
|
||||
static let attachments = "attachments"
|
||||
static let mimeType = "mime_type"
|
||||
static let sizeInBytes = "size_in_bytes"
|
||||
static let durationInSeconds = "duration_in_seconds"
|
||||
static let language = "language"
|
||||
}
|
||||
|
||||
static let jsonFeedVersionMarker = "://jsonfeed.org/version/" // Allow for the mistake of not getting the scheme exactly correct.
|
||||
|
||||
public static func parse(_ parserData: ParserData) throws -> ParsedFeed? {
|
||||
|
||||
guard let d = JSONUtilities.dictionary(with: parserData.data) else {
|
||||
throw FeedParserError(.invalidJSON)
|
||||
}
|
||||
|
||||
guard let version = d[Key.version] as? String, let _ = version.range(of: JSONFeedParser.jsonFeedVersionMarker) else {
|
||||
throw FeedParserError(.jsonFeedVersionNotFound)
|
||||
}
|
||||
guard let itemsArray = d[Key.items] as? JSONArray else {
|
||||
throw FeedParserError(.jsonFeedItemsNotFound)
|
||||
}
|
||||
guard let title = d[Key.title] as? String else {
|
||||
throw FeedParserError(.jsonFeedTitleNotFound)
|
||||
}
|
||||
|
||||
let authors = parseAuthors(d)
|
||||
let homePageURL = d[Key.homePageURL] as? String
|
||||
let feedURL = d[Key.feedURL] as? String ?? parserData.url
|
||||
let feedDescription = d[Key.feedDescription] as? String
|
||||
let nextURL = d[Key.nextURL] as? String
|
||||
let iconURL = d[Key.icon] as? String
|
||||
let faviconURL = d[Key.favicon] as? String
|
||||
let expired = d[Key.expired] as? Bool ?? false
|
||||
let hubs = parseHubs(d)
|
||||
let language = d[Key.language] as? String
|
||||
|
||||
let items = parseItems(itemsArray, parserData.url)
|
||||
|
||||
return ParsedFeed(type: .jsonFeed, title: title, homePageURL: homePageURL, feedURL: feedURL, language: language, feedDescription: feedDescription, nextURL: nextURL, iconURL: iconURL, faviconURL: faviconURL, authors: authors, expired: expired, hubs: hubs, items: items)
|
||||
}
|
||||
}
|
||||
|
||||
private extension JSONFeedParser {
|
||||
|
||||
static func parseAuthors(_ dictionary: JSONDictionary) -> Set<ParsedAuthor>? {
|
||||
|
||||
if let authorsArray = dictionary[Key.authors] as? JSONArray {
|
||||
var authors = Set<ParsedAuthor>()
|
||||
for author in authorsArray {
|
||||
if let parsedAuthor = parseAuthor(author) {
|
||||
authors.insert(parsedAuthor)
|
||||
}
|
||||
}
|
||||
return authors
|
||||
}
|
||||
|
||||
guard let authorDictionary = dictionary[Key.author] as? JSONDictionary,
|
||||
let parsedAuthor = parseAuthor(authorDictionary) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Set([parsedAuthor])
|
||||
}
|
||||
|
||||
static func parseAuthor(_ dictionary: JSONDictionary) -> ParsedAuthor? {
|
||||
let name = dictionary[Key.name] as? String
|
||||
let url = dictionary[Key.url] as? String
|
||||
let avatar = dictionary[Key.avatar] as? String
|
||||
if name == nil && url == nil && avatar == nil {
|
||||
return nil
|
||||
}
|
||||
return ParsedAuthor(name: name, url: url, avatarURL: avatar, emailAddress: nil)
|
||||
}
|
||||
|
||||
static func parseHubs(_ dictionary: JSONDictionary) -> Set<ParsedHub>? {
|
||||
|
||||
guard let hubsArray = dictionary[Key.hubs] as? JSONArray else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let hubs = hubsArray.compactMap { (hubDictionary) -> ParsedHub? in
|
||||
guard let hubURL = hubDictionary[Key.url] as? String, let hubType = hubDictionary[Key.type] as? String else {
|
||||
return nil
|
||||
}
|
||||
return ParsedHub(type: hubType, url: hubURL)
|
||||
}
|
||||
return hubs.isEmpty ? nil : Set(hubs)
|
||||
}
|
||||
|
||||
static func parseItems(_ itemsArray: JSONArray, _ feedURL: String) -> Set<ParsedItem> {
|
||||
|
||||
return Set(itemsArray.compactMap { (oneItemDictionary) -> ParsedItem? in
|
||||
return parseItem(oneItemDictionary, feedURL)
|
||||
})
|
||||
}
|
||||
|
||||
static func parseItem(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? {
|
||||
|
||||
guard let uniqueID = parseUniqueID(itemDictionary) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let contentHTML = itemDictionary[Key.contentHTML] as? String
|
||||
let contentText = itemDictionary[Key.contentText] as? String
|
||||
if contentHTML == nil && contentText == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
let url = itemDictionary[Key.url] as? String
|
||||
let externalURL = itemDictionary[Key.externalURL] as? String
|
||||
let title = parseTitle(itemDictionary, feedURL)
|
||||
let language = itemDictionary[Key.language] as? String
|
||||
let summary = itemDictionary[Key.summary] as? String
|
||||
let imageURL = itemDictionary[Key.image] as? String
|
||||
let bannerImageURL = itemDictionary[Key.bannerImage] as? String
|
||||
|
||||
let datePublished = parseDate(itemDictionary[Key.datePublished] as? String)
|
||||
let dateModified = parseDate(itemDictionary[Key.dateModified] as? String)
|
||||
|
||||
let authors = parseAuthors(itemDictionary)
|
||||
var tags: Set<String>? = nil
|
||||
if let tagsArray = itemDictionary[Key.tags] as? [String] {
|
||||
tags = Set(tagsArray)
|
||||
}
|
||||
let attachments = parseAttachments(itemDictionary)
|
||||
|
||||
return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: feedURL, url: url, externalURL: externalURL, title: title, language: language, contentHTML: contentHTML, contentText: contentText, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments)
|
||||
}
|
||||
|
||||
static func parseTitle(_ itemDictionary: JSONDictionary, _ feedURL: String) -> String? {
|
||||
|
||||
guard let title = itemDictionary[Key.title] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isSpecialCaseTitleWithEntitiesFeed(feedURL) {
|
||||
return (title as NSString).rsparser_stringByDecodingHTMLEntities()
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
static func isSpecialCaseTitleWithEntitiesFeed(_ feedURL: String) -> Bool {
|
||||
|
||||
// As of 16 Feb. 2018, Kottke’s and Heer’s feeds includes HTML entities in the title elements.
|
||||
// If we find more feeds like this, we’ll add them here. If these feeds get fixed, we’ll remove them.
|
||||
|
||||
let lowerFeedURL = feedURL.lowercased()
|
||||
let matchStrings = ["kottke.org", "pxlnv.com", "macstories.net", "macobserver.com"]
|
||||
for matchString in matchStrings {
|
||||
if lowerFeedURL.contains(matchString) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
static func parseUniqueID(_ itemDictionary: JSONDictionary) -> String? {
|
||||
|
||||
if let uniqueID = itemDictionary[Key.uniqueID] as? String {
|
||||
return uniqueID // Spec says it must be a string
|
||||
}
|
||||
// Version 1 spec also says that if it’s a number, even though that’s incorrect, it should be coerced to a string.
|
||||
if let uniqueID = itemDictionary[Key.uniqueID] as? Int {
|
||||
return "\(uniqueID)"
|
||||
}
|
||||
if let uniqueID = itemDictionary[Key.uniqueID] as? Double {
|
||||
return "\(uniqueID)"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func parseDate(_ dateString: String?) -> Date? {
|
||||
|
||||
guard let dateString = dateString, !dateString.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return RSDateWithString(dateString)
|
||||
}
|
||||
|
||||
static func parseAttachments(_ itemDictionary: JSONDictionary) -> Set<ParsedAttachment>? {
|
||||
|
||||
guard let attachmentsArray = itemDictionary[Key.attachments] as? JSONArray else {
|
||||
return nil
|
||||
}
|
||||
return Set(attachmentsArray.compactMap { parseAttachment($0) })
|
||||
}
|
||||
|
||||
static func parseAttachment(_ attachmentObject: JSONDictionary) -> ParsedAttachment? {
|
||||
|
||||
guard let url = attachmentObject[Key.url] as? String else {
|
||||
return nil
|
||||
}
|
||||
guard let mimeType = attachmentObject[Key.mimeType] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = attachmentObject[Key.title] as? String
|
||||
let sizeInBytes = attachmentObject[Key.sizeInBytes] as? Int
|
||||
let durationInSeconds = attachmentObject[Key.durationInSeconds] as? Int
|
||||
|
||||
return ParsedAttachment(url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds)
|
||||
}
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
//
|
||||
// RSSInJSONParser.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/24/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if SWIFT_PACKAGE
|
||||
import RSParserObjC
|
||||
#endif
|
||||
|
||||
// See https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md
|
||||
// Also: http://cyber.harvard.edu/rss/rss.html
|
||||
|
||||
public struct RSSInJSONParser {
|
||||
|
||||
public static func parse(_ parserData: ParserData) throws -> ParsedFeed? {
|
||||
|
||||
do {
|
||||
guard let parsedObject = try JSONSerialization.jsonObject(with: parserData.data) as? JSONDictionary else {
|
||||
throw FeedParserError(.invalidJSON)
|
||||
}
|
||||
guard let rssObject = parsedObject["rss"] as? JSONDictionary else {
|
||||
throw FeedParserError(.rssChannelNotFound)
|
||||
}
|
||||
guard let channelObject = rssObject["channel"] as? JSONDictionary else {
|
||||
throw FeedParserError(.rssChannelNotFound)
|
||||
}
|
||||
|
||||
// I’d bet money that in practice the items array won’t always appear correctly inside the channel object.
|
||||
// I’d also bet that sometimes it gets called "items" instead of "item".
|
||||
var itemsObject = channelObject["item"] as? JSONArray
|
||||
if itemsObject == nil {
|
||||
itemsObject = parsedObject["item"] as? JSONArray
|
||||
}
|
||||
if itemsObject == nil {
|
||||
itemsObject = channelObject["items"] as? JSONArray
|
||||
}
|
||||
if itemsObject == nil {
|
||||
itemsObject = parsedObject["items"] as? JSONArray
|
||||
}
|
||||
if itemsObject == nil {
|
||||
throw FeedParserError(.rssItemsNotFound)
|
||||
}
|
||||
|
||||
let title = channelObject["title"] as? String
|
||||
let homePageURL = channelObject["link"] as? String
|
||||
let feedURL = parserData.url
|
||||
let feedDescription = channelObject["description"] as? String
|
||||
let feedLanguage = channelObject["language"] as? String
|
||||
|
||||
let items = parseItems(itemsObject!, parserData.url)
|
||||
|
||||
return ParsedFeed(type: .rssInJSON, title: title, homePageURL: homePageURL, feedURL: feedURL, language: feedLanguage, feedDescription: feedDescription, nextURL: nil, iconURL: nil, faviconURL: nil, authors: nil, expired: false, hubs: nil, items: items)
|
||||
|
||||
}
|
||||
catch { throw error }
|
||||
}
|
||||
}
|
||||
|
||||
private extension RSSInJSONParser {
|
||||
|
||||
static func parseItems(_ itemsObject: JSONArray, _ feedURL: String) -> Set<ParsedItem> {
|
||||
|
||||
return Set(itemsObject.compactMap{ (oneItemDictionary) -> ParsedItem? in
|
||||
|
||||
return parsedItemWithDictionary(oneItemDictionary, feedURL)
|
||||
})
|
||||
}
|
||||
|
||||
static func parsedItemWithDictionary(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? {
|
||||
|
||||
let externalURL = itemDictionary["link"] as? String
|
||||
let title = itemDictionary["title"] as? String
|
||||
|
||||
var contentHTML = itemDictionary["description"] as? String
|
||||
var contentText: String? = nil
|
||||
if contentHTML != nil && !(contentHTML!.contains("<")) {
|
||||
contentText = contentHTML
|
||||
contentHTML = nil
|
||||
}
|
||||
if contentHTML == nil && contentText == nil && title == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var datePublished: Date? = nil
|
||||
if let datePublishedString = itemDictionary["pubDate"] as? String {
|
||||
datePublished = RSDateWithString(datePublishedString)
|
||||
}
|
||||
|
||||
let authors = parseAuthors(itemDictionary)
|
||||
let tags = parseTags(itemDictionary)
|
||||
let attachments = parseAttachments(itemDictionary)
|
||||
|
||||
var uniqueID: String? = itemDictionary["guid"] as? String
|
||||
if uniqueID == nil {
|
||||
|
||||
// Calculate a uniqueID based on a combination of non-empty elements. Then hash the result.
|
||||
// Items should have guids. When they don't, re-runs are very likely
|
||||
// because there's no other 100% reliable way to determine identity.
|
||||
// This calculated uniqueID is valid only for this particular feed. (Just like ids in JSON Feed.)
|
||||
|
||||
var s = ""
|
||||
if let datePublished = datePublished {
|
||||
s += "\(datePublished.timeIntervalSince1970)"
|
||||
}
|
||||
if let title = title {
|
||||
s += title
|
||||
}
|
||||
if let externalURL = externalURL {
|
||||
s += externalURL
|
||||
}
|
||||
if let authorEmailAddress = authors?.first?.emailAddress {
|
||||
s += authorEmailAddress
|
||||
}
|
||||
if let oneAttachmentURL = attachments?.first?.url {
|
||||
s += oneAttachmentURL
|
||||
}
|
||||
if s.isEmpty {
|
||||
// Sheesh. Tough case.
|
||||
if let _ = contentHTML {
|
||||
s = contentHTML!
|
||||
}
|
||||
if let _ = contentText {
|
||||
s = contentText!
|
||||
}
|
||||
}
|
||||
uniqueID = (s as NSString).rsparser_md5Hash()
|
||||
}
|
||||
|
||||
if let uniqueID = uniqueID {
|
||||
return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: feedURL, url: nil, externalURL: externalURL, title: title, language: nil, contentHTML: contentHTML, contentText: contentText, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: datePublished, dateModified: nil, authors: authors, tags: tags, attachments: attachments)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func parseAuthors(_ itemDictionary: JSONDictionary) -> Set<ParsedAuthor>? {
|
||||
|
||||
guard let authorEmailAddress = itemDictionary["author"] as? String else {
|
||||
return nil
|
||||
}
|
||||
let parsedAuthor = ParsedAuthor(name: nil, url: nil, avatarURL: nil, emailAddress: authorEmailAddress)
|
||||
return Set([parsedAuthor])
|
||||
}
|
||||
|
||||
static func parseTags(_ itemDictionary: JSONDictionary) -> Set<String>? {
|
||||
|
||||
if let categoryObject = itemDictionary["category"] as? JSONDictionary {
|
||||
if let oneTag = categoryObject["#value"] as? String {
|
||||
return Set([oneTag])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
else if let categoryArray = itemDictionary["category"] as? JSONArray {
|
||||
return Set(categoryArray.compactMap{ $0["#value"] as? String })
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func parseAttachments(_ itemDictionary: JSONDictionary) -> Set<ParsedAttachment>? {
|
||||
|
||||
guard let enclosureObject = itemDictionary["enclosure"] as? JSONDictionary else {
|
||||
return nil
|
||||
}
|
||||
guard let attachmentURL = enclosureObject["url"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var attachmentSize = enclosureObject["length"] as? Int
|
||||
if attachmentSize == nil {
|
||||
if let attachmentSizeString = enclosureObject["length"] as? String {
|
||||
attachmentSize = (attachmentSizeString as NSString).integerValue
|
||||
}
|
||||
}
|
||||
|
||||
let type = enclosureObject["type"] as? String
|
||||
if let attachment = ParsedAttachment(url: attachmentURL, mimeType: type, title: nil, sizeInBytes: attachmentSize, durationInSeconds: nil) {
|
||||
return Set([attachment])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
//
|
||||
// ParsedAttachment.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/20/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ParsedAttachment: Hashable {
|
||||
|
||||
public let url: String
|
||||
public let mimeType: String?
|
||||
public let title: String?
|
||||
public let sizeInBytes: Int?
|
||||
public let durationInSeconds: Int?
|
||||
|
||||
public init?(url: String, mimeType: String?, title: String?, sizeInBytes: Int?, durationInSeconds: Int?) {
|
||||
if url.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.url = url
|
||||
self.mimeType = mimeType
|
||||
self.title = title
|
||||
self.sizeInBytes = sizeInBytes
|
||||
self.durationInSeconds = durationInSeconds
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(url)
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
//
|
||||
// ParsedAuthor.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/20/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ParsedAuthor: Hashable, Codable {
|
||||
|
||||
public let name: String?
|
||||
public let url: String?
|
||||
public let avatarURL: String?
|
||||
public let emailAddress: String?
|
||||
|
||||
public init(name: String?, url: String?, avatarURL: String?, emailAddress: String?) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.avatarURL = avatarURL
|
||||
self.emailAddress = emailAddress
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
if let name = name {
|
||||
hasher.combine(name)
|
||||
}
|
||||
else if let url = url {
|
||||
hasher.combine(url)
|
||||
}
|
||||
else if let emailAddress = emailAddress {
|
||||
hasher.combine(emailAddress)
|
||||
}
|
||||
else if let avatarURL = avatarURL {
|
||||
hasher.combine(avatarURL)
|
||||
}
|
||||
else {
|
||||
hasher.combine("")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
//
|
||||
// ParsedFeed.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/20/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ParsedFeed {
|
||||
|
||||
public let type: FeedType
|
||||
public let title: String?
|
||||
public let homePageURL: String?
|
||||
public let feedURL: String?
|
||||
public let language: String?
|
||||
public let feedDescription: String?
|
||||
public let nextURL: String?
|
||||
public let iconURL: String?
|
||||
public let faviconURL: String?
|
||||
public let authors: Set<ParsedAuthor>?
|
||||
public let expired: Bool
|
||||
public let hubs: Set<ParsedHub>?
|
||||
public let items: Set<ParsedItem>
|
||||
|
||||
public init(type: FeedType, title: String?, homePageURL: String?, feedURL: String?, language: String?, feedDescription: String?, nextURL: String?, iconURL: String?, faviconURL: String?, authors: Set<ParsedAuthor>?, expired: Bool, hubs: Set<ParsedHub>?, items: Set<ParsedItem>) {
|
||||
self.type = type
|
||||
self.title = title
|
||||
self.homePageURL = homePageURL?.nilIfEmptyOrWhitespace
|
||||
self.feedURL = feedURL
|
||||
self.language = language
|
||||
self.feedDescription = feedDescription
|
||||
self.nextURL = nextURL
|
||||
self.iconURL = iconURL
|
||||
self.faviconURL = faviconURL
|
||||
self.authors = authors
|
||||
self.expired = expired
|
||||
self.hubs = hubs
|
||||
self.items = items
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// ParsedHub.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/20/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ParsedHub: Hashable {
|
||||
|
||||
public let type: String
|
||||
public let url: String
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// ParsedItem.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/20/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ParsedItem: Hashable {
|
||||
|
||||
public let syncServiceID: String? //Nil when not syncing
|
||||
public let uniqueID: String //RSS guid, for instance; may be calculated
|
||||
public let feedURL: String
|
||||
public let url: String?
|
||||
public let externalURL: String?
|
||||
public let title: String?
|
||||
public let language: String?
|
||||
public let contentHTML: String?
|
||||
public let contentText: String?
|
||||
public let summary: String?
|
||||
public let imageURL: String?
|
||||
public let bannerImageURL: String?
|
||||
public let datePublished: Date?
|
||||
public let dateModified: Date?
|
||||
public let authors: Set<ParsedAuthor>?
|
||||
public let tags: Set<String>?
|
||||
public let attachments: Set<ParsedAttachment>?
|
||||
|
||||
public init(syncServiceID: String?, uniqueID: String, feedURL: String, url: String?, externalURL: String?, title: String?,
|
||||
language: String?, contentHTML: String?, contentText: String?, summary: String?, imageURL: String?,
|
||||
bannerImageURL: String?,datePublished: Date?, dateModified: Date?, authors: Set<ParsedAuthor>?,
|
||||
tags: Set<String>?, attachments: Set<ParsedAttachment>?) {
|
||||
|
||||
self.syncServiceID = syncServiceID
|
||||
self.uniqueID = uniqueID
|
||||
self.feedURL = feedURL
|
||||
self.url = url
|
||||
self.externalURL = externalURL
|
||||
self.title = title
|
||||
self.language = language
|
||||
self.contentHTML = contentHTML
|
||||
self.contentText = contentText
|
||||
self.summary = summary
|
||||
self.imageURL = imageURL
|
||||
self.bannerImageURL = bannerImageURL
|
||||
self.datePublished = datePublished
|
||||
self.dateModified = dateModified
|
||||
self.authors = authors
|
||||
self.tags = tags
|
||||
self.attachments = attachments
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
if let syncServiceID = syncServiceID {
|
||||
hasher.combine(syncServiceID)
|
||||
}
|
||||
else {
|
||||
hasher.combine(uniqueID)
|
||||
hasher.combine(feedURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
//
|
||||
// AtomParser.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/25/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if SWIFT_PACKAGE
|
||||
import RSParserObjC
|
||||
#endif
|
||||
|
||||
// RSSParser wraps the Objective-C RSAtomParser.
|
||||
//
|
||||
// The Objective-C parser creates RSParsedFeed, RSParsedArticle, etc.
|
||||
// This wrapper then creates ParsedFeed, ParsedItem, etc. so that it creates
|
||||
// the same things that JSONFeedParser and RSSInJSONParser create.
|
||||
//
|
||||
// In general, you should see FeedParser.swift for all your feed-parsing needs.
|
||||
|
||||
public struct AtomParser {
|
||||
|
||||
public static func parse(_ parserData: ParserData) -> ParsedFeed? {
|
||||
|
||||
if let rsParsedFeed = RSAtomParser.parseFeed(with: parserData) {
|
||||
return RSParsedFeedTransformer.parsedFeed(rsParsedFeed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
//
|
||||
// RSParsedFeedTransformer.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/25/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if SWIFT_PACKAGE
|
||||
import RSParserObjC
|
||||
#endif
|
||||
|
||||
// RSRSSParser and RSAtomParser were written in Objective-C quite a while ago.
|
||||
// They create an RSParsedFeed object and related Objective-C objects.
|
||||
// These functions take an RSParsedFeed and return a Swift-y ParsedFeed,
|
||||
// which is part of providing a single API for feed parsing.
|
||||
|
||||
struct RSParsedFeedTransformer {
|
||||
|
||||
static func parsedFeed(_ rsParsedFeed: RSParsedFeed) -> ParsedFeed {
|
||||
|
||||
let items = parsedItems(rsParsedFeed.articles)
|
||||
return ParsedFeed(type: .rss, title: rsParsedFeed.title, homePageURL: rsParsedFeed.link, feedURL: rsParsedFeed.urlString, language: rsParsedFeed.language, feedDescription: nil, nextURL: nil, iconURL: nil, faviconURL: nil, authors: nil, expired: false, hubs: nil, items: items)
|
||||
}
|
||||
}
|
||||
|
||||
private extension RSParsedFeedTransformer {
|
||||
|
||||
static func parsedItems(_ parsedArticles: Set<RSParsedArticle>) -> Set<ParsedItem> {
|
||||
|
||||
// Create Set<ParsedItem> from Set<RSParsedArticle>
|
||||
|
||||
return Set(parsedArticles.map(parsedItem))
|
||||
}
|
||||
|
||||
static func parsedItem(_ parsedArticle: RSParsedArticle) -> ParsedItem {
|
||||
|
||||
let uniqueID = parsedArticle.articleID
|
||||
let url = parsedArticle.permalink
|
||||
let externalURL = parsedArticle.link
|
||||
let title = parsedArticle.title
|
||||
let language = parsedArticle.language
|
||||
let contentHTML = parsedArticle.body
|
||||
let datePublished = parsedArticle.datePublished
|
||||
let dateModified = parsedArticle.dateModified
|
||||
let authors = parsedAuthors(parsedArticle.authors)
|
||||
let attachments = parsedAttachments(parsedArticle.enclosures)
|
||||
|
||||
return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: parsedArticle.feedURL, url: url, externalURL: externalURL, title: title, language: language, contentHTML: contentHTML, contentText: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: nil, attachments: attachments)
|
||||
}
|
||||
|
||||
static func parsedAuthors(_ authors: Set<RSParsedAuthor>?) -> Set<ParsedAuthor>? {
|
||||
|
||||
guard let authors = authors, !authors.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let transformedAuthors = authors.compactMap { (author) -> ParsedAuthor? in
|
||||
return ParsedAuthor(name: author.name, url: author.url, avatarURL: nil, emailAddress: author.emailAddress)
|
||||
}
|
||||
|
||||
return transformedAuthors.isEmpty ? nil : Set(transformedAuthors)
|
||||
}
|
||||
|
||||
static func parsedAttachments(_ enclosures: Set<RSParsedEnclosure>?) -> Set<ParsedAttachment>? {
|
||||
|
||||
guard let enclosures = enclosures, !enclosures.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let attachments = enclosures.compactMap { (enclosure) -> ParsedAttachment? in
|
||||
|
||||
let sizeInBytes = enclosure.length > 0 ? enclosure.length : nil
|
||||
return ParsedAttachment(url: enclosure.url, mimeType: enclosure.mimeType, title: nil, sizeInBytes: sizeInBytes, durationInSeconds: nil)
|
||||
}
|
||||
|
||||
return attachments.isEmpty ? nil : Set(attachments)
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
//
|
||||
// RSSParser.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/25/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSParserObjC
|
||||
|
||||
// RSSParser wraps the Objective-C RSRSSParser.
|
||||
//
|
||||
// The Objective-C parser creates RSParsedFeed, RSParsedArticle, etc.
|
||||
// This wrapper then creates ParsedFeed, ParsedItem, etc. so that it creates
|
||||
// the same things that JSONFeedParser and RSSInJSONParser create.
|
||||
//
|
||||
// In general, you should see FeedParser.swift for all your feed-parsing needs.
|
||||
|
||||
public struct RSSParser {
|
||||
|
||||
public static func parse(_ parserData: ParserData) -> ParsedFeed? {
|
||||
|
||||
if let rsParsedFeed = RSRSSParser.parseFeed(with: parserData) {
|
||||
return RSParsedFeedTransformer.parsedFeed(rsParsedFeed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
//
|
||||
// JSONDictionary.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/24/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias JSONDictionary = [String: Any]
|
||||
public typealias JSONArray = [JSONDictionary]
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// JSONUtilities.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 12/10/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct JSONUtilities {
|
||||
|
||||
public static func object(with data: Data) -> Any? {
|
||||
|
||||
return try? JSONSerialization.jsonObject(with: data)
|
||||
}
|
||||
|
||||
public static func dictionary(with data: Data) -> JSONDictionary? {
|
||||
|
||||
return object(with: data) as? JSONDictionary
|
||||
}
|
||||
|
||||
public static func array(with data: Data) -> JSONArray? {
|
||||
|
||||
return object(with: data) as? JSONArray
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
//
|
||||
// String+RSParser.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Nate Weaver on 2020-01-19.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
var nilIfEmptyOrWhitespace: String? {
|
||||
return self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self
|
||||
}
|
||||
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
//
|
||||
// AtomParserTests.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/26/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
|
||||
class AtomParserTests: XCTestCase {
|
||||
|
||||
func testDaringFireballPerformance() {
|
||||
|
||||
// 0.009 sec on my 2012 iMac.
|
||||
let d = parserData("DaringFireball", "atom", "http://daringfireball.net/") //It’s actually an Atom feed
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testAllThisPerformance() {
|
||||
|
||||
// 0.003 sec on my 2012 iMac.
|
||||
let d = parserData("allthis", "atom", "http://leancrew.com/all-this")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testGettingHomePageLink() {
|
||||
|
||||
let d = parserData("allthis", "atom", "http://leancrew.com/all-this")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
XCTAssertTrue(parsedFeed.homePageURL == "http://leancrew.com/all-this")
|
||||
}
|
||||
|
||||
func testDaringFireball() {
|
||||
|
||||
let d = parserData("DaringFireball", "atom", "http://daringfireball.net/") //It’s actually an Atom feed
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
|
||||
XCTAssertNotNil(article.url)
|
||||
|
||||
XCTAssertTrue(article.uniqueID.hasPrefix("tag:daringfireball.net,2017:/"))
|
||||
|
||||
XCTAssertEqual(article.authors!.count, 1) // TODO: parse Atom authors
|
||||
let author = article.authors!.first!
|
||||
if author.name == "Daring Fireball Department of Commerce" {
|
||||
XCTAssertNil(author.url)
|
||||
}
|
||||
else {
|
||||
XCTAssertEqual(author.name, "John Gruber")
|
||||
XCTAssertEqual(author.url, "http://daringfireball.net/")
|
||||
}
|
||||
|
||||
XCTAssertNotNil(article.datePublished)
|
||||
XCTAssert(article.attachments == nil)
|
||||
|
||||
XCTAssertEqual(article.language, "en")
|
||||
}
|
||||
}
|
||||
|
||||
func test4fsodonlineAttachments() {
|
||||
|
||||
// Thanks to Marco for finding me some Atom podcast feeds. Apparently they’re super-rare.
|
||||
|
||||
let d = parserData("4fsodonline", "atom", "http://4fsodonline.blogspot.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
|
||||
XCTAssertTrue(article.attachments!.count > 0)
|
||||
let attachment = article.attachments!.first!
|
||||
|
||||
XCTAssertTrue(attachment.url.hasPrefix("http://www.blogger.com/video-play.mp4?"))
|
||||
XCTAssertNil(attachment.sizeInBytes)
|
||||
XCTAssertEqual(attachment.mimeType!, "video/mp4")
|
||||
}
|
||||
}
|
||||
|
||||
func testExpertOpinionENTAttachments() {
|
||||
|
||||
// Another from Marco.
|
||||
|
||||
let d = parserData("expertopinionent", "atom", "http://expertopinionent.typepad.com/my-blog/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
|
||||
guard let attachments = article.attachments else {
|
||||
continue
|
||||
}
|
||||
|
||||
XCTAssertEqual(attachments.count, 1)
|
||||
let attachment = attachments.first!
|
||||
|
||||
XCTAssertTrue(attachment.url.hasSuffix(".mp3"))
|
||||
XCTAssertNil(attachment.sizeInBytes)
|
||||
XCTAssertEqual(attachment.mimeType!, "audio/mpeg")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
//
|
||||
// EntityDecodingTests.swift
|
||||
// RSParserTests
|
||||
//
|
||||
// Created by Brent Simmons on 12/30/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
|
||||
class EntityDecodingTests: XCTestCase {
|
||||
|
||||
func test39Decoding() {
|
||||
|
||||
// Bug found by Manton Reece — the ' entity was not getting decoded by NetNewsWire in JSON Feeds from micro.blog.
|
||||
|
||||
let s = "These are the times that try men's souls."
|
||||
let decoded = s.rsparser_stringByDecodingHTMLEntities()
|
||||
|
||||
XCTAssertEqual(decoded, "These are the times that try men's souls.")
|
||||
}
|
||||
|
||||
func testEntities() {
|
||||
var s = "…"
|
||||
var decoded = s.rsparser_stringByDecodingHTMLEntities()
|
||||
|
||||
XCTAssertEqual(decoded, "…")
|
||||
|
||||
s = "…"
|
||||
decoded = s.rsparser_stringByDecodingHTMLEntities()
|
||||
XCTAssertEqual(decoded, "…")
|
||||
|
||||
s = "'"
|
||||
decoded = s.rsparser_stringByDecodingHTMLEntities()
|
||||
XCTAssertEqual(decoded, "'")
|
||||
|
||||
s = "§"
|
||||
decoded = s.rsparser_stringByDecodingHTMLEntities()
|
||||
XCTAssertEqual(decoded, "§")
|
||||
|
||||
s = "£"
|
||||
decoded = s.rsparser_stringByDecodingHTMLEntities()
|
||||
XCTAssertEqual(decoded, "£")
|
||||
|
||||
}
|
||||
}
|
|
@ -1,245 +0,0 @@
|
|||
//
|
||||
// FeedParserTypeTests.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/25/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
import RSParserObjC
|
||||
|
||||
class FeedParserTypeTests: XCTestCase {
|
||||
|
||||
// MARK: HTML
|
||||
|
||||
func testDaringFireballHTMLType() {
|
||||
|
||||
let d = parserData("DaringFireball", "html", "http://daringfireball.net/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .notAFeed)
|
||||
}
|
||||
|
||||
func testFurboHTMLType() {
|
||||
|
||||
let d = parserData("furbo", "html", "http://furbo.org/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .notAFeed)
|
||||
}
|
||||
|
||||
func testInessentialHTMLType() {
|
||||
|
||||
let d = parserData("inessential", "html", "http://inessential.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .notAFeed)
|
||||
}
|
||||
|
||||
func testSixColorsHTMLType() {
|
||||
|
||||
let d = parserData("sixcolors", "html", "https://sixcolors.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .notAFeed)
|
||||
}
|
||||
|
||||
// MARK: RSS
|
||||
|
||||
func testEMarleyRSSType() {
|
||||
|
||||
let d = parserData("EMarley", "rss", "https://medium.com/@emarley")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testScriptingNewsRSSType() {
|
||||
|
||||
let d = parserData("scriptingNews", "rss", "http://scripting.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testKatieFloydRSSType() {
|
||||
|
||||
let d = parserData("KatieFloyd", "rss", "https://katiefloyd.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testMantonRSSType() {
|
||||
|
||||
let d = parserData("manton", "rss", "http://manton.org/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testDCRainmakerRSSType() {
|
||||
|
||||
let d = parserData("dcrainmaker", "xml", "https://www.dcrainmaker.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testMacworldRSSType() {
|
||||
|
||||
let d = parserData("macworld", "rss", "https://www.macworld.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testNatashaTheRobotRSSType() {
|
||||
|
||||
let d = parserData("natasha", "xml", "https://www.natashatherobot.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testDontHitSaveRSSWithBOMType() {
|
||||
|
||||
let d = parserData("donthitsave", "xml", "http://donthitsave.com/donthitsavefeed.xml")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testBioRDF() {
|
||||
let d = parserData("bio", "rdf", "http://connect.biorxiv.org/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
func testPHPXML() {
|
||||
let d = parserData("phpxml", "rss", "https://www.fcutrecht.net/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rss)
|
||||
}
|
||||
|
||||
// MARK: Atom
|
||||
|
||||
func testDaringFireballAtomType() {
|
||||
|
||||
// File extension is .rss, but it’s really an Atom feed.
|
||||
let d = parserData("DaringFireball", "rss", "http://daringfireball.net/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .atom)
|
||||
}
|
||||
|
||||
func testOneFootTsunamiAtomType() {
|
||||
|
||||
let d = parserData("OneFootTsunami", "atom", "http://onefoottsunami.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .atom)
|
||||
}
|
||||
|
||||
func testRussCoxAtomType() {
|
||||
let d = parserData("russcox", "atom", "https://research.swtch.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .atom)
|
||||
}
|
||||
|
||||
// MARK: RSS-in-JSON
|
||||
|
||||
func testScriptingNewsJSONType() {
|
||||
|
||||
let d = parserData("ScriptingNews", "json", "http://scripting.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .rssInJSON)
|
||||
}
|
||||
|
||||
// MARK: JSON Feed
|
||||
|
||||
func testInessentialJSONFeedType() {
|
||||
|
||||
let d = parserData("inessential", "json", "http://inessential.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .jsonFeed)
|
||||
}
|
||||
|
||||
func testAllThisJSONFeedType() {
|
||||
|
||||
let d = parserData("allthis", "json", "http://leancrew.com/allthis/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .jsonFeed)
|
||||
}
|
||||
|
||||
func testCurtJSONFeedType() {
|
||||
|
||||
let d = parserData("curt", "json", "http://curtclifton.net/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .jsonFeed)
|
||||
}
|
||||
|
||||
func testPixelEnvyJSONFeedType() {
|
||||
|
||||
let d = parserData("pxlnv", "json", "http://pxlnv.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .jsonFeed)
|
||||
}
|
||||
|
||||
func testRoseJSONFeedType() {
|
||||
|
||||
let d = parserData("rose", "json", "https://www.rosemaryorchard.com/")
|
||||
let type = feedType(d)
|
||||
XCTAssertTrue(type == .jsonFeed)
|
||||
}
|
||||
|
||||
// MARK: Unknown
|
||||
|
||||
func testPartialAllThisUnknownFeedType() {
|
||||
|
||||
// In the case of this feed, the partial data isn’t enough to detect that it’s a JSON Feed.
|
||||
// The type detector should return .unknown rather than .notAFeed.
|
||||
|
||||
let d = parserData("allthis-partial", "json", "http://leancrew.com/allthis/")
|
||||
let type = feedType(d, isPartialData: true)
|
||||
XCTAssertEqual(type, .unknown)
|
||||
}
|
||||
|
||||
// MARK: Performance
|
||||
|
||||
func testFeedTypePerformance() {
|
||||
|
||||
// 0.000 on my 2012 iMac.
|
||||
|
||||
let d = parserData("EMarley", "rss", "https://medium.com/@emarley")
|
||||
self.measure {
|
||||
let _ = feedType(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testFeedTypePerformance2() {
|
||||
|
||||
// 0.000 on my 2012 iMac.
|
||||
|
||||
let d = parserData("inessential", "json", "http://inessential.com/")
|
||||
self.measure {
|
||||
let _ = feedType(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testFeedTypePerformance3() {
|
||||
|
||||
// 0.000 on my 2012 iMac.
|
||||
|
||||
let d = parserData("DaringFireball", "html", "http://daringfireball.net/")
|
||||
self.measure {
|
||||
let _ = feedType(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testFeedTypePerformance4() {
|
||||
|
||||
// 0.001 on my 2012 iMac.
|
||||
|
||||
let d = parserData("DaringFireball", "rss", "http://daringfireball.net/")
|
||||
self.measure {
|
||||
let _ = feedType(d)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func parserData(_ filename: String, _ fileExtension: String, _ url: String) -> ParserData {
|
||||
let filename = "Resources/\(filename)"
|
||||
let path = Bundle.module.path(forResource: filename, ofType: fileExtension)!
|
||||
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
|
||||
return ParserData(url: url, data: data)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
//
|
||||
// HTMLLinkTests.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/25/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
import RSParserObjC
|
||||
|
||||
class HTMLLinkTests: XCTestCase {
|
||||
|
||||
func testSixColorsPerformance() {
|
||||
|
||||
// 0.003 sec on my 2012 iMac
|
||||
let d = parserData("sixcolors", "html", "http://sixcolors.com/")
|
||||
self.measure {
|
||||
let _ = RSHTMLLinkParser.htmlLinks(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
func testSixColorsLink() {
|
||||
|
||||
let d = parserData("sixcolors", "html", "http://sixcolors.com/")
|
||||
let links = RSHTMLLinkParser.htmlLinks(with: d)
|
||||
|
||||
let linkToFind = "https://www.theincomparable.com/theincomparable/290/index.php"
|
||||
let textToFind = "this week’s episode of The Incomparable"
|
||||
|
||||
var found = false
|
||||
for oneLink in links {
|
||||
if let urlString = oneLink.urlString, let text = oneLink.text, urlString == linkToFind, text == textToFind {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertTrue(found)
|
||||
XCTAssertEqual(links.count, 131)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
//
|
||||
// HTMLMetadataTests.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/25/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
import RSParserObjC
|
||||
|
||||
class HTMLMetadataTests: XCTestCase {
|
||||
|
||||
func testDaringFireball() {
|
||||
|
||||
let d = parserData("DaringFireball", "html", "http://daringfireball.net/")
|
||||
let metadata = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
|
||||
XCTAssertEqual(metadata.favicons.first?.urlString, "http://daringfireball.net/graphics/favicon.ico?v=005")
|
||||
|
||||
XCTAssertEqual(metadata.feedLinks.count, 1)
|
||||
|
||||
let feedLink = metadata.feedLinks.first!
|
||||
XCTAssertNil(feedLink.title)
|
||||
XCTAssertEqual(feedLink.type, "application/atom+xml")
|
||||
XCTAssertEqual(feedLink.urlString, "http://daringfireball.net/feeds/main")
|
||||
}
|
||||
|
||||
func testDaringFireballPerformance() {
|
||||
|
||||
// 0.002 sec on my 2012 iMac
|
||||
let d = parserData("DaringFireball", "html", "http://daringfireball.net/")
|
||||
self.measure {
|
||||
let _ = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
func testFurbo() {
|
||||
|
||||
let d = parserData("furbo", "html", "http://furbo.org/")
|
||||
let metadata = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
|
||||
XCTAssertEqual(metadata.favicons.first?.urlString, "http://furbo.org/favicon.ico")
|
||||
|
||||
XCTAssertEqual(metadata.feedLinks.count, 1)
|
||||
|
||||
let feedLink = metadata.feedLinks.first!
|
||||
XCTAssertEqual(feedLink.title, "Iconfactory News Feed")
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml")
|
||||
}
|
||||
|
||||
func testFurboPerformance() {
|
||||
|
||||
// 0.001 sec on my 2012 iMac
|
||||
let d = parserData("furbo", "html", "http://furbo.org/")
|
||||
self.measure {
|
||||
let _ = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
func testInessential() {
|
||||
|
||||
let d = parserData("inessential", "html", "http://inessential.com/")
|
||||
let metadata = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
|
||||
XCTAssertNil(metadata.favicons.first?.urlString)
|
||||
|
||||
XCTAssertEqual(metadata.feedLinks.count, 1)
|
||||
let feedLink = metadata.feedLinks.first!
|
||||
XCTAssertEqual(feedLink.title, "RSS")
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml")
|
||||
XCTAssertEqual(feedLink.urlString, "http://inessential.com/xml/rss.xml")
|
||||
|
||||
XCTAssertEqual(metadata.appleTouchIcons.count, 0);
|
||||
}
|
||||
|
||||
func testInessentialPerformance() {
|
||||
|
||||
// 0.001 sec on my 2012 iMac
|
||||
let d = parserData("inessential", "html", "http://inessential.com/")
|
||||
self.measure {
|
||||
let _ = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
func testCocoPerformance() {
|
||||
|
||||
// 0.004 sec on my 2012 iMac
|
||||
let d = parserData("coco", "html", "https://www.theatlantic.com/entertainment/archive/2017/11/coco-is-among-pixars-best-movies-in-years/546695/")
|
||||
self.measure {
|
||||
let _ = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
func testSixColors() {
|
||||
|
||||
let d = parserData("sixcolors", "html", "http://sixcolors.com/")
|
||||
let metadata = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
|
||||
XCTAssertEqual(metadata.favicons.first?.urlString, "https://sixcolors.com/images/favicon.ico")
|
||||
|
||||
XCTAssertEqual(metadata.feedLinks.count, 1);
|
||||
let feedLink = metadata.feedLinks.first!
|
||||
XCTAssertEqual(feedLink.title, "RSS");
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml");
|
||||
XCTAssertEqual(feedLink.urlString, "http://feedpress.me/sixcolors");
|
||||
|
||||
XCTAssertEqual(metadata.appleTouchIcons.count, 6);
|
||||
let icon = metadata.appleTouchIcons[3];
|
||||
XCTAssertEqual(icon.rel, "apple-touch-icon");
|
||||
XCTAssertEqual(icon.sizes, "120x120");
|
||||
XCTAssertEqual(icon.urlString, "https://sixcolors.com/apple-touch-icon-120.png");
|
||||
}
|
||||
|
||||
func testSixColorsPerformance() {
|
||||
|
||||
// 0.002 sec on my 2012 iMac
|
||||
let d = parserData("sixcolors", "html", "http://sixcolors.com/")
|
||||
self.measure {
|
||||
let _ = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
}
|
||||
}
|
||||
|
||||
func testCocoOGImage() {
|
||||
|
||||
let d = parserData("coco", "html", "https://www.theatlantic.com/entertainment/archive/2017/11/coco-is-among-pixars-best-movies-in-years/546695/")
|
||||
let metadata = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
let openGraphData = metadata.openGraphProperties
|
||||
let image = openGraphData.images.first!
|
||||
XCTAssert(image.url == "https://cdn.theatlantic.com/assets/media/img/mt/2017/11/1033101_first_full_length_trailer_arrives_pixars_coco/facebook.jpg?1511382177")
|
||||
}
|
||||
|
||||
func testCocoTwitterImage() {
|
||||
|
||||
let d = parserData("coco", "html", "https://www.theatlantic.com/entertainment/archive/2017/11/coco-is-among-pixars-best-movies-in-years/546695/")
|
||||
let metadata = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
let twitterData = metadata.twitterProperties
|
||||
let imageURL = twitterData.imageURL!
|
||||
XCTAssert(imageURL == "https://cdn.theatlantic.com/assets/media/img/mt/2017/11/1033101_first_full_length_trailer_arrives_pixars_coco/facebook.jpg?1511382177")
|
||||
}
|
||||
|
||||
func testYouTube() {
|
||||
// YouTube is a special case — the feed links appear after the head section, in the body section.
|
||||
let d = parserData("YouTubeTheVolvoRocks", "html", "https://www.youtube.com/user/TheVolvorocks")
|
||||
let metadata = RSHTMLMetadataParser.htmlMetadata(with: d)
|
||||
|
||||
XCTAssertEqual(metadata.feedLinks.count, 1);
|
||||
let feedLink = metadata.feedLinks.first!
|
||||
XCTAssertEqual(feedLink.title, "RSS");
|
||||
XCTAssertEqual(feedLink.type, "application/rss+xml");
|
||||
XCTAssertEqual(feedLink.urlString, "https://www.youtube.com/feeds/videos.xml?channel_id=UCct7QF2jcWRY6dhXWMSq9LQ");
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,124 +0,0 @@
|
|||
//
|
||||
// JSONFeedParserTests.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/26/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
|
||||
class JSONFeedParserTests: XCTestCase {
|
||||
|
||||
func testInessentialPerformance() {
|
||||
|
||||
// 0.001 sec on my 2012 iMac.
|
||||
let d = parserData("inessential", "json", "http://inessential.com/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testDaringFireballPerformance() {
|
||||
|
||||
// 0.009 sec on my 2012 iMac.
|
||||
let d = parserData("DaringFireball", "json", "http://daringfireball.net/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testGettingFaviconAndIconURLs() {
|
||||
|
||||
let d = parserData("DaringFireball", "json", "http://daringfireball.net/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
XCTAssert(parsedFeed.faviconURL == "https://daringfireball.net/graphics/favicon-64.png")
|
||||
XCTAssert(parsedFeed.iconURL == "https://daringfireball.net/graphics/apple-touch-icon.png")
|
||||
}
|
||||
|
||||
func testAllThis() {
|
||||
|
||||
let d = parserData("allthis", "json", "http://leancrew.com/allthis/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
XCTAssertEqual(parsedFeed.items.count, 12)
|
||||
}
|
||||
|
||||
func testCurt() {
|
||||
|
||||
let d = parserData("curt", "json", "http://curtclifton.net/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
XCTAssertEqual(parsedFeed.items.count, 26)
|
||||
|
||||
var didFindTwitterQuitterArticle = false
|
||||
for article in parsedFeed.items {
|
||||
if article.title == "Twitter Quitter" {
|
||||
didFindTwitterQuitterArticle = true
|
||||
XCTAssertTrue(article.contentHTML!.hasPrefix("<p>I’ve decided to close my Twitter account. William Van Hecke <a href=\"https://tinyletter.com/fet/letters/microcosmographia-xlxi-reasons-to-stay-on-twitter\">makes a convincing case</a>"))
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertTrue(didFindTwitterQuitterArticle)
|
||||
}
|
||||
|
||||
func testPixelEnvy() {
|
||||
|
||||
let d = parserData("pxlnv", "json", "http://pxlnv.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
XCTAssertEqual(parsedFeed.items.count, 20)
|
||||
|
||||
}
|
||||
|
||||
func testRose() {
|
||||
let d = parserData("rose", "json", "http://www.rosemaryorchard.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
XCTAssertEqual(parsedFeed.items.count, 84)
|
||||
}
|
||||
|
||||
func test3960() {
|
||||
let d = parserData("3960", "json", "http://journal.3960.org/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
XCTAssertEqual(parsedFeed.items.count, 20)
|
||||
XCTAssertEqual(parsedFeed.language, "de-DE")
|
||||
|
||||
for item in parsedFeed.items {
|
||||
XCTAssertEqual(item.language, "de-DE")
|
||||
}
|
||||
}
|
||||
|
||||
func testAuthors() {
|
||||
let d = parserData("authors", "json", "https://example.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
XCTAssertEqual(parsedFeed.items.count, 4)
|
||||
|
||||
let rootAuthors = Set([
|
||||
ParsedAuthor(name: "Root Author 1", url: nil, avatarURL: nil, emailAddress: nil),
|
||||
ParsedAuthor(name: "Root Author 2", url: nil, avatarURL: nil, emailAddress: nil)
|
||||
])
|
||||
let itemAuthors = Set([
|
||||
ParsedAuthor(name: "Item Author 1", url: nil, avatarURL: nil, emailAddress: nil),
|
||||
ParsedAuthor(name: "Item Author 2", url: nil, avatarURL: nil, emailAddress: nil)
|
||||
])
|
||||
let legacyItemAuthors = Set([
|
||||
ParsedAuthor(name: "Legacy Item Author", url: nil, avatarURL: nil, emailAddress: nil)
|
||||
])
|
||||
|
||||
XCTAssertEqual(parsedFeed.authors?.count, 2)
|
||||
XCTAssertEqual(parsedFeed.authors, rootAuthors)
|
||||
|
||||
let noAuthorsItem = parsedFeed.items.first { $0.uniqueID == "Item without authors" }!
|
||||
XCTAssertEqual(noAuthorsItem.authors, nil)
|
||||
|
||||
let legacyAuthorItem = parsedFeed.items.first { $0.uniqueID == "Item with legacy author" }!
|
||||
XCTAssertEqual(legacyAuthorItem.authors, legacyItemAuthors)
|
||||
|
||||
let modernAuthorsItem = parsedFeed.items.first { $0.uniqueID == "Item with modern authors" }!
|
||||
XCTAssertEqual(modernAuthorsItem.authors, itemAuthors)
|
||||
|
||||
let bothAuthorsItem = parsedFeed.items.first { $0.uniqueID == "Item with both" }!
|
||||
XCTAssertEqual(bothAuthorsItem.authors, itemAuthors)
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
//
|
||||
// OPMLTests.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/25/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
import RSParserObjC
|
||||
|
||||
class OPMLTests: XCTestCase {
|
||||
|
||||
let subsData = parserData("Subs", "opml", "http://example.org/")
|
||||
|
||||
func testOPMLParsingPerformance() {
|
||||
|
||||
// 0.002 sec on my 2012 iMac.
|
||||
self.measure {
|
||||
let _ = try! RSOPMLParser.parseOPML(with: self.subsData)
|
||||
}
|
||||
}
|
||||
|
||||
func testNotOPML() {
|
||||
|
||||
let d = parserData("DaringFireball", "rss", "http://daringfireball.net/")
|
||||
XCTAssertThrowsError(try RSOPMLParser.parseOPML(with: d))
|
||||
}
|
||||
|
||||
func testSubsStructure() {
|
||||
let opmlDocument = try! RSOPMLParser.parseOPML(with: subsData)
|
||||
XCTAssertEqual("Subs", opmlDocument.title)
|
||||
XCTAssertEqual("http://example.org/", opmlDocument.url)
|
||||
recursivelyCheckOPMLStructure(opmlDocument)
|
||||
}
|
||||
|
||||
|
||||
func testFindingTitles() {
|
||||
// https://github.com/brentsimmons/NetNewsWire/issues/527
|
||||
// Fix a bug where titles aren’t found when there’s no title attribute in the OPML,
|
||||
// which appears to be true with OPML generated by The Old Reader.
|
||||
|
||||
let d = parserData("SubsNoTitleAttributes", "opml", "http://example.org/")
|
||||
let opmlDocument = try! RSOPMLParser.parseOPML(with: d)
|
||||
recursivelyCheckOPMLStructure(opmlDocument)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension OPMLTests {
|
||||
|
||||
func recursivelyCheckOPMLStructure(_ item: RSOPMLItem) {
|
||||
let feedSpecifier = item.feedSpecifier
|
||||
if !(item is RSOPMLDocument) {
|
||||
XCTAssertNotNil((item.attributes! as NSDictionary).opml_text)
|
||||
}
|
||||
|
||||
// If it has no children, it should have a feed specifier. The converse is also true.
|
||||
var isFolder = item.children != nil && item.children!.count > 0
|
||||
if !isFolder && (item.attributes! as NSDictionary).opml_title == "Skip" {
|
||||
isFolder = true
|
||||
}
|
||||
|
||||
if !isFolder {
|
||||
XCTAssertNotNil(feedSpecifier!.title)
|
||||
XCTAssertNotNil(feedSpecifier!.feedURL)
|
||||
}
|
||||
else {
|
||||
XCTAssertNil(feedSpecifier)
|
||||
}
|
||||
|
||||
if item.children != nil && item.children!.count > 0 {
|
||||
for oneItem in item.children! {
|
||||
recursivelyCheckOPMLStructure(oneItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
//
|
||||
// RSDateParserTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Maurice Parker on 4/1/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import RSParser
|
||||
|
||||
class RSDateParserTests: XCTestCase {
|
||||
|
||||
static func dateWithValues(_ year: Int, _ month: Int, _ day: Int, _ hour: Int, _ minute: Int, _ second: Int) -> Date {
|
||||
var dateComponents = DateComponents()
|
||||
dateComponents.calendar = Calendar.current
|
||||
dateComponents.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
|
||||
dateComponents.year = year
|
||||
dateComponents.month = month
|
||||
dateComponents.day = day
|
||||
dateComponents.hour = hour
|
||||
dateComponents.minute = minute
|
||||
dateComponents.second = second
|
||||
|
||||
return dateComponents.date!
|
||||
}
|
||||
|
||||
func testDateWithString() {
|
||||
var expectedDateResult = Self.dateWithValues(2010, 5, 28, 21, 3, 38)
|
||||
|
||||
var d = RSDateWithString("Fri, 28 May 2010 21:03:38 +0000")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("Fri, 28 May 2010 21:03:38 +00:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("Fri, 28 May 2010 21:03:38 -00:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("Fri, 28 May 2010 21:03:38 -0000")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("Fri, 28 May 2010 21:03:38 GMT")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("2010-05-28T21:03:38+00:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("2010-05-28T21:03:38+0000")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("2010-05-28T21:03:38-0000")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("2010-05-28T21:03:38-00:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
d = RSDateWithString("2010-05-28T21:03:38Z")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
expectedDateResult = Self.dateWithValues(2010, 7, 13, 17, 6, 40)
|
||||
d = RSDateWithString("2010-07-13T17:06:40+00:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
expectedDateResult = Self.dateWithValues(2010, 4, 30, 12, 0, 0)
|
||||
d = RSDateWithString("30 Apr 2010 5:00 PDT")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
expectedDateResult = Self.dateWithValues(2010, 5, 21, 21, 22, 53)
|
||||
d = RSDateWithString("21 May 2010 21:22:53 GMT")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
expectedDateResult = Self.dateWithValues(2010, 6, 9, 5, 0, 0)
|
||||
d = RSDateWithString("Wed, 09 Jun 2010 00:00 EST")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
expectedDateResult = Self.dateWithValues(2010, 6, 23, 3, 43, 50)
|
||||
d = RSDateWithString("Wed, 23 Jun 2010 03:43:50 Z")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
expectedDateResult = Self.dateWithValues(2010, 6, 22, 3, 57, 49)
|
||||
d = RSDateWithString("2010-06-22T03:57:49+00:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
|
||||
expectedDateResult = Self.dateWithValues(2010, 11, 17, 13, 40, 07)
|
||||
d = RSDateWithString("2010-11-17T08:40:07-05:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
}
|
||||
|
||||
func testAtomDateWithMissingTCharacter() {
|
||||
let expectedDateResult = Self.dateWithValues(2010, 11, 17, 13, 40, 07)
|
||||
let d = RSDateWithString("2010-11-17 08:40:07-05:00")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
}
|
||||
|
||||
func testFeedbinDate() {
|
||||
let expectedDateResult = Self.dateWithValues(2019, 9, 27, 21, 01, 48)
|
||||
let d = RSDateWithString("2019-09-27T21:01:48.000000Z")
|
||||
XCTAssertEqual(d, expectedDateResult)
|
||||
}
|
||||
|
||||
// func testHighMillisecondDate() {
|
||||
// let expectedDateResult = Self.dateWithValues(2021, 03, 29, 10, 46, 56)
|
||||
// let d = RSDateWithString("2021-03-29T10:46:56.516941+00:00")
|
||||
// XCTAssertEqual(d, expectedDateResult)
|
||||
// }
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
//
|
||||
// RSSInJSONParserTests.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/26/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
|
||||
class RSSInJSONParserTests: XCTestCase {
|
||||
|
||||
func testScriptingNewsPerformance() {
|
||||
|
||||
// 0.003 sec on my 2012 iMac.
|
||||
let d = parserData("ScriptingNews", "json", "http://scripting.com/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testFeedLanguage() {
|
||||
let d = parserData("ScriptingNews", "json", "http://scripting.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
XCTAssertEqual(parsedFeed.language, "en-us")
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
//
|
||||
// RSSParserTests.swift
|
||||
// RSParser
|
||||
//
|
||||
// Created by Brent Simmons on 6/26/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
|
||||
class RSSParserTests: XCTestCase {
|
||||
|
||||
func testScriptingNewsPerformance() {
|
||||
|
||||
// 0.004 sec on my 2012 iMac.
|
||||
let d = parserData("scriptingNews", "rss", "http://scripting.com/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testKatieFloydPerformance() {
|
||||
|
||||
// 0.004 sec on my 2012 iMac.
|
||||
let d = parserData("KatieFloyd", "rss", "http://katiefloyd.com/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testEMarleyPerformance() {
|
||||
|
||||
// 0.001 sec on my 2012 iMac.
|
||||
let d = parserData("EMarley", "rss", "https://medium.com/@emarley")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testMantonPerformance() {
|
||||
|
||||
// 0.002 sec on my 2012 iMac.
|
||||
let d = parserData("manton", "rss", "http://manton.org/")
|
||||
self.measure {
|
||||
let _ = try! FeedParser.parse(d)
|
||||
}
|
||||
}
|
||||
|
||||
func testNatashaTheRobot() {
|
||||
|
||||
let d = parserData("natasha", "xml", "https://www.natashatherobot.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
XCTAssertEqual(parsedFeed.items.count, 10)
|
||||
}
|
||||
|
||||
func testTheOmniShowAttachments() {
|
||||
|
||||
let d = parserData("theomnishow", "rss", "https://theomnishow.omnigroup.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
XCTAssertNotNil(article.attachments)
|
||||
XCTAssertEqual(article.attachments!.count, 1)
|
||||
let attachment = Array(article.attachments!).first!
|
||||
XCTAssertNotNil(attachment.mimeType)
|
||||
XCTAssertNotNil(attachment.sizeInBytes)
|
||||
XCTAssert(attachment.url.contains("cloudfront"))
|
||||
XCTAssertGreaterThanOrEqual(attachment.sizeInBytes!, 22275279)
|
||||
XCTAssertEqual(attachment.mimeType, "audio/mpeg")
|
||||
}
|
||||
}
|
||||
|
||||
func testTheOmniShowUniqueIDs() {
|
||||
|
||||
let d = parserData("theomnishow", "rss", "https://theomnishow.omnigroup.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
XCTAssertNotNil(article.uniqueID)
|
||||
XCTAssertTrue(article.uniqueID.hasPrefix("https://theomnishow.omnigroup.com/episode/"))
|
||||
}
|
||||
}
|
||||
|
||||
func testMacworldUniqueIDs() {
|
||||
|
||||
// Macworld’s feed doesn’t have guids, so they should be calculated unique IDs.
|
||||
|
||||
let d = parserData("macworld", "rss", "https://www.macworld.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
XCTAssertNotNil(article.uniqueID)
|
||||
XCTAssertEqual(article.uniqueID.count, 32) // calculated unique IDs are MD5 hashes
|
||||
}
|
||||
}
|
||||
|
||||
func testMacworldAuthors() {
|
||||
|
||||
// Macworld uses names instead of email addresses (despite the RSS spec saying they should be email addresses).
|
||||
|
||||
let d = parserData("macworld", "rss", "https://www.macworld.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
|
||||
let author = article.authors!.first!
|
||||
XCTAssertNil(author.emailAddress)
|
||||
XCTAssertNil(author.url)
|
||||
XCTAssertNotNil(author.name)
|
||||
}
|
||||
}
|
||||
|
||||
func testMonkeyDomGuids() {
|
||||
|
||||
// https://coding.monkeydom.de/posts.rss has a bug in the feed (at this writing):
|
||||
// It has guids that are supposed to be permalinks, per the spec —
|
||||
// except that they’re not actually permalinks. The RSS parser should
|
||||
// detect this situation, and every article in the feed should have a permalink.
|
||||
|
||||
let d = parserData("monkeydom", "rss", "https://coding.monkeydom.de/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
XCTAssertNil(article.url)
|
||||
XCTAssertNotNil(article.uniqueID)
|
||||
}
|
||||
}
|
||||
|
||||
func testEmptyContentEncoded() {
|
||||
// The ATP feed (at the time of this writing) has some empty content:encoded elements. The parser should ignore those.
|
||||
// https://github.com/brentsimmons/NetNewsWire/issues/529
|
||||
|
||||
let d = parserData("atp", "rss", "http://atp.fm/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
|
||||
for article in parsedFeed.items {
|
||||
XCTAssertNotNil(article.contentHTML)
|
||||
}
|
||||
}
|
||||
|
||||
func testFeedKnownToHaveGuidsThatArentPermalinks() {
|
||||
let d = parserData("livemint", "xml", "https://www.livemint.com/rss/news")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
for article in parsedFeed.items {
|
||||
XCTAssertNil(article.url)
|
||||
}
|
||||
}
|
||||
|
||||
func testAuthorsWithTitlesInside() {
|
||||
// This feed uses atom authors, and we don’t want author/title to be used as item/title.
|
||||
// https://github.com/brentsimmons/NetNewsWire/issues/943
|
||||
let d = parserData("cloudblog", "rss", "https://cloudblog.withgoogle.com/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
for article in parsedFeed.items {
|
||||
XCTAssertNotEqual(article.title, "Product Manager, Office of the CTO")
|
||||
XCTAssertNotEqual(article.title, "Developer Programs Engineer")
|
||||
XCTAssertNotEqual(article.title, "Product Director")
|
||||
}
|
||||
}
|
||||
|
||||
func testTitlesWithInvalidFeedWithImageStructures() {
|
||||
// This invalid feed has <image> elements inside <item>s.
|
||||
// 17 Jan 2021 bug report — we’re not parsing titles in this feed.
|
||||
let d = parserData("aktuality", "rss", "https://www.aktuality.sk/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
for article in parsedFeed.items {
|
||||
XCTAssertNotNil(article.title)
|
||||
}
|
||||
}
|
||||
|
||||
func testFeedLanguage() {
|
||||
let d = parserData("manton", "rss", "http://manton.org/")
|
||||
let parsedFeed = try! FeedParser.parse(d)!
|
||||
XCTAssertEqual(parsedFeed.language, "en-US")
|
||||
}
|
||||
|
||||
// func testFeedWithGB2312Encoding() {
|
||||
// // This feed has an encoding we don’t run into very often.
|
||||
// // https://github.com/Ranchero-Software/NetNewsWire/issues/1477
|
||||
// let d = parserData("kc0011", "rss", "http://kc0011.net/")
|
||||
// let parsedFeed = try! FeedParser.parse(d)!
|
||||
// XCTAssert(parsedFeed.items.count > 0)
|
||||
// for article in parsedFeed.items {
|
||||
// XCTAssertNotNil(article.contentHTML)
|
||||
// }
|
||||
// }
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -1,149 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
|
||||
<channel>
|
||||
<title><![CDATA[Stories by Hodl Hodl on Medium]]></title>
|
||||
<description><![CDATA[Stories by Hodl Hodl on Medium]]></description>
|
||||
<link>https://medium.com/@hodlhodl?source=rss-b1f3d322dadf------2</link>
|
||||
<image>
|
||||
<url>https://cdn-images-1.medium.com/fit/c/150/150/1*PJ4xeTc0v0DOWgJIb25k8Q.jpeg</url>
|
||||
<title>Stories by Hodl Hodl on Medium</title>
|
||||
<link>https://medium.com/@hodlhodl?source=rss-b1f3d322dadf------2</link>
|
||||
</image>
|
||||
<generator>Medium</generator>
|
||||
<lastBuildDate>Thu, 22 Nov 2018 05:50:04 GMT</lastBuildDate>
|
||||
<atom:link href="https://medium.com/feed/@hodlhodl" rel="self" type="application/rss+xml"/>
|
||||
<webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
|
||||
<atom:link href="http://medium.superfeedr.com" rel="hub"/>
|
||||
<item>
|
||||
<title><![CDATA[Hodl Hodl closes funding round]]></title>
|
||||
<link>https://medium.com/@hodlhodl/hodl-hodl-closes-funding-round-417d97952d42?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/417d97952d42</guid>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<category><![CDATA[funding]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Mon, 19 Nov 2018 08:10:26 GMT</pubDate>
|
||||
<atom:updated>2018-11-19T08:42:59.267Z</atom:updated>
|
||||
<content:encoded><![CDATA[<p>For a long time Hodl Hodl remained self-funded. It was our own money that paid for the development and the team. This year we realized we needed to grow and move forward faster so we decided to start looking for external funding. We initially talked to a number of VCs and after months of negotiations it became obvious this wouldn’t work. Some declined us, some were constantly putting us on hold. We figured it’s just the way it works with VCs (read an awesome article <a href="http://paulgraham.com/fr.html">“How to raise money”</a> by Paul Graham on the subject, very helpful if you’re raising money for the first time).</p><p>In retrospect, we don’t really think VC funding would’ve been the right fit for a Bitcoin-company like ours. We don’t do KYC/AML and we’re not bending and selling our customers (and not because we’re so good and honest, but because that’s part of the value we provide and, thus, part of our business model) — and, if we took that VC money, our feeling was, we’d start bending.</p><p>Instead, we were very lucky to be approached by a number of bitcoiners who attended our “<a href="https://bh2018.hodlhodl.com">Baltic Honeybadger</a>” conference, know us personally and decided that they wanted to invest in Hodl Hodl. And so they did.</p><p>So today, we’re proud to announce that <a href="https://twitter.com/WhalePanda">WhalePanda</a>, <a href="https://twitter.com/anambroid">Ambroid</a>, <a href="https://twitter.com/Marsmensch">Marsmensch</a> and two more undisclosed persons became the first investors of Hodl Hodl.</p><p>In 2019 Hodl Hodl will be:</p><ol><li>Improving the P2P Exchange platform</li><li>Releasing P2P prediction contracts market (aka P2P Bitcoin Futures)</li><li>Releasing a number of other stealth features that will hopefully be incredibly useful to the Bitcoin economy.</li></ol><p><strong>In general, Hodl Hodl will become THE platform for multisig contracts on Bitcoin.</strong></p><p>We would like to thank all our investors for believing in us and we hope to not only deliver what is expected of us, but become good friends with them all over time. They’ve been really helpful and were encouraging us from Hodl Hodl’s launch so we duly appreciate that.</p><p>And finally, some good news for our customers: we’ll lower the exchange commission to 0.4% for December 2018, until the end of the year.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=417d97952d42" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Update on Hodl Hodl: New payment type “Cryptocurrency”]]></title>
|
||||
<link>https://medium.com/@hodlhodl/update-on-hodl-hodl-new-payment-type-cryptocurrency-ca7b7ab94b2f?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/ca7b7ab94b2f</guid>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Fri, 09 Nov 2018 14:40:34 GMT</pubDate>
|
||||
<atom:updated>2018-11-09T14:40:34.747Z</atom:updated>
|
||||
<content:encoded><![CDATA[<p>Dear Hodlers,</p><p>We want introduce to you an update at Hodl Hodl — we have added a new payment type “Cryptocurrency”.</p><p><strong>Quick summary</strong></p><p>From now on, every user can buy and sell Bitcoin and Litecoin, for other cryptocurrencies at Hodl Hodl.</p><p><strong>About</strong></p><p>When creating an offer, every user has to specify the payment method(-s) that he will accept. And every payment method is tied to a specific payment type: for example, payment methods “SWIFT” and “SEPA” are tied to the payment type “Bank wire”.</p><p>With this update, in the list of payment types, you can find a new one: “Cryptocurrency”.</p><p>It is tied to different payment methods such as: “Dash”, “Monero”, “Ethereum”, etc., which can be used to buy and sell your bitcoins and litecoins.</p><p><strong>Further plans</strong></p><p>We also have plans related to cryptocurrencies as payment methods. In the future, our customers will be able to set prices in terms of cryptocurrency, not fiat, as it is currently.</p><p>So you could create a Bitcoin buy or sell offer, for the price of 60 monero’s per 1 Bitcoin.</p><p><strong>Propose a payment method</strong></p><p>In case you don’t see the cryptocurrency you need in the list, you can propose one in the page “Payment methods” (which you can find in the user menu in the right top corner).</p><p>Once administrator approves your request — you will able to create an offer with the newly added cryptocurrency.</p><p><strong>Previously</strong></p><p>We had a payment method “Cryptocurrency” related to the payment type “Online payment system”, and with this update, we have deleted this payment method and disabled all offers using it.</p><p>Therefore, if you were the owner of such an offer, we kindly ask you to update your offer(-s), please set new payment methods using the exact name of the cryptocurrency you want, and don’t forget, you can set multiple payment methods in the same offer.</p><p><strong>Reach us via</strong></p><ul><li>Hodl Hodl exchange: <a href="http://hodlhodl.com/">hodlhodl.com</a></li><li>TESTNET Hodl Hodl exchange: <a href="http://testnet.hodlhodl.com/">testnet.hodlhodl.com</a></li><li>E-mail: support@hodlhodl.com</li><li>Blog: <a href="https://medium.com/@hodlhodl">https://medium.com/@hodlhodl</a></li><li>Twitter: <a href="https://twitter.com/hodlhodl">https://twitter.com/hodlhodl</a></li><li>Telegram: <a href="https://t.me/HodlHodl">https://t.me/HodlHodl</a></li><li>Reddit: <a href="https://www.reddit.com/r/hodlhodl/">https://www.reddit.com/r/hodlhodl</a></li><li>Slack: <a href="https://goo.gl/zaMnCn">https://goo.gl/zaMnCn</a></li><li>Facebook: <a href="https://www.facebook.com/hodlexchange/">https://www.facebook.com/hodlexchange</a></li><li>YouTube: <a href="https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg">https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg</a></li></ul><p><strong>Hodl!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ca7b7ab94b2f" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Introducing a new feature: Lower exchange fees for staying online]]></title>
|
||||
<link>https://medium.com/@hodlhodl/introducing-a-new-feature-lower-exchange-fees-for-staying-online-302906f8b49f?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/302906f8b49f</guid>
|
||||
<category><![CDATA[exchange]]></category>
|
||||
<category><![CDATA[litecoin]]></category>
|
||||
<category><![CDATA[p2p]]></category>
|
||||
<category><![CDATA[cryptocurrency]]></category>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Thu, 08 Nov 2018 14:02:57 GMT</pubDate>
|
||||
<atom:updated>2018-11-08T14:02:57.512Z</atom:updated>
|
||||
<content:encoded><![CDATA[<p>Dear Hodlers,</p><p>Today we introduce a new feature at Hodl Hodl — lower exchange fees for staying online.</p><p><strong>Quick summary</strong></p><p>Based on feedback from our users, we understand that everyone wants to see other users online at Hodl Hodl, otherwise, when starting a trade, nobody knows when it will advance from there.</p><p>So from now on, every user will get an exchange fee discount for staying online.</p><p><strong>Description</strong></p><p>Starting from today, Hodl Hodl now gives a discount of 20% off the exchange fee for being online for at least 8 hours within the past 24 hours.</p><p>We placed a timer in every user’s dashboard, which indicates how much time you need to be online until your discount is applied. When you reach the required time online — we’ll indicate in your dashboard that you are receiving the exchange fee discount.</p><p>Please be advised, the “time online discount” is applied at the moment the trade starts.</p><p><strong>Reach us via</strong></p><ul><li>Hodl Hodl exchange: <a href="http://hodlhodl.com/">hodlhodl.com</a></li><li>TESTNET Hodl Hodl exchange: <a href="http://testnet.hodlhodl.com/">testnet.hodlhodl.com</a></li><li>E-mail: support@hodlhodl.com</li><li>Blog: <a href="https://medium.com/@hodlhodl">https://medium.com/@hodlhodl</a></li><li>Twitter: <a href="https://twitter.com/hodlhodl">https://twitter.com/hodlhodl</a></li><li>Telegram: <a href="https://t.me/HodlHodl">https://t.me/HodlHodl</a></li><li>Reddit: <a href="https://www.reddit.com/r/hodlhodl/">https://www.reddit.com/r/hodlhodl</a></li><li>Slack: <a href="https://goo.gl/zaMnCn">https://goo.gl/zaMnCn</a></li><li>Facebook: <a href="https://www.facebook.com/hodlexchange/">https://www.facebook.com/hodlexchange</a></li><li>YouTube: <a href="https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg">https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg</a></li></ul><p><strong>Hodl!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=302906f8b49f" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Hodl Hodl announces OTC trading desk and brokerage company Tenbagger]]></title>
|
||||
<link>https://medium.com/@hodlhodl/hodl-hodl-announces-otc-trading-desk-and-brokerage-company-tenbagger-fed4be91db6?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/fed4be91db6</guid>
|
||||
<category><![CDATA[exchange]]></category>
|
||||
<category><![CDATA[p2p]]></category>
|
||||
<category><![CDATA[litecoin]]></category>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<category><![CDATA[cryptocurrency]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Fri, 02 Nov 2018 14:41:47 GMT</pubDate>
|
||||
<atom:updated>2018-11-02T14:41:47.679Z</atom:updated>
|
||||
<content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T4YcnmS72gYkOBPYIWv5pg.png" /></figure><p>We are proud to announce our OTC trading desk!</p><p>From now on, Hodl Hodl, in partnership with our EU licensed broker <a href="https://tenbagger.co/">Tenbagger</a>, will match and guide counter-parties, allowing you to buy and sell bitcoins with same day settlement, at the most competitive rates.</p><p>There has been a vibrant OTC Bitcoin trading market operating in parallel to the existing exchanges, but none of them are offering non-custodial escrow services for cryptocurrencies which would eliminate the risk of losing funds.</p><p>Hodl Hodl has developed non-custodial cryptocurrency escrow that leverages the full potential of Bitcoin’s blockchain, and ensures the highest level of security. For each trade, we create a unique multisig escrow account on Bitcoin’s blockchain, ensuring transparency and the highest level of security.</p><p>This also doesn’t affect our current operations. We are still non-KYC/AML non-custodial P2P exchange suitable for any type of trade — small or large. We are sticking with our original plan to help people buy Bitcoin/Litecoin in an easy and secure way.</p><p>Why choose OTC trading with us?</p><p>- Secure transactions</p><p>- Competitive rates</p><p>- Same day settlement</p><p>- Private and personalized trading experience</p><p>- 24/7 personalized support</p><p>To contact us:</p><ul><li>Hodl Hodl exchange: <a href="https://hodlhodl.com/">hodlhodl.com</a></li><li>TESTNET Hodl Hodl exchange: <a href="https://testnet.hodlhodl.com/">testnet.hodlhodl.com</a></li><li>E-mail: otc@hodlhodl.com</li><li>Blog: <a href="https://medium.com/@hodlhodl">https://medium.com/@hodlhodl</a></li><li>Twitter: <a href="https://twitter.com/hodlhodl">https://twitter.com/hodlhodl</a></li><li>Telegram: <a href="https://t.me/HodlHodl">https://t.me/HodlHodl</a></li><li>Reddit: <a href="https://www.reddit.com/r/hodlhodl">https://www.reddit.com/r/hodlhodl</a></li><li>Slack: <a href="https://goo.gl/zaMnCn">https://goo.gl/zaMnCn</a></li><li>Facebook: <a href="https://www.facebook.com/hodlexchange">https://www.facebook.com/hodlexchange</a></li><li>YouTube: <a href="https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg">https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg</a></li></ul><p><strong>Hodl!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fed4be91db6" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Hodl Hodl introduces 2 out of 3 multisig escrow]]></title>
|
||||
<link>https://medium.com/@hodlhodl/hodl-hodl-introduces-2-out-of-3-multisig-escrow-b2110580e036?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/b2110580e036</guid>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<category><![CDATA[cryptocurrency]]></category>
|
||||
<category><![CDATA[p2p]]></category>
|
||||
<category><![CDATA[exchange]]></category>
|
||||
<category><![CDATA[litecoin]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Mon, 22 Oct 2018 14:38:05 GMT</pubDate>
|
||||
<atom:updated>2018-10-22T14:38:05.903Z</atom:updated>
|
||||
<content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fKGPumd7UGnsqnLjV54iZg.png" /></figure><p>Hey Hodlers,</p><p>Today <a href="http://hodlhodl.com/">Hodl Hodl</a>, a P2P cryptocurrency exchange, introduces a new type of multisig escrow account available for contracts at the exchange. From now on, during offer creation, every user will choose what type of contract will be created by the offer: 2-out-of-2 or 2-out-of-3.</p><p><strong>About</strong></p><p>We have recently added a new type of escrow, and it’s already available for trades, it requires 2 out of 3 keys for making a release. This means, that from now on, in 2 out of 3 contracts, buyers have some control over the funds locked in escrow, since one of the keys belongs to himself, and the other two belong to the seller and Hodl Hodl. Also, 2 out of 2 contracts are still available for trading.</p><p><strong>How does it work?</strong></p><p>In every trade, Hodl Hodl exchange generates a unique multisig escrow cryptocurrency address, where the seller locks the funds, and then buyer sends the payment. Only after seller has received the payment, he releases funds from escrow directly to the buyers wallet.</p><p>In a regular 2 out of 3 contract, where everything goes well, buyer’s key is not needed — it only comes into play if the contract was disputed, and Hodl Hodl administrator resolved it in favor of buyer. In this case, buyer is able to sign a release transaction with his key and receive the funds without seller’s participation. This is how the 2 out of 3 contract type works.</p><p><strong>Differences between these types of contracts</strong></p><p>Now, you may have a question: what does the term “contract type” mean? Contract type indicates how many keys are needed to make a release from escrow:</p><ul><li>Contract type “2 out of 2”: this means, that in contracts of this type, 2 keys are needed for a release, and in total there are 2 keys to escrow: one belongs to the seller, and one to Hodl Hodl.</li><li>Contract type “2 out of 3”: this means, that in contracts of this type, 2 keys are needed for a release, and in total there are 3 keys to escrow: one belongs to the seller, one to the buyer, and one to Hodl Hodl.</li></ul><p>During the offer creation process, every user specifies what type of contract will be created with this specific offer. Once the offer is created, the contract type will be shown in the offer list for other users.</p><p>In case you already had offer(-s) before today, these contract types will remain 2 out of 2, so if you want, you can now edit your offer(-s).</p><p><strong>Reach us via</strong></p><p>Hodl Hodl exchange: <a href="https://hodlhodl.com/">hodlhodl.com</a></p><p>TESTNET Hodl Hodl exchange: <a href="https://testnet.hodlhodl.com/">testnet.hodlhodl.com</a></p><p>E-mail: <a href="mailto:support@hodlhodl.com">support@hodlhodl.com</a></p><p>Blog: <a href="https://medium.com/@hodlhodl">https://medium.com/@hodlhodl</a></p><p>Twitter: <a href="https://twitter.com/hodlhodl">https://twitter.com/hodlhodl</a></p><p>Telegram: <a href="https://t.me/HodlHodl">https://t.me/HodlHodl</a></p><p>Reddit: <a href="https://www.reddit.com/r/hodlhodl">https://www.reddit.com/r/hodlhodl</a></p><p>Slack: <a href="https://goo.gl/zaMnCn">https://goo.gl/zaMnCn</a></p><p>Facebook: <a href="https://www.facebook.com/hodlexchange">https://www.facebook.com/hodlexchange</a></p><p>YouTube: <a href="https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg">https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg</a></p><p><strong>Hodl!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b2110580e036" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Introducing a new feature: Chat attachments]]></title>
|
||||
<link>https://medium.com/@hodlhodl/introducing-a-new-feature-chat-attachments-bb0d749b4381?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/bb0d749b4381</guid>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<category><![CDATA[cryptocurrency]]></category>
|
||||
<category><![CDATA[exchange]]></category>
|
||||
<category><![CDATA[litecoin]]></category>
|
||||
<category><![CDATA[p2p]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Fri, 05 Oct 2018 14:53:27 GMT</pubDate>
|
||||
<atom:updated>2018-10-05T14:58:45.675Z</atom:updated>
|
||||
<content:encoded><![CDATA[<p><strong>Introducing a new feature: Chat attachments</strong></p><p>Dear all,</p><p>Today we are introducing a new feature at Hodl Hodl — chat attachments.</p><p><strong>Quick summary</strong></p><p>After receiving requests from our users, we’ve decided to add a new feature. At first glance, it seems to be a minor one, but it is crucial for trading cryptocurrencies via a P2P platform — to allow sending files in the contract’s chat window. Chat attachments would be very beneficial in case of dispute, users will have an opportunity to exchange information quick, which will reduce the time of solving the dispute.</p><p><strong>Description</strong></p><p>Each user now has the “paper clip” icon in the contract chat window, by clicking it, you can now send files to the chat.</p><p>An important feature to consider is that you may send an attachment to the administrator only, meaning your counterparty cannot see it — you may do so by leaving the checkbox “Show attachment to counterparty” blank.</p><p>Conversely, you must check the box to send an attachment to your counterparty.</p><p><strong>Reach</strong> <strong>us</strong> <strong>via</strong></p><p>Hodl Hodl exchange: hodlhodl.com</p><p>TESTNET Hodl Hodl exchange: testnet.hodlhodl.com</p><p>E-mail: support@hodlhodl.com</p><p>Blog: https://medium.com/@hodlhodl</p><p>Twitter: https://twitter.com/hodlhodl</p><p>Telegram: https://t.me/HodlHodl</p><p>Reddit: https://www.reddit.com/r/hodlhodl</p><p>Slack: https://goo.gl/zaMnCn</p><p>Facebook: https://www.facebook.com/hodlexchange</p><p>YouTube: https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg</p><p><strong>Hodl!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bb0d749b4381" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Hodl Hodl’s upcoming features]]></title>
|
||||
<link>https://medium.com/@hodlhodl/hodl-hodls-upcoming-features-854248d843a5?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/854248d843a5</guid>
|
||||
<category><![CDATA[exchange]]></category>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<category><![CDATA[cryptocurrency]]></category>
|
||||
<category><![CDATA[litecoin]]></category>
|
||||
<category><![CDATA[p2p]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Fri, 28 Sep 2018 13:59:06 GMT</pubDate>
|
||||
<atom:updated>2018-09-28T13:59:06.252Z</atom:updated>
|
||||
<content:encoded><![CDATA[<style>body[data-twttr-rendered="true"] {background-color: transparent;}.twitter-tweet {margin: auto !important;}</style><blockquote class="twitter-tweet" data-conversation="none" data-align="center" data-dnt="true"><p>Hodl Hodl's upcoming features</p><p> — <a href="https://twitter.com/hodlhodl/status/1045673927614320640">@hodlhodl</a></p></blockquote><script src="//platform.twitter.com/widgets.js" charset="utf-8"></script><script>function notifyResize(height) {height = height ? height : document.documentElement.offsetHeight; var resized = false; if (window.donkey && donkey.resize) {donkey.resize(height); resized = true;}if (parent && parent._resizeIframe) {var obj = {iframe: window.frameElement, height: height}; parent._resizeIframe(obj); resized = true;}if (window.location && window.location.hash === "#amp=1" && window.parent && window.parent.postMessage) {window.parent.postMessage({sentinel: "amp", type: "embed-size", height: height}, "*");}if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.resize) {window.webkit.messageHandlers.resize.postMessage(height); resized = true;}return resized;}twttr.events.bind('rendered', function (event) {notifyResize();}); twttr.events.bind('resize', function (event) {notifyResize();});</script><script>if (parent && parent._resizeIframe) {var maxWidth = parseInt(window.frameElement.getAttribute("width")); if ( 500 < maxWidth) {window.frameElement.setAttribute("width", "500");}}</script><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=854248d843a5" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Highlights of Baltic Honeybadger 2018]]></title>
|
||||
<link>https://medium.com/@hodlhodl/highlights-of-baltic-honeybadger-2018-d920a791d0ab?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/d920a791d0ab</guid>
|
||||
<category><![CDATA[bh2018]]></category>
|
||||
<category><![CDATA[baltics]]></category>
|
||||
<category><![CDATA[2018]]></category>
|
||||
<category><![CDATA[honey-badger]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Wed, 26 Sep 2018 14:05:32 GMT</pubDate>
|
||||
<atom:updated>2018-09-26T14:05:32.621Z</atom:updated>
|
||||
<content:encoded><![CDATA[<style>body[data-twttr-rendered="true"] {background-color: transparent;}.twitter-tweet {margin: auto !important;}</style><blockquote class="twitter-tweet" data-conversation="none" data-align="center" data-dnt="true"><p>Highlights of Baltic Honeybadger 2018</p><p> — <a href="https://twitter.com/hodlhodl/status/1044949899048091649">@hodlhodl</a></p></blockquote><script src="//platform.twitter.com/widgets.js" charset="utf-8"></script><script>function notifyResize(height) {height = height ? height : document.documentElement.offsetHeight; var resized = false; if (window.donkey && donkey.resize) {donkey.resize(height); resized = true;}if (parent && parent._resizeIframe) {var obj = {iframe: window.frameElement, height: height}; parent._resizeIframe(obj); resized = true;}if (window.location && window.location.hash === "#amp=1" && window.parent && window.parent.postMessage) {window.parent.postMessage({sentinel: "amp", type: "embed-size", height: height}, "*");}if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.resize) {window.webkit.messageHandlers.resize.postMessage(height); resized = true;}return resized;}twttr.events.bind('rendered', function (event) {notifyResize();}); twttr.events.bind('resize', function (event) {notifyResize();});</script><script>if (parent && parent._resizeIframe) {var maxWidth = parseInt(window.frameElement.getAttribute("width")); if ( 500 < maxWidth) {window.frameElement.setAttribute("width", "500");}}</script><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d920a791d0ab" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Baltic Honeybadger 2018]]></title>
|
||||
<link>https://medium.com/@hodlhodl/baltic-honeybadger-2018-1c70eaa53d5c?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/1c70eaa53d5c</guid>
|
||||
<category><![CDATA[cryptocurrency]]></category>
|
||||
<category><![CDATA[litecoin]]></category>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<category><![CDATA[exchange]]></category>
|
||||
<category><![CDATA[p2p]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Fri, 21 Sep 2018 13:34:11 GMT</pubDate>
|
||||
<atom:updated>2018-09-21T13:34:54.429Z</atom:updated>
|
||||
<content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/824/1*VcGfuImO6NhF9sgRIVK8XQ.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1c70eaa53d5c" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Introducing a new feature: offer balance]]></title>
|
||||
<link>https://medium.com/@hodlhodl/introducing-a-new-feature-offer-balance-341c9e3ff0e7?source=rss-b1f3d322dadf------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/341c9e3ff0e7</guid>
|
||||
<category><![CDATA[exchange]]></category>
|
||||
<category><![CDATA[litecoin]]></category>
|
||||
<category><![CDATA[p2p]]></category>
|
||||
<category><![CDATA[cryptocurrency]]></category>
|
||||
<category><![CDATA[bitcoin]]></category>
|
||||
<dc:creator><![CDATA[Hodl Hodl]]></dc:creator>
|
||||
<pubDate>Wed, 19 Sep 2018 13:26:44 GMT</pubDate>
|
||||
<atom:updated>2018-09-19T13:29:51.979Z</atom:updated>
|
||||
<content:encoded><![CDATA[<h3>Introducing a new feature: the offer balance</h3><p>Dear all,</p><p>Today we introduce to you a new feature at Hodl Hodl — the offer balance.</p><p><strong>Quick summary</strong></p><p>Some users may have a certain amount of fiat or cryptocurrency that they’re willing to trade at Hodl Hodl, so we decided to add an additional field to the offer creation form — where our customers can specify the total amount they are willing to trade through an offer.</p><p><strong>Description</strong></p><p>When creating an offer, every user is now able to specify the offer balance — the total amount of fiat that the offer creator is willing to trade. This is the max amount that may be purchased through the offer, in terms of fiat, or if it’s a sell offer, in terms of cryptocurrency (in fiat equivalent).</p><p>The balance is reduced by the amount of each completed contract through the offer, and when the remaining balance has been expended, the offer becomes inactive unless the user updates this field.</p><p><strong>Reach us via</strong></p><ul><li>Hodl Hodl exchange: <a href="http://hodlhodl.com/">hodlhodl.com</a></li><li>TESTNET Hodl Hodl exchange: <a href="http://testnet.hodlhodl.com/">testnet.hodlhodl.com</a></li><li>E-mail: support@hodlhodl.com</li><li>Blog: <a href="https://medium.com/@hodlhodl">https://medium.com/@hodlhodl</a></li><li>Twitter: <a href="https://twitter.com/hodlhodl">https://twitter.com/hodlhodl</a></li><li>Telegram: <a href="https://t.me/HodlHodl">https://t.me/HodlHodl</a></li><li>Reddit: <a href="https://www.reddit.com/r/hodlhodl/">https://www.reddit.com/r/hodlhodl</a></li><li>Slack: <a href="https://goo.gl/zaMnCn">https://goo.gl/zaMnCn</a></li><li>Facebook: <a href="https://www.facebook.com/hodlexchange/">https://www.facebook.com/hodlexchange</a></li><li>YouTube: <a href="https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg">https://www.youtube.com/channel/UCgujEoZqX_FfDTLb3Uuhsdg</a></li></ul><p><strong>Hodl!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=341c9e3ff0e7" width="1" height="1">]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -1,97 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
|
||||
<channel>
|
||||
<title><![CDATA[Stories by Liz Marley on Medium]]></title>
|
||||
<description><![CDATA[Stories by Liz Marley on Medium]]></description>
|
||||
<link>https://medium.com/@emarley?source=rss-b4981c59ffa5------2</link>
|
||||
<image>
|
||||
<url>https://d262ilb51hltx0.cloudfront.net/fit/c/150/150/0*I9s5OlzJw_En0NzC.jpg</url>
|
||||
<title>Stories by Liz Marley on Medium</title>
|
||||
<link>https://medium.com/@emarley?source=rss-b4981c59ffa5------2</link>
|
||||
</image>
|
||||
<generator>Medium</generator>
|
||||
<lastBuildDate>Sun, 28 Aug 2016 17:27:51 GMT</lastBuildDate>
|
||||
<atom:link href="https://medium.com/feed/@emarley" rel="self" type="application/rss+xml"/>
|
||||
<webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
|
||||
<atom:link href="http://medium.superfeedr.com" rel="hub"/>
|
||||
<item>
|
||||
<title><![CDATA[UI Automation & screenshots]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-snippet">Here’s a partial collection of links from my talk today…</p><p class="medium-feed-link"><a href="https://medium.com/@emarley/ui-automation-screenshots-c44a41af38d1?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/ui-automation-screenshots-c44a41af38d1?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/c44a41af38d1</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Sat, 07 May 2016 23:53:30 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[They didn’t.]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-snippet">“The [software developer tool] team clearly doesn’t use [that tool] themselves.”</p><p class="medium-feed-link"><a href="https://medium.com/@emarley/they-didn-t-3a4dab489f45?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/they-didn-t-3a4dab489f45?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/3a4dab489f45</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Sat, 09 Jan 2016 15:29:25 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Side quest: Drawing]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@emarley/side-quest-drawing-b959ded1a1a4?source=rss-b4981c59ffa5------2"><img src="https://d262ilb51hltx0.cloudfront.net/max/700/1*9TewpOfYBlH8kDIbZmlWDA.jpeg" width="700"></a></p><p class="medium-feed-link"><a href="https://medium.com/@emarley/side-quest-drawing-b959ded1a1a4?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/side-quest-drawing-b959ded1a1a4?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/b959ded1a1a4</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Wed, 09 Dec 2015 03:37:35 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[And if I somehow lose the iPad Pro, I can find that with Find My iPhone.]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-link"><a href="https://medium.com/@emarley/and-if-i-somehow-lose-the-ipad-pro-i-can-find-that-with-find-my-iphone-e9aa43486521?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/and-if-i-somehow-lose-the-ipad-pro-i-can-find-that-with-find-my-iphone-e9aa43486521?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/e9aa43486521</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Mon, 23 Nov 2015 19:38:20 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Though not as much more weight as you might expect.]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-link"><a href="https://medium.com/@emarley/though-not-as-much-more-weight-as-you-might-expect-7b33fe989f6e?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/though-not-as-much-more-weight-as-you-might-expect-7b33fe989f6e?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/7b33fe989f6e</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Mon, 23 Nov 2015 19:37:38 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[I avoided art classes in high school and college because I was afraid they would hurt my GPA.]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-link"><a href="https://medium.com/@emarley/i-avoided-art-classes-in-high-school-and-college-because-i-was-afraid-they-would-hurt-my-gpa-ab916601f2ad?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/i-avoided-art-classes-in-high-school-and-college-because-i-was-afraid-they-would-hurt-my-gpa-ab916601f2ad?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/ab916601f2ad</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Mon, 23 Nov 2015 19:37:13 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Finding Value]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-snippet">I lose things a lot. Sometimes they’re just misplaced, sometimes gone forever. I don’t know if I have ever run out of ink in a pen—there’s…</p><p class="medium-feed-link"><a href="https://medium.com/@emarley/finding-value-20a90bf5ebf?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/finding-value-20a90bf5ebf?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/20a90bf5ebf</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Mon, 23 Nov 2015 19:34:18 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Replaying this post in my head last night, I regret this word.]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-snippet">Keyboard shortcuts, and other little details may be programmatically simple to set up, but they are still an important part of an app’s…</p><p class="medium-feed-link"><a href="https://medium.com/@emarley/replaying-this-post-in-my-head-last-night-i-regret-this-word-d8ed0b43f0f9?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/replaying-this-post-in-my-head-last-night-i-regret-this-word-d8ed0b43f0f9?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/d8ed0b43f0f9</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Tue, 10 Nov 2015 18:08:19 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Betterment]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-snippet">I moved from Senior Test Pilot to Software Engineer last month.</p><p class="medium-feed-link"><a href="https://medium.com/@emarley/betterment-e0ef45fcd284?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/betterment-e0ef45fcd284?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/e0ef45fcd284</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Tue, 10 Nov 2015 02:17:46 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[This is a test.]]></title>
|
||||
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@emarley/this-is-a-test-6ab141a1c5b5?source=rss-b4981c59ffa5------2"><img src="https://d262ilb51hltx0.cloudfront.net/max/2000/1*aTkSAe_3uz1xVtnszIL-Og.jpeg" width="6528"></a></p><p class="medium-feed-snippet">This is only a test.</p><p class="medium-feed-link"><a href="https://medium.com/@emarley/this-is-a-test-6ab141a1c5b5?source=rss-b4981c59ffa5------2">Continue reading on »</a></p></div>]]></description>
|
||||
<link>https://medium.com/@emarley/this-is-a-test-6ab141a1c5b5?source=rss-b4981c59ffa5------2</link>
|
||||
<guid isPermaLink="false">https://medium.com/p/6ab141a1c5b5</guid>
|
||||
<dc:creator><![CDATA[Liz Marley]]></dc:creator>
|
||||
<pubDate>Sun, 20 Sep 2015 07:00:44 GMT</pubDate>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
File diff suppressed because one or more lines are too long
|
@ -1,673 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/css" href="http://onefoottsunami.com/wordpress/wp-content/themes/OFTtheme/style-rss.css"?><feed
|
||||
xmlns="http://www.w3.org/2005/Atom"
|
||||
xmlns:thr="http://purl.org/syndication/thread/1.0"
|
||||
xml:lang=""
|
||||
xml:base="http://onefoottsunami.com/wp-atom.php"
|
||||
>
|
||||
<title type="text">One Foot Tsunami</title>
|
||||
<subtitle type="text">Slightly less disappointing than it sounds</subtitle>
|
||||
|
||||
<updated>2015-09-08T14:21:41Z</updated>
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://onefoottsunami.com" />
|
||||
<id>http://onefoottsunami.com/feed/atom/</id>
|
||||
<link rel="self" type="application/atom+xml" href="http://onefoottsunami.com/feed/atom/" />
|
||||
|
||||
<generator uri="http://wordpress.org/" version="4.3">WordPress</generator>
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Pillow Fight Leaves 24 Concussed]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.nytimes.com/2015/09/05/us/at-west-point-annual-pillow-fight-becomes-weaponized.html?mwrsm=Email&_r=1&pagewanted=all" />
|
||||
<id>http://onefoottsunami.com/?p=14863</id>
|
||||
<updated>2015-09-07T18:14:11Z</updated>
|
||||
<published>2015-09-08T14:21:41Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/09/08/pillow-fight-leaves-24-concussed/"><![CDATA[<p>At West Point, freshman cadets have long had an annual massive nighttime pillow fight to build esprit de corps. This year, it <a href="http://www.nytimes.com/2015/09/05/us/at-west-point-annual-pillow-fight-becomes-weaponized.html?mwrsm=Email&_r=1&pagewanted=all">turned violent</a>.</p><br><a href="http://onefoottsunami.com/2015/09/08/pillow-fight-leaves-24-concussed/" title="Permanent Link to 'Pillow Fight Leaves 24 Concussed' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html"><![CDATA[Perverse Incentives]]></title>
|
||||
<link rel="alternate" type="text/html" href="http://onefoottsunami.com/2015/09/07/perverse-incentives-2/" />
|
||||
<id>http://onefoottsunami.com/?p=14861</id>
|
||||
<updated>2015-09-07T12:21:22Z</updated>
|
||||
<published>2015-09-07T12:21:01Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/09/07/perverse-incentives-2/"><![CDATA[<p>In China and Taiwan, drivers who’ve hit someone with their car may attempt to <em>kill</em> the person. Why?</p>
|
||||
|
||||
<blockquote><p>[I]f you cripple a man, you pay for the injured person’s care for a lifetime. But if you kill the person, you “only have to pay once, like a burial fee.”</p></blockquote>
|
||||
|
||||
<p>Because the legal system has often failed to prosecute these murders, <a href="http://www.slate.com/articles/news_and_politics/foreigners/2015/09/why_drivers_in_china_intentionally_kill_the_pedestrians_they_hit_china_s.html">a perverse incentive has been created</a>. Once a driver hits an individual, the financially prudent move is for him to kill the injured party, rather than allowing them to live with a severe injury.</p>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Space Jam Forever]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.rollingstone.com/sports/features/space-jam-forever-the-website-that-wouldnt-die-20150819?print=true" />
|
||||
<id>http://onefoottsunami.com/?p=14858</id>
|
||||
<updated>2015-09-04T14:31:25Z</updated>
|
||||
<published>2015-09-04T14:31:31Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/09/04/space-jam-forever/"><![CDATA[<p>In 2010, a user on Reddit <a href="https://www.reddit.com/r/todayilearned/comments/esxwd/til_that_the_space_jam_website_is_still_up_and/">discovered</a> that the website for the 1996 movie “Space Jam” was inexplicably still online. Almost 5 years later, and nearly 20 years after the film was released, that’s still true. It’s <a href="http://www.warnerbros.com/archive/spacejam/movie/jam.htm">a wonder to behold</a>. Now, Rolling Stone has done a wonderful <a href="http://www.rollingstone.com/sports/features/space-jam-forever-the-website-that-wouldnt-die-20150819?print=true">archaeological dig</a> on a piece of the ancient Internet, well preserved.</p><br><a href="http://onefoottsunami.com/2015/09/04/space-jam-forever/" title="Permanent Link to 'Space Jam Forever' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html"><![CDATA[Head of the Charles]]></title>
|
||||
<link rel="alternate" type="text/html" href="http://onefoottsunami.com/2015/09/03/head-of-the-charles/" />
|
||||
<id>http://onefoottsunami.com/?p=14854</id>
|
||||
<updated>2015-09-03T03:34:24Z</updated>
|
||||
<published>2015-09-03T14:32:12Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/09/03/head-of-the-charles/"><![CDATA[<p>As America grew in the 1800 and 1900s, many of our waterways became incredibly polluted. Cleveland’s Cuyahoga River actually managed to <a href="http://clevelandhistorical.org/items/show/63">catch fire</a> on multiple occasions. Likewise, Boston’s Charles River was famously polluted by both sewage runoff and industrial wastewater. That impression of a foul waterway is now deeply engrained in the minds of locals, reinforced constantly by the oft-heard classic Standells’ song “Dirty Water”.</p>
|
||||
|
||||
<p>However, Herculean efforts made since 1995 have improved the river’s quality. Recently folks have even begun <a href="http://www.thecharles.org/projects-and-programs/swimmable-charles/">swimming in the Charles again</a>, at least when water quality permits it. The EPA has tracked the steady improvement, now rating the Charles as one of cleanest urban rivers in America.</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150903charlesriver/swamthecharles.jpg" style="width: 500px; height: 500px;" alt="'I Swam The Charles' Bumper Sticker" />
|
||||
<br />[<span style="font-size: smaller;">Photo courtesy of <a href="https://instagram.com/p/ww6aqwtjeZ/">P. Kafasis</a></span>]</p>
|
||||
|
||||
<p>Still, does <em>anyone</em> actually want to drink water from the Charles River? Boston-based beermaker Harpoon aims to find out. Their new “Charles River Pale Ale” contains a not-so-secret ingredient: <a href="http://www.bostonglobe.com/metro/2015/09/01/the-secret-ingredient-harpoon-brewery-new-beer-charles-river-water/qsGtGU3IMTwI3yXSUo9xkM/story.html">300 gallons of Charles River water</a>. While locals are likely gagging and reflexively spitting at the very thought, Harpoon is assuring the public the suds will be both safe and delicious.<p>
|
||||
|
||||
|
||||
<p>In fact, the water was treated by Desalitech, a local water desalination company, purified to make it ideal for drinking. So taking a little taste of the Charles, at least in this form, probably won’t kill you. And hey, if/when you survive the experience, you can wear a shirt in this vein:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150903charlesriver/atetheworm.jpg" style="width: 600px; height: 450px;" alt="Milton from Office Space in a shirt reading 'I Ate The Worm!!'" />
|
||||
<br />I Drank the River!!</p>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[No More Sense Than an Amish Bus Driver]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.freep.com/story/opinion/contributors/2015/09/01/s-time-remove-kentucky-clerk-kim-davis/71505026/" />
|
||||
<id>http://onefoottsunami.com/?p=14852</id>
|
||||
<updated>2015-09-02T17:32:10Z</updated>
|
||||
<published>2015-09-02T17:31:10Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/09/02/no-more-sense-than-an-amish-bus-driver/"><![CDATA[<p>Back in June, supporters of marriage equality <a href="http://onefoottsunami.com/2015/06/29/one-and-the-same/">declared victory in America</a> with the Supreme Court’s ruling on <em>Obergefell v. Hodges</em>. Of late, Kentucky county clerk Kim Davis has been making news by defying the authority of the highest courts in the land, refusing to issue any marriage licenses whatsoever. Surprise surprise, Ms. Davis is a tremendous hypocrite, as <a href="http://www.usnews.com/news/articles/2015/09/01/kentucky-clerk-fighting-gay-marriage-has-wed-four-times">US News reports</a>:</p>
|
||||
|
||||
<blockquote><p>The marriages are documented in court records obtained by U.S. News, which show that Rowan County Clerk Kim Davis divorced three times, first in 1994, then 2006 and again in 2008.</p>
|
||||
|
||||
<p>She gave birth to twins five months after divorcing her first husband. They were fathered by her third husband but adopted by her second. Davis worked at the clerk’s office at the time of each divorce and has since remarried.</p></blockquote>
|
||||
|
||||
<p>If Ms. Davis doesn’t wish to do her job, she should resign or expect to be removed, as John Corvino <a href="http://www.freep.com/story/opinion/contributors/2015/09/01/s-time-remove-kentucky-clerk-kim-davis/71505026/">ably discusses</a>:</p>
|
||||
|
||||
<blockquote><p>If [Davis’s] conscience renders her unable to issue marriage licenses to those legally qualified, then the right thing for her to do is resign. After all, issuing marriage licenses is not a peripheral, non-essential part of being county clerk — it’s a central job function. Her current stance makes no more sense than that of an Amish person who expects to retain a job as a bus driver.</p></blockquote>
|
||||
|
||||
<p>Here’s hoping this odious woman is found in contempt of court tomorrow, and quickly removed from her job, or at least from the spotlight.</p><br><a href="http://onefoottsunami.com/2015/09/02/no-more-sense-than-an-amish-bus-driver/" title="Permanent Link to 'No More Sense Than an Amish Bus Driver' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Self-Driving Cars vs. Fixed-Gear Bikes]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.washingtonpost.com/news/innovations/wp/2015/08/26/how-fixed-gear-bikes-can-confuse-googles-self-driving-cars/?postshare=921440689194781" />
|
||||
<id>http://onefoottsunami.com/?p=14850</id>
|
||||
<updated>2015-09-01T16:22:03Z</updated>
|
||||
<published>2015-09-01T16:22:00Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/09/01/self-driving-cars-vs-fixed-gear-bikes/"><![CDATA[<p>When a rider on a fixed-gear bicycle arrived at an intersection with a Google self-driving car, the car <a href="http://www.washingtonpost.com/news/innovations/wp/2015/08/26/how-fixed-gear-bikes-can-confuse-googles-self-driving-cars/?postshare=921440689194781">didn’t quite know what to do</a>.</p><br><a href="http://onefoottsunami.com/2015/09/01/self-driving-cars-vs-fixed-gear-bikes/" title="Permanent Link to 'Self-Driving Cars vs. Fixed-Gear Bikes' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[That’s Not What Dolphins Do]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="" />
|
||||
<id>http://onefoottsunami.com/?p=14848</id>
|
||||
<updated>2015-08-31T15:38:01Z</updated>
|
||||
<published>2015-08-31T15:37:47Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/31/thats-not-what-dolphins-do/"><![CDATA[<p>Surfer Elinor Dempsey didn’t catch any waves when she hit the ocean on Saturday, but she did get <a href="http://www.sanluisobispo.com/2015/08/29/3783230_surfer-unhurt-after-shark-attack.html?rh=1">a pretty good story</a>. As she waited in the water, something approached her from underneath.</p>
|
||||
|
||||
<blockquote><p>“First I thought it was a dolphin and I thought, ‘What the hell is he doing?’ ” she said. “And he kind of landed on my board. Then I realized he had taken a chunk. And I was, like, that’s not what dolphins do.”</p></blockquote>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150831bittenboard.jpg" style="width: 512px; height: 341px;" alt="Shark Bite photo" />
|
||||
<br />Better the board than her hand</p>
|
||||
|
||||
<p><em>That</em> is indeed not what dolphins do.</p><br><a href="http://onefoottsunami.com/2015/08/31/thats-not-what-dolphins-do/" title="Permanent Link to 'That’s Not What Dolphins Do' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[It’s Also a Very High-End Hunting Load for Ducks, Geese, or Turkeys]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="" />
|
||||
<id>http://onefoottsunami.com/?p=14846</id>
|
||||
<updated>2015-08-28T00:41:21Z</updated>
|
||||
<published>2015-08-28T14:41:06Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/28/its-also-a-very-high-end-hunting-load-for-ducks-geese-or-turkeys/"><![CDATA[<p>Well of course company is <a href="http://www.fieldandstream.com/blogs/field-notes/idaho-company-selling-drone-specific-ammunition?LMeW6Yx7lVML6w2j.01">selling ammunition specifically marketed for shooting down drones</a>. Of course they are.</p><br><a href="http://onefoottsunami.com/2015/08/28/its-also-a-very-high-end-hunting-load-for-ducks-geese-or-turkeys/" title="Permanent Link to 'It’s Also a Very High-End Hunting Load for Ducks, Geese, or Turkeys' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Waste Not, Want Not]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="https://thenanfang.com/chinese-female-passenger-drinks-entire-bottle-cognac-airport-security-rather-throw/" />
|
||||
<id>http://onefoottsunami.com/?p=14839</id>
|
||||
<updated>2015-08-27T17:16:50Z</updated>
|
||||
<published>2015-08-27T17:15:34Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/27/waste-not-want-not/"><![CDATA[<p>What do you do when you buy an expensive bottle of cognac but you’re not permitted to take it on your flight? Maybe you throw it out. You might gift it to a stranger. Perhaps you could throw an impromptu party in the airport. You’d be sure to make some new friends. Whatever you do, avoid the path taken by one Ms. Zhao:</p>
|
||||
|
||||
<blockquote><p>[S]he sat down in a corner and drank the entire bottle of cognac herself.</p></blockquote>
|
||||
|
||||
<p>As you’d probably imagine, <a href="https://thenanfang.com/chinese-female-passenger-drinks-entire-bottle-cognac-airport-security-rather-throw/">this did not turn out well</a>.</p><br><a href="http://onefoottsunami.com/2015/08/27/waste-not-want-not/" title="Permanent Link to 'Waste Not, Want Not' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html"><![CDATA[At Least He Can Still Go to the Same Church]]></title>
|
||||
<link rel="alternate" type="text/html" href="http://onefoottsunami.com/2015/08/26/at-least-he-can-still-go-to-the-same-church/" />
|
||||
<id>http://onefoottsunami.com/?p=14836</id>
|
||||
<updated>2015-08-26T15:43:55Z</updated>
|
||||
<published>2015-08-26T14:43:17Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/26/at-least-he-can-still-go-to-the-same-church/"><![CDATA[<p>We open with a montage of newspaper articles, quickly showing JIMMY McGINTY’s criminal arc.
|
||||
|
||||
<p align="center">JIMMY is caught.
|
||||
<br />(“NOTORIOUS MOB KILLER JIMMY McGINTY ARRESTED!”)</p>
|
||||
|
||||
<p align="center">He turns state’s evidence and aids the prosecution.
|
||||
<br />(“McGINTY TESTIFIES AGAINST LEFTY HANNIGAN”)</p>
|
||||
|
||||
<p align="center">The trial brings down the last vestiges of the Boston Mafia.
|
||||
<br />(“LEFTY HANNIGAN SENTENCED TO LIFE”)</p>
|
||||
|
||||
<p align="center">Finally, he disappears.
|
||||
<br />(“WHERE IS JIMMY McGINTY NOW?”)</p>
|
||||
|
||||
<p>CUT TO:</p>
|
||||
|
||||
<p><strong>INT. STEREOTYPICAL ITALIAN RESTAURANT – NIGHT</strong></p>
|
||||
|
||||
<p>Open on the CHEF, a older man with pale, freckled skin and bright red hair noticeably peeking out from under his chef’s hat. He is wearing an apron and a bushy mustache that is quite clearly fake. </p>
|
||||
|
||||
<p>The CHEF approaches—</p>
|
||||
|
||||
<p>A TABLE dressed in a red checkered tablecloth, with a candle lit atop it.</p>
|
||||
|
||||
<p>—Where a lone female CUSTOMER, sits wearing a simple gray dress. She is persuing the menu.</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<p style="width: 375px;"><strong>CHEF</strong>
|
||||
<br />(in a ridiculously over-the-top Italian-American accent — think “It’sa me, Mario!”)
|
||||
<br />Buonasera, bella! What-a you like-a to have tonight?</p>
|
||||
|
||||
<p style="width: 375px;"><strong>CUSTOMER</strong>
|
||||
<br />(hesitant)
|
||||
<br />Well, I’m not sure. I thought this was an Italian restaurant…</p>
|
||||
|
||||
<p style="width: 375px;"><strong>CHEF</strong>
|
||||
<br />(with delight)
|
||||
<br />Oh, sì, sì! It is, it is!</p>
|
||||
|
||||
<p style="width: 375px;"><strong>CUSTOMER</strong>
|
||||
<br />(still hesitant)
|
||||
<br />But I don’t recognize any of these dishes. “Black pudding”? “Limerick Ham”? “Corned Beef and Cabbage”, now that’s an <em>Irish</em> dish!</p>
|
||||
|
||||
<p style="width: 375px;"><strong>CHEF</strong>
|
||||
<br />(shaken, slips into a very real Irish-American accent)
|
||||
<br />Ah, no, no, cailín
|
||||
<br />(Quickly recovering his over-the-top Italian-American accent)
|
||||
<br />Err, we Italians have-a that as well! But I-a tell you what. I’m-a gonna make you the specialty of the house! You-a trust me, no? After all, it’sa me, Mario! I own-a this place!</p>
|
||||
|
||||
<p style="width: 375px;"><strong>CUSTOMER</strong>
|
||||
<br />(relieved)
|
||||
<br />Well, alright. That sounds lovely. Thank you, Mario!</p>
|
||||
</div>
|
||||
|
||||
<p>MARIO walks quickly to the back, through the inward swinging right kitchen door, then immediately back out the outward swinging left kitchen door with a tray he carries with two hands.</p>
|
||||
|
||||
<p>On the tray is a plate which appears to contain a large tortilla covered in chunky tomato soup, with grated orange American cheddar cheese cooked on top. It is a comically poor imitation of Italy’s most famous dish.</p>
|
||||
|
||||
<p>MARIO places the tray on the CUSTOMER’S table with a flourish.</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<p style="width: 375px;"><strong>CHEF</strong>
|
||||
<br />Buon appetito!</p>
|
||||
|
||||
<p style="width: 375px;"><strong>CUSTOMER</strong>
|
||||
<br />(Staring at the plate, extremely hesitant)
|
||||
<br />Uh…
|
||||
<br />(Now staring intently at “MARIO”, noticing his red hair and fake mustache)
|
||||
<br />What do you call this dish, “Mario”?</p>
|
||||
|
||||
<p style="width: 375px;"><strong>MARIO</strong>
|
||||
<br />(a ridiculously over-the-top Italian-American accent)
|
||||
<br />Ah, you are not-a the first person to ask! In fact, a-so many a-people ask, I name-a the restaurant after a-my reply!</p>
|
||||
|
||||
<p><strong>PULL BACK TO RESTAURANT EXTERIOR, REVEALING THIS SIGN</strong></p>
|
||||
</div>
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150826thatsapizza@2x.jpg" srcset="http://onefoottsunami.com/wordpress/wp-content/uploads/20150826thatsapizza.jpg 1x, http://onefoottsunami.com/wordpress/wp-content/uploads/20150826thatsapizza@2x.jpg 2x" style="width: 560px; height: 375px" alt="That'sa Pizza!"></p>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Tomato Seasoning]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://time.com/4006235/israel-heinz-ketchup/" />
|
||||
<id>http://onefoottsunami.com/?p=14832</id>
|
||||
<updated>2015-08-25T04:38:31Z</updated>
|
||||
<published>2015-08-25T13:38:17Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/25/tomato-seasoning/"><![CDATA[<p>Over in Israel, Heinz is <a href="http://www.haaretz.com/news/israel/1.671800">no longer allowed to sell its most popular condiment as “ketchup”</a>.</p>
|
||||
|
||||
<blockquote><p>Heinz will no longer be allowed to label its red sauce as “ketchup” in Hebrew in Israel after local food manufacturer Osem successfully argued that its competitor’s product doesn’t meet the definition of Israel’s standards institute, Israeli news site Ynet reported.</p></blockquote>
|
||||
|
||||
<p>Instead, Heinz must use the ridiculous euphemism “tomato seasoning” to label their product. Then again, given that “tomato seasoning” sounds like the classy way a fancy restaurant would refer to the side they bring with their “pomme frites”, I’m not sure how much of a punishment this really is.</p><br><a href="http://onefoottsunami.com/2015/08/25/tomato-seasoning/" title="Permanent Link to 'Tomato Seasoning' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[“I Love the Thing That I Most Wish Had Not Happened”]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.gq.com/story/stephen-colbert-gq-cover-story?currentPage=all" />
|
||||
<id>http://onefoottsunami.com/?p=14829</id>
|
||||
<updated>2015-08-24T07:23:02Z</updated>
|
||||
<published>2015-08-24T13:40:35Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/24/i-love-the-thing-that-i-most-wish-had-not-happened/"><![CDATA[<p>GQ has <a href="http://www.gq.com/story/stephen-colbert-gq-cover-story">a rather wonderful piece</a> on the once and future Stephen Colbert, discussing the future of late night, a past full of loss, and being present in the present.</p><br><a href="http://onefoottsunami.com/2015/08/24/i-love-the-thing-that-i-most-wish-had-not-happened/" title="Permanent Link to '“I Love the Thing That I Most Wish Had Not Happened”' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[The UK’s Most Disappointing New Visitor Attraction]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.thisiscolossal.com/2015/08/dismaland/" />
|
||||
<id>http://onefoottsunami.com/?p=14825</id>
|
||||
<updated>2015-08-21T14:26:07Z</updated>
|
||||
<published>2015-08-21T13:51:54Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/21/the-uks-most-disappointing-new-visitor-attraction/"><![CDATA[<p>I’ve always found guerrilla artist Banksy interesting enough, particularly when <a href="http://onefoottsunami.com/2013/11/01/genuine-fake-banksys/">selling his own pieces as fakes</a>. However, his new creation is really something else. Over in England, he’s created a dystopian theme park named <a href="http://www.dismaland.co.uk/">Dismaland</a>, and it’ll be open to the public for the next month. Christopher Jobson <a href="http://www.thisiscolossal.com/2015/08/dismaland/">reports in detail</a>:</p>
|
||||
|
||||
<blockquote><p>The event has all the hallmark details of a traditional Banksy event from a shroud of ultimate secrecy (the event area was plastered in notices designating it as filming location for a movie titled Gray Fox) to general themes of apocalypse, anti-consumerism, and anti-corporate messages. However there’s one major deviation: the emphasis of Dismalanded is largely on other artists’ work instead of Banksy himself.</p></blockquote>
|
||||
|
||||
<p>This trippy piece alone is enough to make me jealous of those who can go:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150821dismaland.jpg" style="width: 500px; height: 500px;" alt="Ariel, Sort Of" />
|
||||
<br />Do not adjust your eyeballs.</p>
|
||||
|
||||
<p>I have to imagine Disney’s lawyers will be all over this whole thing. Heck, the park even bans them (“The following items are strictly prohibited: knives, spray cans, illegal drugs, and lawyers from the Walt Disney corporation.”). If you have a chance to go, it definitely seems worth it.</p><br><a href="http://onefoottsunami.com/2015/08/21/the-uks-most-disappointing-new-visitor-attraction/" title="Permanent Link to 'The UK’s Most Disappointing New Visitor Attraction' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html"><![CDATA[Pretty Damned Good for Around a Thousand Pixels]]></title>
|
||||
<link rel="alternate" type="text/html" href="http://onefoottsunami.com/2015/08/20/pretty-damned-good-for-around-a-thousand-pixels/" />
|
||||
<id>http://onefoottsunami.com/?p=14814</id>
|
||||
<updated>2015-08-21T03:21:12Z</updated>
|
||||
<published>2015-08-20T13:43:08Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/20/pretty-damned-good-for-around-a-thousand-pixels/"><![CDATA[<p>Back in October, I wrote about <a href="https://cash.me/app/SDMRNHB">Square Cash</a>, my favorite service for both exchanging money with friends as well as being amused by <a href="http://onefoottsunami.com/2014/10/03/square-cash-and-the-worlds-worst-negotiator/">the imagined negotiating process of an complete moron</a>. Last year’s post provided me with a brief trickle of $1 referral bonuses, netting me something like $18, so naturally I’ve been itching for another chance to write about <a href="https://cash.me/app/SDMRNHB">Square Cash</a>. Square has since upped their referral bonus to $5 for both sides, so, ya know: <a href="http://cash.me/app/SDMRNHB">Sign up for Square Cash</a> and get yourself a Lincoln.<sup id="fnr1-20150820tubmancash"><a href="#fn1-20150820tubmancash">1</a></sup></p>
|
||||
|
||||
<p>Anyhow, the <a href="https://cash.me/app/SDMRNHB">Square Cash</a> iPhone app was recently updated to include support for the Apple Watch. Our glorious future truly has arrived, because it’s now possible to send money to your friends right from your wrist. Open the app and tap your desired recipient to see a screen like this:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150820tubmancash/applewatch.png" style="width: 312px; height: 390px;" alt="The Apple Watch Square Cash app" /><br />“Select Amounts” is kind of a weird instruction.</p>
|
||||
|
||||
<p>To send cash, you tap the relevant bills to add up to the desired whole number (no change!) you wish to send, then tap “Pay”. Within seconds, and without any further verification or chance of cancelling, your money will be flying off to someone else’s bank account. As your money wings away, there’s even a ridiculous animated image of dollar bills fluttering down.</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150820tubmancash/justpaid.png" style="width: 312px; height: 390px;" alt="The Apple Watch Square Cash app" /><br />No matter how long you stare at this image, it won’t move, because it’s just a still. Feel free to cash $PBones to see the full animated version though.</p>
|
||||
|
||||
<p>I’ve previously written that <a href="http://onefoottsunami.com/2015/05/21/you-do-not-have-to-make-a-watch-app/">you do not have to make an Apple Watch app</a>. However, good third-party apps for the watch are certainly possible.<sup id="fnr2-20150820tubmancash"><a href="#fn2-20150820tubmancash">2</a></sup> The Square Cash watch app is definitely well made, and it offers functionality I’ll describe as at least potentially useful, which means it’s better than most Apple Watch apps to date. Perhaps the best thing the Square Cash Apple Watch app does, however, is advance the cause of gender equality.</p>
|
||||
|
||||
<p>Allow me to back up slightly. You may have seen a recent push to put Harriet Tubman on America’s $20 bill, fully replacing Andrew Jackson. Hey, according to <a href="http://www.nytimes.com/2015/07/05/opinion/sunday/take-jackson-off-the-20-bill-put-a-woman-in-his-place.html?pagewanted=all">this article</a>, Old Hickory might not have minded the change:</p>
|
||||
|
||||
<blockquote><p>[Jackson] also hated paper currency and vetoed the reauthorization of the Second Bank of the United States, a predecessor of the Federal Reserve.</p></blockquote>
|
||||
|
||||
<p>This proposed change has also led to other women being considered for placement on American currency, and it appears that the next re-design of the $10 bill will at least provide Alexander Hamilton <a href="https://thenew10.treasury.gov">with a female co-star</a>. That’s some progress, at least, but the idea of placing Harriet Tubman on the $20 has also laid bare <a href="http://blacksportsonline.com/home/2015/05/facebook-guy-thinks-harriet-tubman-is-rosa-parks/">some incredible stupidity</a>. Take a deep breath and try to absorb this:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150820tubmancash/jimmy.gif" style="width: 450px; height: 411px; border: 1px solid black;" alt="Jimmy's Stupid Comment" /><br />I think it’s the exclamation point that really gets me.</p>
|
||||
|
||||
<p>Oh jeez. I honestly try to avoid highlighting this sort of depressing idiocy. I figure it’s best to let it die quietly in the dark, rather than than live and spread in the light. Some things are just so feebleminded that they must be skewered, however, and this is one of those things. So, how dumb are you, Jimmy Pecoul? Let me count the ways.</p>
|
||||
|
||||
<h4>A list of the ways in which Jimmy Pecoul has shown off his ignorance, in increasing order of stupidity</h4>
|
||||
|
||||
<ul>
|
||||
<li><p><strong>Problem #1: Thinking that only presidents belong on our banknotes</strong></p>
|
||||
|
||||
<p>While this is wrong, I wouldn’t be surprised if a not-insignificant number of people believe this, and think that both Alexander Hamilton (on the $10 bill) and Benjamin Franklin (on the $100 bill) were presidents.</p></li>
|
||||
|
||||
<li><p><strong>Problem #2: Mistaking Harriet Tubman for Rosa Parks</strong></p>
|
||||
|
||||
<p>These are two entirely different woman, who are famous for their work winning progress in different areas (abolitionism for Harriet Tubman and civil rights activism for Rosa Parks), and who were active nearly a full century apart.</p></li>
|
||||
|
||||
<li><p><strong>Problem #3: Having no understanding of what Rosa Parks did</strong></p>
|
||||
|
||||
<p>Rosa Parks did not “stand up to bullies on a bus”. Rosa Parks defied a despicable law and set off the <a href="https://en.wikipedia.org/wiki/Montgomery_Bus_Boycott">Montgomery bus boycott</a>, which helped bring about the end of segregation in America.</p>
|
||||
</li>
|
||||
|
||||
<li><p><strong>Stupidity #4: Thinking he’ll stop using $20 bills</strong></p>
|
||||
|
||||
<p>I like to imagine how this might go. Jimmy would have to avoid just about every ATM in America, for one thing. The interactions with cashiers, waiters, bartenders and the like ought to be something to see as well. I’d give him a week managing to boycott the bill, and that’s being generous.</p>
|
||||
</li>
|
||||
|
||||
<li><p><strong>Stupidity #5: Believing that “most” people will stop using the $20 bill</strong></p>
|
||||
|
||||
<p>I doubt even <em>one</em> person in the entire country would stop using yuppie singles if the picture on them changed from Jackson to Tubman. Most? <em>MOST?</em> Jesus <a href="http://onefoottsunami.com/2015/02/09/jesus-crist/">Crist</a>.</p>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<p>We’ll just ignore the incredibly foolish acts of posting this publicly to Facebook for the whole world to snigger at, as well as thinking anyone gives a single good goddamn what his “vote” is on this matter, because if we don’t my head might explode. Let’s get back to Harriet Tubman and Square Cash (<a href="https://cash.me/app/SDMRNHB">Square Cash</a>!). Have another look at the buttons for selecting the amount of money you wish to send:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150820tubmancash/buttons.png" style="width: 312px; height: 271px;" alt="The Apple Watch Square Cash app's buttons" /></p>
|
||||
|
||||
<p>Each button features a pretty good portrait of the corresponding man who appears on that denomination’s bill, but Andrew Jackson on the $20 doesn’t look quite right.</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150820tubmancash/tubmanpixels.png" style="width: 34px; height: 43px;" alt="The face on Square Cash's $20 bill" /></p>
|
||||
|
||||
<p>Of course, that’s not Andrew Jackson at all — it’s Harriet Tubman! The image appears to be based on <a href="http://www.nps.gov/media/photo/gallery.htm?id=C8C22F7E-155D-451F-675CD5F8A8A77596">an 1895 portrait of Mrs. Tubman</a> which is part of the collection of America’s National Portrait Gallery. Here’s a side-by-side comparison:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150820tubmancash/sidebyside.jpg" style="width: 64px; height: 35px;" alt="Pixel Tubman and Photo Tubman, side by side" /></p>
|
||||
|
||||
<p>And here it is, blown up:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150820tubmancash/sidebyside-256.jpg" style="width: 256px; height: 140px;" alt="Enlarged Pixel Tubman and Photo Tubman, side by side" />
|
||||
<br />The pixel version has managed to turn that dour frown upside down.</p>
|
||||
|
||||
<p>Not bad! Lest you have any lingering doubt as to the true identity of this image, Square has <a href="http://www.buzzfeed.com/brendanklinkenberg/square-put-harriet-tubmans-picture-on-the-20-bill#.ncwxEzNWBM">confirmed</a> that the image does indeed represent Tubman, with a spokesman stating “We put Harriet Tubman on the $20 bill because she is an American hero”. Well done. It’s a small gesture, but it’s a good one nonetheless.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p><strong>Update (August 20th, 2015):</strong> Square’s creative director Robert Anderson used his own accidental invention (the <a href="http://qz.com/135149/the-first-ever-hashtag-reply-and-retweet-as-twitter-users-invented-them/">@-reply</a>) to link me to a <a href="https://twitter.com/rsa/status/634390182775681026">higher resolution version of the Tubman image</a>. He also <a href="https://twitter.com/rsa/status/634391578770079744">confirmed</a> that the 1895 portrait seen above was indeed the inspiration for the cartoon version. Neat!</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150820tubmancash/higherres.png" style="width: 520px; height: 290px;" alt="A higher-res $20 Tubman" />
|
||||
<br />A higher-res Tubman Twenty</p>
|
||||
|
||||
<hr class="footnote" />
|
||||
|
||||
<p class="footnotesheader">Footnotes:</p>
|
||||
<ol class="footnotes">
|
||||
<li id="fn1-20150820tubmancash"><p>The bill, not the McConaughey-endorsed vehicle.
|
||||
<a href="#fnr1-20150820tubmancash" class="footnoteBackLink" title="Jump back to footnote 1 in the text.">↩︎</a></p></li>
|
||||
<li id="fn2-20150820tubmancash"><p>I should note that the Apple Watch app from USAA has been updated since I mocked it in that post. Now, in addition to showing your account balance (and allowing you to refresh that account balance), the app will show any transactions from the last seven days. That’s actually mildly useful!
|
||||
<a href="#fnr2-20150820tubmancash" class="footnoteBackLink" title="Jump back to footnote 2 in the text.">↩︎</a></p></li>
|
||||
</ol>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Early Notes on the Ashley Madison Hack]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.theawl.com/2015/08/notes-on-the-ashley-madison-hack" />
|
||||
<id>http://onefoottsunami.com/?p=14809</id>
|
||||
<updated>2015-08-19T16:13:25Z</updated>
|
||||
<published>2015-08-19T16:13:24Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/19/early-notes-on-the-ashley-madison-hack/"><![CDATA[<p>Over at The Awl, John Herman <a href="http://www.theawl.com/2015/08/notes-on-the-ashley-madison-hack">writes about</a> the Ashley Madison hack. The data from this hack appears to be close to being easily searchable by the public, and while the consequences of that have been joked about, they deserve closer consideration:</p>
|
||||
|
||||
<blockquote><p>I’m not sure anyone is really reckoning with how big this could be, yet. If the data becomes as public and available as seems likely right now, we’re talking about tens of millions of people who will be publicly confronted with choices they thought they made in private…Here were millions of people expecting the highest level of privacy that the commercial web could offer as they conducted business they likely wanted to keep between two people (even if a great number of the emails are junk, or attached to casual gawkers, the leak claims to contain nine million transaction records). This hack could be ruinous—personally, professionally, financially—for them and their families. </p></blockquote>
|
||||
|
||||
<p>While it would be easy to say that the people who used this site deserve whatever happens, the fallout from this hack will affect far more than just the users and the implications for the future are also well-worth considering.</p><br><a href="http://onefoottsunami.com/2015/08/19/early-notes-on-the-ashley-madison-hack/" title="Permanent Link to 'Early Notes on the Ashley Madison Hack' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Unlikely to Be a Viable Alternative]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.thestar.com/news/crime/2015/08/17/toronto-police-exploring-clown-guns-for-officers.html" />
|
||||
<id>http://onefoottsunami.com/?p=14807</id>
|
||||
<updated>2015-08-18T17:11:58Z</updated>
|
||||
<published>2015-08-18T17:11:55Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/18/unlikely-to-be-a-viable-alternative/"><![CDATA[<p>Police departments in Canada and the US are <a href="http://www.thestar.com/news/crime/2015/08/17/toronto-police-exploring-clown-guns-for-officers.html">experimenting</a> with a new, less-than-lethal use-of-force option. A device called “The Alternative” allows an officer to fire a single shot gun which may take down a suspect, without killing them. If the shot fails to take down the suspect, the officer’s gun returns to its normal, lethal state.</p>
|
||||
|
||||
<p>It’s an interesting enough idea, and the physics of the device itself seem practical. However, it’s difficult to imagine officers attaching the device to their service weapon in the middle of a confrontation. Perhaps even less likely is police departments adopting a device nicknamed the “Clown Gun”.</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150818clowngun.jpg" style="width: 600px; height: 490px;" alt="The Clown Gun Explained" />
|
||||
<br />The silver ball was originally bright orange and resembled a clown’s nose.</p><br><a href="http://onefoottsunami.com/2015/08/18/unlikely-to-be-a-viable-alternative/" title="Permanent Link to 'Unlikely to Be a Viable Alternative' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[How to Win Contests and Influence Mexican Soap Stars]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://qz.com/476914/i-built-a-twitter-bot-that-entered-and-won-1000-online-contests-for-me/" />
|
||||
<id>http://onefoottsunami.com/?p=14805</id>
|
||||
<updated>2015-08-17T17:37:20Z</updated>
|
||||
<published>2015-08-17T17:37:14Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/17/how-to-win-contests-and-influence-mexican-soap-stars/"><![CDATA[<p>Hunter Scott <a href="http://qz.com/476914/i-built-a-twitter-bot-that-entered-and-won-1000-online-contests-for-me/">won a whole lot of contests</a> via Twitter, all thanks to a bot.</p>
|
||||
|
||||
<blockquote><p>My favorite thing that I won was a cowboy hat autographed by the stars of a Mexican soap opera that I had never heard of.</p></blockquote>
|
||||
|
||||
<p>Few of the prizes were valuable, but the whole experiment is amusing, and that’s worth something.</p><br><a href="http://onefoottsunami.com/2015/08/17/how-to-win-contests-and-influence-mexican-soap-stars/" title="Permanent Link to 'How to Win Contests and Influence Mexican Soap Stars' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Chicago Falcons]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="" />
|
||||
<id>http://onefoottsunami.com/?p=14803</id>
|
||||
<updated>2015-08-14T04:59:10Z</updated>
|
||||
<published>2015-08-14T13:59:12Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/14/chicago-falcons/"><![CDATA[<p>If you want to see ridiculous good photos of peregrine falcons living on a balcony in Chicago, <a href="https://www.audubon.org/news/peregrines-and-photographer-bunk-out-chicago-mans-apartment">look no further</a>.</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150814peregrine@2x.jpg" srcset="http://onefoottsunami.com/wordpress/wp-content/uploads/20150814peregrine.jpg 1x, http://onefoottsunami.com/wordpress/wp-content/uploads/20150814peregrine@2x.jpg 2x" style="width: 600px; height: 400px" alt="Peregrine Stalking Image">
|
||||
<br />Peregrine Falcon on Patrol</p><br><a href="http://onefoottsunami.com/2015/08/14/chicago-falcons/" title="Permanent Link to 'Chicago Falcons' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[You Reap What You Sow]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://nymag.com/daily/intelligencer/2015/08/fox-news-picked-trump-over-megyn-kelly.html" />
|
||||
<id>http://onefoottsunami.com/?p=14801</id>
|
||||
<updated>2015-08-13T04:52:36Z</updated>
|
||||
<published>2015-08-13T13:44:30Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/13/you-reap-what-you-sow/"><![CDATA[<p>Megyn Kelly is <a href="http://nymag.com/daily/intelligencer/2015/08/fox-news-picked-trump-over-megyn-kelly.html">getting the short end of the stick</a> from her network as they work to sooth the hurt feelings of one <a href="http://onefoottsunami.com/2015/07/21/you-get-one-donald/">Donald Trump</a>. It’s a deplorable situation all around, but at the same time, what did Kelly expect when she went to work for Fox “News”?</p><br><a href="http://onefoottsunami.com/2015/08/13/you-reap-what-you-sow/" title="Permanent Link to 'You Reap What You Sow' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html"><![CDATA[Please Report to the Principal’s Office]]></title>
|
||||
<link rel="alternate" type="text/html" href="http://onefoottsunami.com/2015/08/12/please-report-to-the-principals-office/" />
|
||||
<id>http://onefoottsunami.com/?p=14784</id>
|
||||
<updated>2015-08-11T18:30:28Z</updated>
|
||||
<published>2015-08-12T14:34:52Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/12/please-report-to-the-principals-office/"><![CDATA[<p>Speaking of <a href="http://onefoottsunami.com/2015/08/07/get-off-my-back/">anxiety-inducing email subject lines</a>, here’s a doozie:</p>
|
||||
|
||||
<ul class="quotes"><li><p>A chat about your bad photos?</p></li></ul>
|
||||
|
||||
<p>When Apple removed the Camera Roll feature of iOS, I briefly used an app called <a href="http://www.myroll.com/myroll">MyRoll</a>. Now, the company is emailing me about a new app called <a href="https://geo.itunes.apple.com/us/app/gallery-doctor/id926864471?mt=8&at=11l5FI">Gallery Doctor</a>, which claims to identify and help you remove bad photos, thereby saving you space on your iPhone. That’s great and all, but how about an uplifting introduction, instead of an email that feels like it’s chastizing me?</p>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[There Is, of Course, No Gun]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.runnersworld.com/runners-stories/for-inmates-the-wall-has-a-totally-different-meaning" />
|
||||
<id>http://onefoottsunami.com/?p=14798</id>
|
||||
<updated>2015-08-11T00:09:01Z</updated>
|
||||
<published>2015-08-11T13:08:56Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/11/there-is-of-course-no-gun/"><![CDATA[<p>Michael Heald has written <a href="http://www.runnersworld.com/runners-stories/for-inmates-the-wall-has-a-totally-different-meaning">an incredible story</a> of running a half-marathon behind the walls of Oregon State Penitentiary, where the phrase “The Wall” takes on a very different meaning.</p><br><a href="http://onefoottsunami.com/2015/08/11/there-is-of-course-no-gun/" title="Permanent Link to 'There Is, of Course, No Gun' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Unwelcome and Superfluous]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.hexjam.com/uk/sex-relationships/i-spent-a-month-replying-to-all-of-my-pr-emails-with-i-love-you" />
|
||||
<id>http://onefoottsunami.com/?p=14796</id>
|
||||
<updated>2015-08-10T15:56:03Z</updated>
|
||||
<published>2015-08-10T15:55:54Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/10/unwelcome-and-superfluous/"><![CDATA[<p>Writer Ralph Jones gets a lot of press releases. Recently, he started <a href="http://www.hexjam.com/uk/sex-relationships/i-spent-a-month-replying-to-all-of-my-pr-emails-with-i-love-you">replying to all of them</a> with the phrase “I love you”.</p><br><a href="http://onefoottsunami.com/2015/08/10/unwelcome-and-superfluous/" title="Permanent Link to 'Unwelcome and Superfluous' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html"><![CDATA[Get Off My Back, CVS]]></title>
|
||||
<link rel="alternate" type="text/html" href="http://onefoottsunami.com/2015/08/07/get-off-my-back/" />
|
||||
<id>http://onefoottsunami.com/?p=14754</id>
|
||||
<updated>2015-08-02T15:49:38Z</updated>
|
||||
<published>2015-08-07T14:16:37Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/07/get-off-my-back/"><![CDATA[<p>Sometimes I receive an email that fills me with existential sorrow. An email with a subject like:</p>
|
||||
|
||||
<ul class="quotes"><li><p>Paul, Are You Making the Most of Our App?</p></li></ul>
|
||||
|
||||
<p>Well god, CVS, I guess I really just don’t know. Am I? The implication is clearly that I’m not making the most of your goddamned app. I don’t need this pressure though, man. Hell, you’re clearly aware that I <em>have</em> the app.</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150807cvs@2x.png" srcset="http://onefoottsunami.com/wordpress/wp-content/uploads/20150807cvs.png 1x, http://onefoottsunami.com/wordpress/wp-content/uploads/20150807cvs@2x.png 2x" style="width: 320px; height: 270px" alt="CVS Image"></p>
|
||||
|
||||
<p>But that’s not enough for you, is it? You’ve gotta tell me about the features of an app I already have installed and insist that I use them. But I’m not in the habit of printing out many photos anymore, and I’m not on any pills. You just let me use my iPhone as I like, and I’ll continue buying the assorted cold medicine, greeting cards, and clandestine bars of candy that I usually do. Deal?</p>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html">Link: <![CDATA[Drones Are Delivering Contraband to Prisons]]></title>
|
||||
|
||||
|
||||
|
||||
<link rel="alternate" type="text/html" href="http://www.fastcompany.com/3049413/fast-feed/drones-are-delivering-contraband-to-prisons" />
|
||||
<id>http://onefoottsunami.com/?p=14789</id>
|
||||
<updated>2015-08-06T14:43:29Z</updated>
|
||||
<published>2015-08-06T14:43:28Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/06/drones-are-delivering-contraband-to-prisons/"><![CDATA[<p>Well of course <a href="http://www.fastcompany.com/3049413/fast-feed/drones-are-delivering-contraband-to-prisons">drones are being used to deliver illicit goods into prisons</a>.</p><br><a href="http://onefoottsunami.com/2015/08/06/drones-are-delivering-contraband-to-prisons/" title="Permanent Link to 'Drones Are Delivering Contraband to Prisons' on One Foot Tsunami">∞ Permalink</a>]]></content>
|
||||
</entry>
|
||||
|
||||
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>Paul Kafasis</name>
|
||||
</author>
|
||||
<title type="html"><![CDATA[Planes, “Planes”, and Automated Fare Pricing]]></title>
|
||||
<link rel="alternate" type="text/html" href="http://onefoottsunami.com/2015/08/05/planes-planes-and-automated-fare-pricing/" />
|
||||
<id>http://onefoottsunami.com/?p=14756</id>
|
||||
<updated>2015-08-05T16:59:42Z</updated>
|
||||
<published>2015-08-05T15:50:23Z</published>
|
||||
<content type="html" xml:base="http://onefoottsunami.com/2015/08/05/planes-planes-and-automated-fare-pricing/"><![CDATA[<p>Let’s say that you wanted to get from Dayton, Ohio to central New Jersey, as friend of the site Chris DiNoia recently wanted to do. On United.com, you might select Newark as your destination, and get a result like this:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/newark@2x.png" srcset="http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/newark.png 1x, http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/newark@2x.png 2x" style="width: 600px; height: 100px; border: 1px solid black;" alt="Flying into Newark"></p>
|
||||
|
||||
<p>$353 is rather pricey for a one-way flight. Let’s check some other options. Philadelphia is about 30 minutes farther than Newark from central Jersey, and presumably you’re <a href="http://onefoottsunami.com/2015/08/03/well-i-guess-its-bust/">not a defenseless robot</a>, so you should be safe there for a few minutes. Set Philadelphia as your destination instead, and hey, why not turn on the “Search Nearby Airports” checkbox?</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/philadelphia@2x.png" srcset="http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/philadelphia.png 1x, http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/philadelphia@2x.png 2x" style="width: 600px; 216px; border: 1px solid black;" alt="Flying into Philadelphia"></p>
|
||||
|
||||
<p>Hey, now there’s a better deal. You can get home for just $149! But hang on a sec. A close look shows that rather than Philadelphia International Airport (PHL), this flight winds up at “ZFV”, which is labeled as a “rail station”. How exactly is a plane going to land there?</p>
|
||||
|
||||
<p>Examine this flight, and you’ll see something bizarre:</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/train@2x.png" srcset="http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/train.png 1x, http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/train@2x.png 2x" style="width: 265px; 96px; border: 1px solid black;" alt="Train Service">
|
||||
<br />“NOTE: This is Train Service” is a truly amazing warning.</p>
|
||||
|
||||
<p>United Airlines is apparently code-sharing with Amtrak’s passenger railroad service to get you to Philadelphia. They’re also referring to Philadelphia’s 30th Street Rail Station (that’s what ZFV stands for) as an airport. Do you think the conductor announces that train as “United flight 3174”? Man, I hope so.</p>
|
||||
|
||||
<p>But the wacky train-instead-of-plane isn’t even the half of it. Take a look at the first half of this itinerary, and compare it to the original search. It’s the exact same flight!</p>
|
||||
|
||||
<p class="centeredimage"><img src="http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/comparison.png" srcset="http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/comparison.png 1x, http://onefoottsunami.com/wordpress/wp-content/uploads/20150805planestrains/comparison.png 2x" style="width: 483px; 220px; border: 1px solid black;" alt="Comparison"></p>
|
||||
|
||||
<p>This type of airline pricing nonsense is not entirely uncommon. Opting for the lower fare and then getting off at Newark even has a name, ”<a href="https://en.wikipedia.org/wiki/Airline_booking_ploys#Hidden_city_ticketing">Hidden city ticketing</a>”. Still, it’s not very often that you can save over $200 just by missing a train.</p>]]></content>
|
||||
</entry>
|
||||
|
||||
</feed>
|
||||
|
||||
|
||||
<!-- Performance optimized by W3 Total Cache. Learn more: http://www.w3-edge.com/wordpress-plugins/
|
||||
|
||||
Minified using disk
|
||||
Page Caching using xcache
|
||||
Database Caching 14/22 queries in 0.009 seconds using disk
|
||||
|
||||
Served from: onefoottsunami.com @ 2015-09-08 23:39:37 by W3 Total Cache -->
|
|
@ -1,945 +0,0 @@
|
|||
{
|
||||
"rss": {
|
||||
"version": "2.0",
|
||||
"xmlns:source": "http://source.scripting.com/",
|
||||
"channel": {
|
||||
"title": "Scripting News",
|
||||
"link": "http://scripting.com/",
|
||||
"description": "Scripting News, the weblog started in 1997 that bootstrapped the blogging revolution.",
|
||||
"pubDate": "Mon, 26 Jun 2017 19:40:58 GMT",
|
||||
"lastBuildDate": "Mon, 26 Jun 2017 19:41:48 GMT",
|
||||
"language": "en-us",
|
||||
"copyright": "© 1994-2017 <a href=\"http://davewiner.com/\">Dave Winer</a>.",
|
||||
"generator": "oldSchool v0.42c",
|
||||
"docs": "https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md",
|
||||
"source:localTime": "Mon, June 26, 2017 3:41 PM EDT",
|
||||
"cloud": {
|
||||
"domain": "rpc.rsscloud.io",
|
||||
"port": 5337,
|
||||
"path": "/pleaseNotify",
|
||||
"registerProcedure": "",
|
||||
"protocol": "http-post"
|
||||
},
|
||||
"source:account": [
|
||||
{
|
||||
"service": "twitter",
|
||||
"#value": "davewiner"
|
||||
},
|
||||
{
|
||||
"service": "facebook",
|
||||
"#value": "dave.winer.12"
|
||||
},
|
||||
{
|
||||
"service": "github",
|
||||
"#value": "scripting"
|
||||
},
|
||||
{
|
||||
"service": "linkedin",
|
||||
"#value": "scripting"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/26.html#a080605",
|
||||
"description": "Good morning students and teachers! 🍏 ",
|
||||
"pubDate": "Mon, 26 Jun 2017 12:20:05 GMT",
|
||||
"guid": "http://scripting.com/2017/06/26.html#a080605",
|
||||
"source:outline": {
|
||||
"text": "Good morning students and teachers! :green_apple: ",
|
||||
"created": "Mon, 26 Jun 2017 12:20:05 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a080605"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/26.html#a030658",
|
||||
"description": "This is the <a href=\"https://twitter.com/kennethn/status/879367091953868801\">human side</a> of <a href=\"http://scripting.com/2017/03/08/theWorldIsSocialistPartIi.html\">Health care is socialist</a>. ",
|
||||
"pubDate": "Mon, 26 Jun 2017 19:40:58 GMT",
|
||||
"guid": "http://scripting.com/2017/06/26.html#a030658",
|
||||
"source:outline": {
|
||||
"text": "This is the <a href=\"https://twitter.com/kennethn/status/879367091953868801\">human side</a> of <a href=\"http://scripting.com/2017/03/08/theWorldIsSocialistPartIi.html\">Health care is socialist</a>. ",
|
||||
"created": "Mon, 26 Jun 2017 19:40:58 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a030658"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/26.html#a020604",
|
||||
"description": "Interesting Politico <a href=\"http://www.politico.com/magazine/story/2017/06/26/trump-president-style-mayor-215294\">piece</a> posits that Trump acts as if he's mayor of the United States. If <a href=\"https://en.wikipedia.org/wiki/Mayor_of_New_York_City\">NYC</a> is his model, that mayor is esp powerless, because the governor of the state also has a lot of power over the city. It's approx 1/2 of the population of the state, and probably much more than 1/2 of the money. For example, the <a href=\"https://en.wikipedia.org/wiki/Metropolitan_Transportation_Authority\">MTA</a>, which runs the buses and subway, is run by the state, not the city.",
|
||||
"pubDate": "Mon, 26 Jun 2017 18:10:04 GMT",
|
||||
"guid": "http://scripting.com/2017/06/26.html#a020604",
|
||||
"source:outline": {
|
||||
"text": "Interesting Politico <a href=\"http://www.politico.com/magazine/story/2017/06/26/trump-president-style-mayor-215294\">piece</a> posits that Trump acts as if he's mayor of the United States. If <a href=\"https://en.wikipedia.org/wiki/Mayor_of_New_York_City\">NYC</a> is his model, that mayor is esp powerless, because the governor of the state also has a lot of power over the city. It's approx 1/2 of the population of the state, and probably much more than 1/2 of the money. For example, the <a href=\"https://en.wikipedia.org/wiki/Metropolitan_Transportation_Authority\">MTA</a>, which runs the buses and subway, is run by the state, not the city.",
|
||||
"created": "Mon, 26 Jun 2017 18:10:04 GMT",
|
||||
"type": "outline",
|
||||
"image": "http://scripting.com/images/2017/06/26/quimby.png",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a020604"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/26.html#a020602",
|
||||
"description": "<a href=\"https://github.com/scripting/Scripting-News/issues/12\">Brent asks</a> if the <i>length</i> in enclosures in <a href=\"https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md\">RSS-in-JSON</a> is a number or string. That's what the test podcast <a href=\"http://scripting.com/2017/06/26.html#a010633\">below</a> is for. ",
|
||||
"pubDate": "Mon, 26 Jun 2017 18:05:02 GMT",
|
||||
"guid": "http://scripting.com/2017/06/26.html#a020602",
|
||||
"source:outline": {
|
||||
"text": "<a href=\"https://github.com/scripting/Scripting-News/issues/12\">Brent asks</a> if the <i>length</i> in enclosures in \"RSS-in-JSON\" is a number or string. That's what the test podcast <a href=\"http://scripting.com/2017/06/26.html#a010633\">below</a> is for. ",
|
||||
"created": "Mon, 26 Jun 2017 18:05:02 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a020602"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/26.html#a010633",
|
||||
"description": "From time to time I have to do a podcast to test things out. This is one of those times. Let's see what happens. ",
|
||||
"pubDate": "Mon, 26 Jun 2017 17:29:33 GMT",
|
||||
"guid": "http://scripting.com/2017/06/26.html#a010633",
|
||||
"enclosure": {
|
||||
"url": "http://scripting.com/2017/06/26/yetAnotherTestPodcast.m4a",
|
||||
"type": "audio/mpeg",
|
||||
"length": 277413
|
||||
},
|
||||
"source:outline": {
|
||||
"text": "From time to time I have to do a podcast to test things out. This is one of those times. Let's see what happens. ",
|
||||
"created": "Mon, 26 Jun 2017 17:29:33 GMT",
|
||||
"type": "outline",
|
||||
"enclosure": "http://scripting.com/2017/06/26/yetAnotherTestPodcast.m4a",
|
||||
"enclosureType": "audio/mpeg",
|
||||
"enclosureLength": "277413",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a010633"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/26.html#a110603",
|
||||
"description": "Body shaming is wrong no matter who you're using as the example. Someone is being hurt by <a href=\"https://twitter.com/xeni/status/879354857374732288\">this</a>. No, I don't care how much you have suffered.",
|
||||
"pubDate": "Mon, 26 Jun 2017 15:26:03 GMT",
|
||||
"guid": "http://scripting.com/2017/06/26.html#a110603",
|
||||
"source:outline": {
|
||||
"text": "Body shaming is wrong no matter who you're using as the example. Someone is being hurt by <a href=\"https://twitter.com/xeni/status/879354857374732288\">this</a>. No, I don't care how much you have suffered.",
|
||||
"created": "Mon, 26 Jun 2017 15:26:03 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a110603"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/26.html#a090616",
|
||||
"description": "I need <a href=\"https://github.com/scripting/xmlViewer/blob/master/xmlviewer.js\">an app</a> to <a href=\"http://xmlviewer.scripting.com/?url=http://scripting.com/rss.xml\">view</a> RSS feeds in the browser because Chrome and Safari refuse to let me do that. I'd love to hear the reason why. ",
|
||||
"pubDate": "Mon, 26 Jun 2017 13:24:16 GMT",
|
||||
"guid": "http://scripting.com/2017/06/26.html#a090616",
|
||||
"source:outline": {
|
||||
"text": "I need <a href=\"https://github.com/scripting/xmlViewer/blob/master/xmlviewer.js\">an app</a> to <a href=\"http://xmlviewer.scripting.com/?url=http://scripting.com/rss.xml\">view</a> RSS feeds in the browser because Chrome and Safari refuse to let me do that. I'd love to hear the reason why. ",
|
||||
"created": "Mon, 26 Jun 2017 13:24:16 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a090616"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Subscribable feed lists give power to users",
|
||||
"link": "http://scripting.com/2017/06/26.html#a080636",
|
||||
"description": "<p>An interesting <a href=\"https://github.com/kylewm/woodwind/issues/7#issuecomment-311000922\">comment</a> from <a href=\"https://github.com/chrisaldrich\">Chris Aldrich</a> about subscribing to lists of feeds in a thread on the <a href=\"https://github.com/kylewm/woodwind\">Woodwind</a> app site on GitHub. </p>\n<p>Here's the basic idea. There's a difference between importing OPML into a reader and subscribing to it. The latter is very powerful, for the user, but a lot of RSS reader devs may not want their users to have that much power. It's not a very hard feature to implement. </p>\n<p>The idea has been <a href=\"https://duckduckgo.com/?q=site%3Ascripting.com+%22reading+list%22&t=hz&ia=web\">much-discussed</a> here. We call them reading lists. Michael Arrington even wrote a <a href=\"https://techcrunch.com/2005/10/16/my-thoughts-on-reading-lists/\">TechCrunch piece</a> about it in 2005. </p>\n<p>Subscribable OPML is something all my readers have been able to do through an OPML feature called <a href=\"http://dev.opml.org/spec2.html#inclusion\">inclusion</a>. I wrote a <a href=\"https://github.com/scripting/river5/blob/master/docs/DROPBOXSUBSCRIPTIONLISTS.md\">howto</a> for a <a href=\"https://github.com/scripting/river5\">River5</a> user re inclusion just last week.</p>\n<p><a href=\"https://techcrunch.com/2006/05/07/share-your-opml/\">Share Your OPML</a> was a service I operated for a while. It made it possible to manage your OPML separate from the reader you used. It was meant to encourage readers to support subscribable OPML. I'm looking for an excuse to bring it back, but first we need a base of shared feed lists. </p>\n<p>A lot of good stuff can be done if feed readers are willing to delegate list management to other services. IMHO the only reason a reader developer <i>wouldn't</i> do it is because they want to lock users in. If I let you edit your feed list elsewhere that means you could give the list to another vendor and have a choice which to use. It's really something users should demand, esp if you're paying for the service. </p>\n",
|
||||
"pubDate": "Mon, 26 Jun 2017 12:24:36 GMT",
|
||||
"guid": "http://scripting.com/2017/06/26.html#a080636",
|
||||
"source:outline": {
|
||||
"text": "Subscribable feed lists give power to users",
|
||||
"created": "Mon, 26 Jun 2017 12:24:36 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "An interesting <a href=\"https://github.com/kylewm/woodwind/issues/7#issuecomment-311000922\">comment</a> from <a href=\"https://github.com/chrisaldrich\">Chris Aldrich</a> about subscribing to lists of feeds in a thread on the <a href=\"https://github.com/kylewm/woodwind\">Woodwind</a> app site on GitHub. ",
|
||||
"created": "Mon, 26 Jun 2017 12:24:49 GMT",
|
||||
"image": "http://static.scripting.com/larryKing/images/2014/05/25/goodHumor.gif",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a080649"
|
||||
},
|
||||
{
|
||||
"text": "Here's the basic idea. There's a difference between importing OPML into a reader and subscribing to it. The latter is very powerful, for the user, but a lot of RSS reader devs may not want their users to have that much power. It's not a very hard feature to implement. ",
|
||||
"created": "Mon, 26 Jun 2017 12:40:32 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a080632"
|
||||
},
|
||||
{
|
||||
"text": "The idea has been <a href=\"https://duckduckgo.com/?q=site%3Ascripting.com+%22reading+list%22&t=hz&ia=web\">much-discussed</a> here. We call them reading lists. Michael Arrington even wrote a <a href=\"https://techcrunch.com/2005/10/16/my-thoughts-on-reading-lists/\">TechCrunch piece</a> about it in 2005. ",
|
||||
"created": "Mon, 26 Jun 2017 12:26:01 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a080601"
|
||||
},
|
||||
{
|
||||
"text": "Subscribable OPML is something all my readers have been able to do through an OPML feature called <a href=\"http://dev.opml.org/spec2.html#inclusion\">inclusion</a>. I wrote a <a href=\"https://github.com/scripting/river5/blob/master/docs/DROPBOXSUBSCRIPTIONLISTS.md\">howto</a> for a \"River5\" user re inclusion just last week.",
|
||||
"created": "Mon, 26 Jun 2017 12:27:15 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a080615"
|
||||
},
|
||||
{
|
||||
"text": "<a href=\"https://techcrunch.com/2006/05/07/share-your-opml/\">Share Your OPML</a> was a service I operated for a while. It made it possible to manage your OPML separate from the reader you used. It was meant to encourage readers to support subscribable OPML. I'm looking for an excuse to bring it back, but first we need a base of shared feed lists. ",
|
||||
"created": "Mon, 26 Jun 2017 12:33:10 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a080610"
|
||||
},
|
||||
{
|
||||
"text": "A lot of good stuff can be done if feed readers are willing to delegate list management to other services. IMHO the only reason a reader developer <i>wouldn't</i> do it is because they want to lock users in. If I let you edit your feed list elsewhere that means you could give the list to another vendor and have a choice which to use. It's really something users should demand, esp if you're paying for the service. ",
|
||||
"created": "Mon, 26 Jun 2017 12:47:13 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a080613"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/26.html#a080636"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/25.html#a080631",
|
||||
"description": "Good morning Internet guzzlers! 🍺",
|
||||
"pubDate": "Sun, 25 Jun 2017 12:27:31 GMT",
|
||||
"guid": "http://scripting.com/2017/06/25.html#a080631",
|
||||
"source:outline": {
|
||||
"text": "Good morning Internet guzzlers! :beer:",
|
||||
"created": "Sun, 25 Jun 2017 12:27:31 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/25.html#a080631"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/25.html#a080606",
|
||||
"description": "So glad I stopped worrying <a href=\"https://duckduckgo.com/?q=site%3Ascripting.com+facebook&t=hz&ia=web\">about</a> Facebook and am now blogging <i>Old School</i> style on <a href=\"http://scripting.com/\">scripting.com</a>. I've found my <a href=\"https://en.wikipedia.org/wiki/Sea_legs\">sea legs</a> once again. ",
|
||||
"pubDate": "Sun, 25 Jun 2017 12:43:06 GMT",
|
||||
"guid": "http://scripting.com/2017/06/25.html#a080606",
|
||||
"source:outline": {
|
||||
"text": "So glad I stopped worrying <a href=\"https://duckduckgo.com/?q=site%3Ascripting.com+facebook&t=hz&ia=web\">about</a> Facebook and am now blogging <i>Old School</i> style on <a href=\"http://scripting.com/\">scripting.com</a>. I've found my <a href=\"https://en.wikipedia.org/wiki/Sea_legs\">sea legs</a> once again. ",
|
||||
"created": "Sun, 25 Jun 2017 12:43:06 GMT",
|
||||
"type": "tweet",
|
||||
"tweetId": "878956761302147072",
|
||||
"tweetUserName": "davewiner",
|
||||
"permalink": "http://scripting.com/2017/06/25.html#a080606"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/25.html#a080623",
|
||||
"description": "Dan Shafer <a href=\"https://www.facebook.com/vicky.elder.77/posts/10211645108716478?pnref=story\">died</a>. I knew Dan from the Mac developer community in the 80s, hired <a href=\"https://en.wikipedia.org/wiki/Dan_Shafer\">him</a> to write the first docs for <a href=\"http://en.wikipedia.org/wiki/UserLand_Software\">Frontier</a>. Bon voyage mi amigo! 💥",
|
||||
"pubDate": "Sun, 25 Jun 2017 12:56:23 GMT",
|
||||
"guid": "http://scripting.com/2017/06/25.html#a080623",
|
||||
"source:outline": {
|
||||
"text": "Dan Shafer <a href=\"https://www.facebook.com/vicky.elder.77/posts/10211645108716478?pnref=story\">died</a>. I knew Dan from the Mac developer community in the 80s, hired <a href=\"https://en.wikipedia.org/wiki/Dan_Shafer\">him</a> to write the first docs for \"Frontier\". Bon voyage mi amigo! :boom:",
|
||||
"created": "Sun, 25 Jun 2017 12:56:23 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/25.html#a080623"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/25.html#a080624",
|
||||
"description": "Money was a big issue yesterday in the nascent tech blogosphere. First, you do this because you love it, not because it pays well. (It doesn't pay at all.) Now I'd like to take you back to a discsussion that was had many years ago that resulted in this conclusion. You don't make money from this work, but it leads to opportunities where you can make money. Ideas and information make their way to you and if you are so-inclined you can make money by investing in those ideas. No sure things, but some bloggers have made billions, and others have made millions. And others have made a decent living. Not <i>from</i> their blog but <i>because</i> they blog. ",
|
||||
"pubDate": "Sun, 25 Jun 2017 12:29:24 GMT",
|
||||
"guid": "http://scripting.com/2017/06/25.html#a080624",
|
||||
"source:outline": {
|
||||
"text": "Money was a big issue yesterday in the nascent tech blogosphere. First, you do this because you love it, not because it pays well. (It doesn't pay at all.) Now I'd like to take you back to a discsussion that was had many years ago that resulted in this conclusion. You don't make money from this work, but it leads to opportunities where you can make money. Ideas and information make their way to you and if you are so-inclined you can make money by investing in those ideas. No sure things, but some bloggers have made billions, and others have made millions. And others have made a decent living. Not <i>from</i> their blog but <i>because</i> they blog. ",
|
||||
"created": "Sun, 25 Jun 2017 12:29:24 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/25.html#a080624"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/25.html#a080631",
|
||||
"description": "In 2015 I <a href=\"http://scripting.com/2015/06/25/dropboxCouldBeKingOfTheOnepageApp.html\">wrote</a> that Dropbox could be the king of the one-page app. Because storage is the thing the web doesn't, on its own, do, and storage is the thing Dropbox does best. And they have an API, and they understood the connection to one-page-apps earlier than anyone. But it didn't happen. I've emailed with people at Dropbox from time to time and the best explanation I can come up with is that they are focused in different areas. It seems to me, from my outside perch, that they are trying to become a competitor to <a href=\"https://www.google.com/docs/about/\">Google's</a> and <a href=\"https://en.wikipedia.org/wiki/Microsoft_Office\">Microsoft's</a> Office products. I was hoping they'd become a platform, focusing on distribution and investment in startups.",
|
||||
"pubDate": "Sun, 25 Jun 2017 12:32:31 GMT",
|
||||
"guid": "http://scripting.com/2017/06/25.html#a080631",
|
||||
"source:outline": {
|
||||
"text": "In 2015 I <a href=\"http://scripting.com/2015/06/25/dropboxCouldBeKingOfTheOnepageApp.html\">wrote</a> that Dropbox could be the king of the one-page app. Because storage is the thing the web doesn't, on its own, do, and storage is the thing Dropbox does best. And they have an API, and they understood the connection to one-page-apps earlier than anyone. But it didn't happen. I've emailed with people at Dropbox from time to time and the best explanation I can come up with is that they are focused in different areas. It seems to me, from my outside perch, that they are trying to become a competitor to <a href=\"https://www.google.com/docs/about/\">Google's</a> and <a href=\"https://en.wikipedia.org/wiki/Microsoft_Office\">Microsoft's</a> Office products. I was hoping they'd become a platform, focusing on distribution and investment in startups.",
|
||||
"created": "Sun, 25 Jun 2017 12:32:31 GMT",
|
||||
"type": "outline",
|
||||
"image": "http://radio3.io/icons/clarus.gif",
|
||||
"permalink": "http://scripting.com/2017/06/25.html#a080631"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Code mode is for real",
|
||||
"link": "http://scripting.com/2017/06/25.html#a110613",
|
||||
"description": "<p><a href=\"http://donthitsave.com/comic/2016/03/25/code-mode\"><img src=\"http://scripting.com/images/2017/06/25/humor.png\" width=\"300\" height=\"285\" border=\"0\" alt=\"Code mode is a real thing.\"></a></p>\n",
|
||||
"pubDate": "Sun, 25 Jun 2017 15:53:13 GMT",
|
||||
"guid": "http://scripting.com/2017/06/25.html#a110613",
|
||||
"source:outline": {
|
||||
"text": "Code mode is for real",
|
||||
"created": "Sun, 25 Jun 2017 15:53:13 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "<a href=\"http://donthitsave.com/comic/2016/03/25/code-mode\"><img src=\"http://scripting.com/images/2017/06/25/humor.png\" width=\"300\" height=\"285\" border=\"0\" alt=\"Code mode is a real thing.\"></a>",
|
||||
"created": "Sun, 25 Jun 2017 15:53:18 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/25.html#a110618"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/25.html#a110613"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a100601",
|
||||
"description": "Good morning sports fans! 🏈",
|
||||
"pubDate": "Sat, 24 Jun 2017 14:57:01 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a100601",
|
||||
"source:outline": {
|
||||
"text": "Good morning sports fans! :football:",
|
||||
"created": "Sat, 24 Jun 2017 14:57:01 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a100601"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a040606",
|
||||
"description": "<a href=\"http://scripting.com/2017/03/08/theWorldIsSocialistPartIi.html\">Health care is socialist</a> is getting a bunch of new reads today thanks to some powerful RTs.",
|
||||
"pubDate": "Sat, 24 Jun 2017 20:50:06 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a040606",
|
||||
"source:outline": {
|
||||
"text": "<a href=\"http://scripting.com/2017/03/08/theWorldIsSocialistPartIi.html\">Health care is socialist</a> is getting a bunch of new reads today thanks to some powerful RTs.",
|
||||
"created": "Sat, 24 Jun 2017 20:50:06 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a040606"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a020644",
|
||||
"description": "Today is emoji day ⭐️ on Scripting News. We have all the ⭐️ best emoji. And they're free, for you, the best ⭐️ people in the universe, the readers of ⭐️ this humble blog. ⭐️ ",
|
||||
"pubDate": "Sat, 24 Jun 2017 18:34:44 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a020644",
|
||||
"source:outline": {
|
||||
"text": "Today is emoji day :star: on Scripting News. We have all the :star: best emoji. And they're free, for you, the best :star: people in the universe, the readers of :star: this humble blog. :star: ",
|
||||
"created": "Sat, 24 Jun 2017 18:34:44 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a020644"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a020620",
|
||||
"description": "It makes sense that because of <a href=\"https://en.wikipedia.org/wiki/Reconciliation_(United_States_Congress)\">reconcilliation</a> Repubs have a hard time repealing ObamaCare. It seems fair that you should have to have 60 votes to repeal something that required 60 votes to pass. How will they explain it to the voters they've been lying to about \"repeal and replace.\" Double-talk. Swamp-talk. 👍",
|
||||
"pubDate": "Sat, 24 Jun 2017 18:20:20 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a020620",
|
||||
"source:outline": {
|
||||
"text": "It makes sense that because of <a href=\"https://en.wikipedia.org/wiki/Reconciliation_(United_States_Congress)\">reconcilliation</a> Repubs have a hard time repealing ObamaCare. It seems fair that you should have to have 60 votes to repeal something that required 60 votes to pass. How will they explain it to the voters they've been lying to about \"repeal and replace.\" Double-talk. Swamp-talk. :+1:",
|
||||
"created": "Sat, 24 Jun 2017 18:20:20 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a020620"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a020649",
|
||||
"description": "I've started a <a href=\"https://gitter.im/scriptingnews/Lobby\">chatroom</a> on Gitter. Not sure what I'll use it for. Also not sure if it's open for anyone to join. I want it to be. The community <a href=\"http://guidelines.scripting.com/\">guidelines</a> apply. Keep it short, respectful and on-topic, and <i>no spam. </i>⚾️",
|
||||
"pubDate": "Sat, 24 Jun 2017 18:05:49 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a020649",
|
||||
"source:outline": {
|
||||
"text": "I've started a <a href=\"https://gitter.im/scriptingnews/Lobby\">chatroom</a> on Gitter. Not sure what I'll use it for. Also not sure if it's open for anyone to join. I want it to be. The community <a href=\"http://guidelines.scripting.com/\">guidelines</a> apply. Keep it short, respectful and on-topic, and <i>no spam. </i>:baseball:",
|
||||
"created": "Sat, 24 Jun 2017 18:05:49 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a020649"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a110617",
|
||||
"description": "BTW, don't ask me to pitch RSS, I won't do it, because the request is based on a misunderstanding. RSS is not a product, it's a format. I have never made a dime from it. You have as much at stake in its success as I do. So I always turn it around and ask the person who asked me for a pitch to instead pitch <i>me</i> on it. I don't budge on this. Ever. 🏀",
|
||||
"pubDate": "Sat, 24 Jun 2017 15:19:17 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a110617",
|
||||
"source:outline": {
|
||||
"text": "BTW, don't ask me to pitch RSS, I won't do it, because the request is based on a misunderstanding. RSS is not a product, it's a format. I have never made a dime from it. You have as much at stake in its success as I do. So I always turn it around and ask the person who asked me for a pitch to instead pitch <i>me</i> on it. I don't budge on this. Ever. :basketball:",
|
||||
"created": "Sat, 24 Jun 2017 15:19:17 GMT",
|
||||
"type": "outline",
|
||||
"image": "http://scripting.com/images/2017/06/24/rssTShirt.png",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a110617"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a110610",
|
||||
"description": "Is anyone using the <a href=\"http://scripting.com/rss.json\">JSON version</a> of the Scripting News feed in their feed reader? It's a bit of a trick question, in a way, because as far as I know, only <a href=\"https://github.com/scripting/river5\">River5</a> and <a href=\"http://this.how/electricRiver\">Electric River</a> support this format. If so, send me an email at my address, on the About page <a href=\"http://scripting.com/about.html\">here</a>. 🍰",
|
||||
"pubDate": "Sat, 24 Jun 2017 15:01:10 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a110610",
|
||||
"source:outline": {
|
||||
"text": "Is anyone using the <a href=\"http://scripting.com/rss.json\">JSON version</a> of the Scripting News feed in their feed reader? It's a bit of a trick question, in a way, because as far as I know, only \"River5\" and \"Electric River\" support this format. If so, send me an email at my address, on the About page <a href=\"http://scripting.com/about.html\">here</a>. :cake:",
|
||||
"created": "Sat, 24 Jun 2017 15:01:10 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a110610"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a010650",
|
||||
"description": "Journalists keep making a serious technology error -- assuming the only damage Russia can do to our government is during elections. 🇺🇸 ",
|
||||
"pubDate": "Sat, 24 Jun 2017 05:50:50 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a010650",
|
||||
"source:outline": {
|
||||
"text": "Journalists keep making a serious technology error -- assuming the only damage Russia can do to our government is during elections. :us: ",
|
||||
"created": "Sat, 24 Jun 2017 05:50:50 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a010650"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/24.html#a100632",
|
||||
"description": "I get my health insurance through the <a href=\"https://en.wikipedia.org/wiki/Patient_Protection_and_Affordable_Care_Act\">ACA</a>. It's very much alive, <a href=\"http://www.politico.com/story/2017/06/23/sean-spicer-obamacare-repeal-bill-239895\">Spicer</a>. Without it I would not have health insurance. 🍋 ",
|
||||
"pubDate": "Sat, 24 Jun 2017 14:26:32 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a100632",
|
||||
"source:outline": {
|
||||
"text": "I get my health insurance through the <a href=\"https://en.wikipedia.org/wiki/Patient_Protection_and_Affordable_Care_Act\">ACA</a>. It's very much alive, <a href=\"http://www.politico.com/story/2017/06/23/sean-spicer-obamacare-repeal-bill-239895\">Spicer</a>. Without it I would not have health insurance. :lemon: ",
|
||||
"created": "Sat, 24 Jun 2017 14:26:32 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a100632"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "The sad state of tech news in 2017",
|
||||
"link": "http://scripting.com/2017/06/24.html#a090648",
|
||||
"description": "<p>It won't take much to reboot the tech blogosphere, just a few bloggers with ideas who listen to each other and want to work with each other. That was the <a href=\"http://scripting.com/davenet/1996/02/08/holdinghandsincyberspace.html\">idea</a> behind blogrolls, to visibly show the relationships. </p>\n<ul>\n\t<li>I'm still thinking about how to integrate a blogroll with the new design of <a href=\"http://scripting.com/\">Scripting News</a>. I pushed everything aside to get a clean look for the new site. I want to avoid bringing it all back. </li>\n\t</ul>\n<p>So, we have a few people who are writing and listening. That's a needed first step. Next we need a way to announce and hear about new tech products. Not just ones that get VC backing or come from big companies. We already hear about those products through <a href=\"http://techmeme.com/\">TechMeme</a> and the pubs that contribute to it. We also have platform-specific news about tech products, it's more limited, but it's there.</p>\n<p>It will likely start with word of mouth among the bloggers. If <a href=\"http://altplatform.org/2017/06/20/building-a-blogroll-in-2017/\">Richard</a> is using a product and speaks highly of it, I'm likely to take a look. Especially if he says it fits into what I'm doing through the open formats my software already supports. </p>\n<p>Then I want a river, a place where I can go to find out quickly what's new, in the way of products, not BigCo bluster or another $250 million VC deal. I want to know what my peers are doing. So I can learn from them, and so we can make our products work with theirs. </p>\n<p><i>Work together</i> is a phrase you'll hear me use a lot. It's the potential of tech, but it often isn't the attitude of tech. Even the smallest most independent developers dream of dominating. You can't work with people who dominate, even if they win. </p>\n<p>I want to hear about products that are open to connecting to mine. </p>\n<p>There have been times, often defined by news sources, that have created huge swells of compatible technology. To name a few: InfoWorld, PC WEEK, MacWEEK, TechCrunch. Very fond memories of the communities that gathered around each of those. </p>\n<p>It's time for another. The opportunity is there. It's been a long time since we had an open development community that worked to create great new user experience without lockin. It's like riding a bicycle or swimming, you don't forget how to do it. And like tennis or baseball, you can't play without partners and competition. </p>\n",
|
||||
"pubDate": "Sat, 24 Jun 2017 13:43:48 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a090648",
|
||||
"source:outline": {
|
||||
"text": "The sad state of tech news in 2017",
|
||||
"created": "Sat, 24 Jun 2017 13:43:48 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "It won't take much to reboot the tech blogosphere, just a few bloggers with ideas who listen to each other and want to work with each other. That was the <a href=\"http://scripting.com/davenet/1996/02/08/holdinghandsincyberspace.html\">idea</a> behind blogrolls, to visibly show the relationships. ",
|
||||
"created": "Sat, 24 Jun 2017 13:43:55 GMT",
|
||||
"subs": [
|
||||
{
|
||||
"text": "I'm still thinking about how to integrate a blogroll with the new design of \"Scripting News\". I pushed everything aside to get a clean look for the new site. I want to avoid bringing it all back. ",
|
||||
"created": "Sat, 24 Jun 2017 14:32:17 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a100617"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a090655"
|
||||
},
|
||||
{
|
||||
"text": "So, we have a few people who are writing and listening. That's a needed first step. Next we need a way to announce and hear about new tech products. Not just ones that get VC backing or come from big companies. We already hear about those products through \"TechMeme\" and the pubs that contribute to it. We also have platform-specific news about tech products, it's more limited, but it's there.",
|
||||
"created": "Sat, 24 Jun 2017 13:44:09 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a090609"
|
||||
},
|
||||
{
|
||||
"text": "It will likely start with word of mouth among the bloggers. If <a href=\"http://altplatform.org/2017/06/20/building-a-blogroll-in-2017/\">Richard</a> is using a product and speaks highly of it, I'm likely to take a look. Especially if he says it fits into what I'm doing through the open formats my software already supports. ",
|
||||
"created": "Sat, 24 Jun 2017 13:52:04 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a090604"
|
||||
},
|
||||
{
|
||||
"text": "Then I want a river, a place where I can go to find out quickly what's new, in the way of products, not BigCo bluster or another $250 million VC deal. I want to know what my peers are doing. So I can learn from them, and so we can make our products work with theirs. ",
|
||||
"created": "Sat, 24 Jun 2017 13:52:04 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a090604"
|
||||
},
|
||||
{
|
||||
"text": "<i>Work together</i> is a phrase you'll hear me use a lot. It's the potential of tech, but it often isn't the attitude of tech. Even the smallest most independent developers dream of dominating. You can't work with people who dominate, even if they win. ",
|
||||
"created": "Sat, 24 Jun 2017 13:54:33 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a090633"
|
||||
},
|
||||
{
|
||||
"text": "I want to hear about products that are open to connecting to mine. ",
|
||||
"created": "Sat, 24 Jun 2017 20:53:41 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a040641"
|
||||
},
|
||||
{
|
||||
"text": "There have been times, often defined by news sources, that have created huge swells of compatible technology. To name a few: InfoWorld, PC WEEK, MacWEEK, TechCrunch. Very fond memories of the communities that gathered around each of those. ",
|
||||
"created": "Sat, 24 Jun 2017 13:48:02 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a090602"
|
||||
},
|
||||
{
|
||||
"text": "It's time for another. The opportunity is there. It's been a long time since we had an open development community that worked to create great new user experience without lockin. It's like riding a bicycle or swimming, you don't forget how to do it. And like tennis or baseball, you can't play without partners and competition. ",
|
||||
"created": "Sat, 24 Jun 2017 13:57:02 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a090602"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a090648"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Republican-inspired art",
|
||||
"link": "http://scripting.com/2017/06/24.html#a100632",
|
||||
"description": "<p><a href=\"http://boingboing.net/2017/06/23/the-white-house-is-having-off.html\"><img src=\"http://scripting.com/images/2017/06/24/spicer.png\" width=\"502\" height=\"339\" border=\"0\" alt=\"A picture named spicer.png\"></a></p>\n",
|
||||
"pubDate": "Sat, 24 Jun 2017 14:52:32 GMT",
|
||||
"guid": "http://scripting.com/2017/06/24.html#a100632",
|
||||
"source:outline": {
|
||||
"text": "Republican-inspired art",
|
||||
"created": "Sat, 24 Jun 2017 14:52:32 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "<a href=\"http://boingboing.net/2017/06/23/the-white-house-is-having-off.html\"><img src=\"http://scripting.com/images/2017/06/24/spicer.png\" width=\"502\" height=\"339\" border=\"0\" alt=\"A picture named spicer.png\"></a>",
|
||||
"created": "Sat, 24 Jun 2017 14:52:37 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a100637"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/24.html#a100632"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/23.html#a070648",
|
||||
"description": "Thank you <a href=\"https://twitter.com/om/status/878075081204711424\">Om</a> for the tweet-love. ❤️ ",
|
||||
"pubDate": "Fri, 23 Jun 2017 11:50:48 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a070648",
|
||||
"source:outline": {
|
||||
"text": "Thank you <a href=\"https://twitter.com/om/status/878075081204711424\">Om</a> for the tweet-love. :heart: ",
|
||||
"created": "Fri, 23 Jun 2017 11:50:48 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070648"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/23.html#a120634",
|
||||
"description": "<a href=\"https://twitter.com/davewiner/status/878279505453826049\">An idea worth RT'ing</a>: \"A site people could go to, fill in some info about themselves, and find out how much they would lose under the Repub plan.\"",
|
||||
"pubDate": "Fri, 23 Jun 2017 16:02:34 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a120634",
|
||||
"source:outline": {
|
||||
"text": "<a href=\"https://twitter.com/davewiner/status/878279505453826049\">An idea worth RT'ing</a>: \"A site people could go to, fill in some info about themselves, and find out how much they would lose under the Repub plan.\"",
|
||||
"created": "Fri, 23 Jun 2017 16:02:34 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a120634"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/23.html#a090638",
|
||||
"description": "<a href=\"https://github.com/scripting/githubpub/blob/master/README.md\">githubpub</a> is a Node app that serves from GitHub repositories.",
|
||||
"pubDate": "Fri, 23 Jun 2017 13:52:38 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a090638",
|
||||
"source:outline": {
|
||||
"text": "\"githubpub\" is a Node app that serves from GitHub repositories.",
|
||||
"created": "Fri, 23 Jun 2017 13:52:38 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a090638"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/23.html#a100602",
|
||||
"description": "Fix for the Scripting News <a href=\"http://scripting.com/rss.xml\">RSS</a> <a href=\"http://scripting.com/rss.json\">feed</a>: we now process glossary entries and emoji short codes. The net effect is that text shortcuts like <a href=\"http://cyber.law.harvard.edu/rss/rss.html\">RSS</a> will be expanded as well as 👏 emoji 👏, in the feeds. As they say, <i>still diggin!</i>",
|
||||
"pubDate": "Fri, 23 Jun 2017 14:52:02 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a100602",
|
||||
"source:outline": {
|
||||
"text": "Fix for the Scripting News <a href=\"http://scripting.com/rss.xml\">RSS</a> <a href=\"http://scripting.com/rss.json\">feed</a>: we now process glossary entries and emoji short codes. The net effect is that text shortcuts like \"rss\" will be expanded as well as :clap: emoji :clap:, in the feeds. As they say, <i>still diggin!</i>",
|
||||
"created": "Fri, 23 Jun 2017 14:52:02 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100602"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/23.html#a070659",
|
||||
"description": "So many thoughts today begin with -- If the Dems only had their shit together. ",
|
||||
"pubDate": "Fri, 23 Jun 2017 11:52:59 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a070659",
|
||||
"source:outline": {
|
||||
"text": "So many thoughts today begin with -- If the Dems only had their shit together. ",
|
||||
"created": "Fri, 23 Jun 2017 11:52:59 GMT",
|
||||
"type": "tweet",
|
||||
"tweetId": "878220762343133184",
|
||||
"tweetUserName": "davewiner",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070659"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/23.html#a070624",
|
||||
"description": "If the Dems only had their shit together, we would be mobleizable to knock on neighbor's doors this weekend with pre-written talking points. \"Did you know that you <s>could</s> will lose your health insurance if the Republicans have their way?\"",
|
||||
"pubDate": "Fri, 23 Jun 2017 11:53:24 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a070624",
|
||||
"source:outline": {
|
||||
"text": "If the Dems only had their shit together, we would be mobleizable to knock on neighbor's doors this weekend with pre-written talking points. \"Did you know that you <s>could</s> will lose your health insurance if the Republicans have their way?\"",
|
||||
"created": "Fri, 23 Jun 2017 11:53:24 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070624"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/23.html#a070633",
|
||||
"description": "Repubs who consider voting for the repeal of Medicaid and the ACA should fear the <a href=\"https://en.wikipedia.org/wiki/Hell\">hellfire</a> they will face when they run for re-election. <i>This weekend</i> is the time to make your feelings felt. ",
|
||||
"pubDate": "Fri, 23 Jun 2017 11:47:33 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a070633",
|
||||
"source:outline": {
|
||||
"text": "Repubs who consider voting for the repeal of Medicaid and the ACA should fear the <a href=\"https://en.wikipedia.org/wiki/Hell\">hellfire</a> they will face when they run for re-election. <i>This weekend</i> is the time to make your feelings felt. ",
|
||||
"created": "Fri, 23 Jun 2017 11:47:33 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070633"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/23.html#a070641",
|
||||
"description": "BTW, if someone expresses frustration it doesn't follow that they blame you, esp on Twitter which is a length-impaired medium. ",
|
||||
"pubDate": "Fri, 23 Jun 2017 11:45:41 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a070641",
|
||||
"source:outline": {
|
||||
"text": "BTW, if someone expresses frustration it doesn't follow that they blame you, esp on Twitter which is a length-impaired medium. ",
|
||||
"created": "Fri, 23 Jun 2017 11:45:41 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070641"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Thin servers",
|
||||
"link": "http://scripting.com/2017/06/23.html#a100620",
|
||||
"description": "<p>Two friends, <a href=\"https://twitter.com/judell\">Jon Udell</a> and <a href=\"https://twitter.com/holden\">Mike Caulfield</a>, are talking about \"dumb\" servers. I call the same things \"thin.\" Also fractional-horsepower servers. They go by a bunch of names, but the idea and motivation is the same. </p>\n<p>The idea: We move functionality from the server to the edge (desktop, mobile device), repeating until someday there's nothing left on the server. We could go all the way, but it needs a strong operational backend, something a big company is good at, not so much individuals. (With the caveat that some think this problem is distributable, notably the fictional <a href=\"https://www.crunchbase.com/person/richard-hendriks#/entity\">CEO</a> of <a href=\"http://www.piedpiper.com/\">Pied Piper</a>.)</p>\n<p>The key thing is identity. Once you have that solved, it all becomes relatively easy. I've factored out identity into a layer I call <a href=\"https://github.com/scripting/nodeStorage\">nodeStorage</a>. It associates storage with a user's Twitter identity. Twitter is a good service to use, unlike some others, because they have a liberal policy of who gets to <a href=\"https://apps.twitter.com/app/new\">create</a> apps. Faceook has an extensive vetting process. Twitter <a href=\"https://apps.twitter.com/app/new\">is</a> \"let a thousand flowers bloom.\" I know some people have problems with Twitter, but I've learned over many years that all corporate vendors are imperfect. If you're waiting for <a href=\"http://this.how/standards#1497798834000\">perfection</a> you'll wait forever. And you build the software so that if Twitter should again become draconian, a new service can be filled in with as little disruption as possible.</p>\n<p>Another place I've looked is Dropbox. There, with one simple feature, the ability to associate a domain with a folder, they would solve the problem. I know there are external services that provide something \"like\" this, but fundamentally Dropbox doesn't provide enough flexibility in the API to do this in a reasonable way. (Lack of granularity in permissions, an app gets access to one folder or everything.)</p>\n<p>Or Amazon, if their identity system for AWS were simpler for end users, or if their end-user storage system could be accessed through the S3 API. I'm sure they've thought of it. There must be a reason they don't do it. </p>\n<p>And Twitter could completely eliminate the need for nodeStorage, by offering users a few gigabytes of storage attached to their Twitter account, accessible through the API. The first person who described the feature to me was Jack Dorsey, about eight years ago, when we met for coffee in SF. So he understands why this idea is so powerful. I'm not sure what the holdup is. </p>\n<p>In the meantime, <a href=\"https://github.com/scripting/nodeStorage\">nodeStorage</a> works. I build the kind of apps I want. Open the sidebar on Scripting News (left margin) and have a look at the apps. Some even have <a href=\"https://github.com/scripting/macwrite/blob/master/macwrite.js\">source code</a> so you can see for yourself. </p>\n",
|
||||
"pubDate": "Fri, 23 Jun 2017 14:03:20 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a100620",
|
||||
"source:outline": {
|
||||
"text": "Thin servers",
|
||||
"created": "Fri, 23 Jun 2017 14:03:20 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "Two friends, <a href=\"https://twitter.com/judell\">Jon Udell</a> and <a href=\"https://twitter.com/holden\">Mike Caulfield</a>, are talking about \"dumb\" servers. I call the same things \"thin.\" Also fractional-horsepower servers. They go by a bunch of names, but the idea and motivation is the same. ",
|
||||
"created": "Fri, 23 Jun 2017 14:05:30 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100630"
|
||||
},
|
||||
{
|
||||
"text": "The idea: We move functionality from the server to the edge (desktop, mobile device), repeating until someday there's nothing left on the server. We could go all the way, but it needs a strong operational backend, something a big company is good at, not so much individuals. (With the caveat that some think this problem is distributable, notably the fictional <a href=\"https://www.crunchbase.com/person/richard-hendriks#/entity\">CEO</a> of <a href=\"http://www.piedpiper.com/\">Pied Piper</a>.)",
|
||||
"created": "Fri, 23 Jun 2017 14:24:35 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100635"
|
||||
},
|
||||
{
|
||||
"text": "The key thing is identity. Once you have that solved, it all becomes relatively easy. I've factored out identity into a layer I call \"nodeStorage\". It associates storage with a user's Twitter identity. Twitter is a good service to use, unlike some others, because they have a liberal policy of who gets to <a href=\"https://apps.twitter.com/app/new\">create</a> apps. Faceook has an extensive vetting process. Twitter <a href=\"https://apps.twitter.com/app/new\">is</a> \"let a thousand flowers bloom.\" I know some people have problems with Twitter, but I've learned over many years that all corporate vendors are imperfect. If you're waiting for <a href=\"http://this.how/standards#1497798834000\">perfection</a> you'll wait forever. And you build the software so that if Twitter should again become draconian, a new service can be filled in with as little disruption as possible.",
|
||||
"created": "Fri, 23 Jun 2017 14:05:43 GMT",
|
||||
"image": "http://scripting.com/images/2017/06/17/bowling.png",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100643"
|
||||
},
|
||||
{
|
||||
"text": "Another place I've looked is Dropbox. There, with one simple feature, the ability to associate a domain with a folder, they would solve the problem. I know there are external services that provide something \"like\" this, but fundamentally Dropbox doesn't provide enough flexibility in the API to do this in a reasonable way. (Lack of granularity in permissions, an app gets access to one folder or everything.)",
|
||||
"created": "Fri, 23 Jun 2017 14:08:41 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100641"
|
||||
},
|
||||
{
|
||||
"text": "Or Amazon, if their identity system for AWS were simpler for end users, or if their end-user storage system could be accessed through the S3 API. I'm sure they've thought of it. There must be a reason they don't do it. ",
|
||||
"created": "Fri, 23 Jun 2017 14:09:10 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100610"
|
||||
},
|
||||
{
|
||||
"text": "And Twitter could completely eliminate the need for nodeStorage, by offering users a few gigabytes of storage attached to their Twitter account, accessible through the API. The first person who described the feature to me was Jack Dorsey, about eight years ago, when we met for coffee in SF. So he understands why this idea is so powerful. I'm not sure what the holdup is. ",
|
||||
"created": "Fri, 23 Jun 2017 14:09:55 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100655"
|
||||
},
|
||||
{
|
||||
"text": "In the meantime, \"nodeStorage\" works. I build the kind of apps I want. Open the sidebar on Scripting News (left margin) and have a look at the apps. Some even have <a href=\"https://github.com/scripting/macwrite/blob/master/macwrite.js\">source code</a> so you can see for yourself. ",
|
||||
"created": "Fri, 23 Jun 2017 14:33:10 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100610"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a100620"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "RicMac, part II",
|
||||
"link": "http://scripting.com/2017/06/23.html#a070643",
|
||||
"description": "<p><a href=\"https://twitter.com/ricmac\">Richard MacManus</a> keeps on truckin. There's nothing more powerful than a persistent and curious user who's relatively fearless. </p>\n<p>In a follow-up <a href=\"https://richardmacmanus.com/2017/06/22/openness-rivers-indieweb/\">post</a> I learned that there is an IndieWeb-approved feed reader called <a href=\"https://github.com/kylewm/woodwind\">Woodwind</a>. That's good news. <a href=\"http://cyber.law.harvard.edu/rss/rss.html\">RSS</a> and related technolgies, <a href=\"https://github.com/kylewm/woodwind/issues/66\">including</a> <a href=\"https://github.com/kylewm/woodwind/issues/7\">OPML</a> import and export, are essential components of the open web. </p>\n<p>BTW, to Richard, I wrote up my <a href=\"http://this.how/standards\">rules for standards-makers</a>, based on experience re what (imho) is important and what works and doesn't. Another item for your consideration. </p>\n",
|
||||
"pubDate": "Fri, 23 Jun 2017 11:14:43 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a070643",
|
||||
"source:outline": {
|
||||
"text": "RicMac, part II",
|
||||
"created": "Fri, 23 Jun 2017 11:14:43 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "<a href=\"https://twitter.com/ricmac\">Richard MacManus</a> keeps on truckin. There's nothing more powerful than a persistent and curious user who's relatively fearless. ",
|
||||
"created": "Fri, 23 Jun 2017 11:20:38 GMT",
|
||||
"image": "http://scripting.com/images/2017/06/23/mrNatural.png",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070638"
|
||||
},
|
||||
{
|
||||
"text": "In a follow-up <a href=\"https://richardmacmanus.com/2017/06/22/openness-rivers-indieweb/\">post</a> I learned that there is an IndieWeb-approved feed reader called <a href=\"https://github.com/kylewm/woodwind\">Woodwind</a>. That's good news. \"RSS\" and related technolgies, <a href=\"https://github.com/kylewm/woodwind/issues/66\">including</a> <a href=\"https://github.com/kylewm/woodwind/issues/7\">OPML</a> import and export, are essential components of the open web. ",
|
||||
"created": "Fri, 23 Jun 2017 11:15:54 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070654"
|
||||
},
|
||||
{
|
||||
"text": "BTW, to Richard, I wrote up my <a href=\"http://this.how/standards\">rules for standards-makers</a>, based on experience re what (imho) is important and what works and doesn't. Another item for your consideration. ",
|
||||
"created": "Fri, 23 Jun 2017 11:19:38 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070638"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a070643"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Test post",
|
||||
"link": "http://scripting.com/2017/06/23.html#a030621",
|
||||
"description": "<p>Here's a list with four items</p>\n<ul>\n\t<li>one</li>\n\t<li>two</li>\n\t<li>three</li>\n\t<li>four</li>\n\t</ul>\n",
|
||||
"pubDate": "Fri, 23 Jun 2017 19:48:21 GMT",
|
||||
"guid": "http://scripting.com/2017/06/23.html#a030621",
|
||||
"source:outline": {
|
||||
"text": "Test post",
|
||||
"created": "Fri, 23 Jun 2017 19:48:21 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "Here's a list with four items",
|
||||
"created": "Fri, 23 Jun 2017 19:49:21 GMT",
|
||||
"flNumberedSubs": "true",
|
||||
"subs": [
|
||||
{
|
||||
"text": "one",
|
||||
"created": "Fri, 23 Jun 2017 19:48:43 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a030643"
|
||||
},
|
||||
{
|
||||
"text": "two",
|
||||
"created": "Fri, 23 Jun 2017 19:48:44 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a030644"
|
||||
},
|
||||
{
|
||||
"text": "three",
|
||||
"created": "Fri, 23 Jun 2017 19:48:45 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a030645"
|
||||
},
|
||||
{
|
||||
"text": "four",
|
||||
"created": "Fri, 23 Jun 2017 19:48:46 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a030646"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a030621"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/23.html#a030621"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/22.html#a120633",
|
||||
"description": "I'm working on a new Node web server that serves out of GitHub repos. It's a very sweet very small piece of software. ",
|
||||
"pubDate": "Thu, 22 Jun 2017 16:28:33 GMT",
|
||||
"guid": "http://scripting.com/2017/06/22.html#a120633",
|
||||
"source:outline": {
|
||||
"text": "I'm working on a new Node web server that serves out of GitHub repos. It's a very sweet very small piece of software. ",
|
||||
"created": "Thu, 22 Jun 2017 16:28:33 GMT",
|
||||
"type": "tweet",
|
||||
"tweetId": "877926320822464512",
|
||||
"tweetUserName": "davewiner",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a120633"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/22.html#a090616",
|
||||
"description": "An epiphany. Mark Zuckerberg is his generation's <a href=\"https://en.wikipedia.org/wiki/Ray_Kroc\">Ray Kroc</a>, and Facebook is <a href=\"https://www.mcdonalds.com/us/en-us.html\">McDonald's</a>. I aspire to be <a href=\"https://en.wikipedia.org/wiki/Alice_Waters\">Alice Waters</a>.",
|
||||
"pubDate": "Thu, 22 Jun 2017 13:29:16 GMT",
|
||||
"guid": "http://scripting.com/2017/06/22.html#a090616",
|
||||
"source:outline": {
|
||||
"text": "An epiphany. Mark Zuckerberg is his generation's <a href=\"https://en.wikipedia.org/wiki/Ray_Kroc\">Ray Kroc</a>, and Facebook is <a href=\"https://www.mcdonalds.com/us/en-us.html\">McDonald's</a>. I aspire to be <a href=\"https://en.wikipedia.org/wiki/Alice_Waters\">Alice Waters</a>.",
|
||||
"created": "Thu, 22 Jun 2017 13:29:16 GMT",
|
||||
"type": "tweet",
|
||||
"tweetId": "877881227298000896",
|
||||
"tweetUserName": "davewiner",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a090616"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/22.html#a090644",
|
||||
"description": "Future-of-journalism conferences that ignore blogging are not about the future of journalism.",
|
||||
"pubDate": "Thu, 22 Jun 2017 13:29:44 GMT",
|
||||
"guid": "http://scripting.com/2017/06/22.html#a090644",
|
||||
"source:outline": {
|
||||
"text": "Future-of-journalism conferences that ignore blogging are not about the future of journalism.",
|
||||
"created": "Thu, 22 Jun 2017 13:29:44 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a090644"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/22.html#a120629",
|
||||
"description": "News will be interesting tonight. They've got the Repub health care <a href=\"https://www.axios.com/on-health-care-moderates-quiet-while-conservatives-put-their-foot-down-2446375738.html\">bill</a> to rip apart, and it's also <a href=\"http://nbariver.com/\">NBA Draft</a> night. ",
|
||||
"pubDate": "Thu, 22 Jun 2017 16:27:29 GMT",
|
||||
"guid": "http://scripting.com/2017/06/22.html#a120629",
|
||||
"source:outline": {
|
||||
"text": "News will be interesting tonight. They've got the Repub health care <a href=\"https://www.axios.com/on-health-care-moderates-quiet-while-conservatives-put-their-foot-down-2446375738.html\">bill</a> to rip apart, and it's also <a href=\"http://nbariver.com/\">NBA Draft</a> night. ",
|
||||
"created": "Thu, 22 Jun 2017 16:27:29 GMT",
|
||||
"type": "tweet",
|
||||
"tweetId": "877926057260728320",
|
||||
"tweetUserName": "davewiner",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a120629"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Fargo puzzler",
|
||||
"link": "http://scripting.com/2017/06/22.html#a030610",
|
||||
"description": "<p>The last two episodes of <a href=\"https://en.wikipedia.org/wiki/Fargo_(season_3)\">season 3</a> of Fargo were fantastic. But, the opening scene of episode 1, which takes place in a police office in East Germany during the Cold War, is without explanation. </p>\n<p>All through the season, I was wondering how it was going to be connected up with the story that takes place in Minnesota in 2011, but as far as I know it never was.</p>\n<p>Maybe that was VM Varga as the accused? Or the police guy?</p>\n<p>This is kind of bothering me! :-)</p>\n<p>Okay then...</p>\n<p>Update: In the <a href=\"https://en.wikipedia.org/wiki/Fargo_(season_3)#Episodes\">episode guide</a> on Wikipedia they describe the opening scene as follows: \"In 1988 East Berlin, Jacob Ungerleider is questioned in the death of a woman, which he claims is a case of mistaken identity.\" So it's not VM Varga in the hot seat. Who is Jacob Ungerleider? I have no idea! ;-)</p>\n",
|
||||
"pubDate": "Thu, 22 Jun 2017 19:26:10 GMT",
|
||||
"guid": "http://scripting.com/2017/06/22.html#a030610",
|
||||
"source:outline": {
|
||||
"text": "Fargo puzzler",
|
||||
"created": "Thu, 22 Jun 2017 19:26:10 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "The last two episodes of <a href=\"https://en.wikipedia.org/wiki/Fargo_(season_3)\">season 3</a> of Fargo were fantastic. But, the opening scene of episode 1, which takes place in a police office in East Germany during the Cold War, is without explanation. ",
|
||||
"created": "Thu, 22 Jun 2017 19:26:18 GMT",
|
||||
"image": "http://scripting.com/images/2017/06/22/paulBunyan.png",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a030618"
|
||||
},
|
||||
{
|
||||
"text": "All through the season, I was wondering how it was going to be connected up with the story that takes place in Minnesota in 2011, but as far as I know it never was.",
|
||||
"created": "Thu, 22 Jun 2017 19:27:20 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a030620"
|
||||
},
|
||||
{
|
||||
"text": "Maybe that was VM Varga as the accused? Or the police guy?",
|
||||
"created": "Thu, 22 Jun 2017 19:27:54 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a030654"
|
||||
},
|
||||
{
|
||||
"text": "This is kind of bothering me! :-)",
|
||||
"created": "Thu, 22 Jun 2017 19:28:15 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a030615"
|
||||
},
|
||||
{
|
||||
"text": "Okay then...",
|
||||
"created": "Thu, 22 Jun 2017 19:28:27 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a030627"
|
||||
},
|
||||
{
|
||||
"text": "Update: In the <a href=\"https://en.wikipedia.org/wiki/Fargo_(season_3)#Episodes\">episode guide</a> on Wikipedia they describe the opening scene as follows: \"In 1988 East Berlin, Jacob Ungerleider is questioned in the death of a woman, which he claims is a case of mistaken identity.\" So it's not VM Varga in the hot seat. Who is Jacob Ungerleider? I have no idea! ;-)",
|
||||
"created": "Thu, 22 Jun 2017 19:38:25 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a030625"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/22.html#a030610"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/21.html#a060647",
|
||||
"description": "Good morning summer solstice fans!",
|
||||
"pubDate": "Wed, 21 Jun 2017 10:21:47 GMT",
|
||||
"guid": "http://scripting.com/2017/06/21.html#a060647",
|
||||
"source:outline": {
|
||||
"text": "Good morning summer solstice fans!",
|
||||
"created": "Wed, 21 Jun 2017 10:21:47 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060647"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/21.html#a060611",
|
||||
"description": "It was a boring NBA postseason, for the most. But the excitement of next season is already starting, with the draft tomorrow, and deal season in full swing. The place to find all the news is <a href=\"http://nbariver.com/\">nbariver.com</a>. It's one of many rivers maintained by my <a href=\"https://github.com/scripting/river5\">River5</a> installation. ",
|
||||
"pubDate": "Wed, 21 Jun 2017 10:22:11 GMT",
|
||||
"guid": "http://scripting.com/2017/06/21.html#a060611",
|
||||
"source:outline": {
|
||||
"text": "It was a boring NBA postseason, for the most. But the excitement of next season is already starting, with the draft tomorrow, and deal season in full swing. The place to find all the news is <a href=\"http://nbariver.com/\">nbariver.com</a>. It's one of many rivers maintained by my \"River5\" installation. ",
|
||||
"created": "Wed, 21 Jun 2017 10:22:11 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060611"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/21.html#a060632",
|
||||
"description": "BTW, I hate the term \"eating the dogfood.\" As much as I love dogs, it says that our users are pets, not sentient human beings, our equals. It also says our software is dog food. I think as a kid, as an experiment, a few of us kids actually ate dog food. It's a vague memory, that must have some basis in reality. It makes me nauseous to think about it. And that's what I think about when I hear the term. Please, let's find another way of saying \"My software is good because I use it, and vice versa.\"",
|
||||
"pubDate": "Wed, 21 Jun 2017 10:57:32 GMT",
|
||||
"guid": "http://scripting.com/2017/06/21.html#a060632",
|
||||
"source:outline": {
|
||||
"text": "BTW, I hate the term \"eating the dogfood.\" As much as I love dogs, it says that our users are pets, not sentient human beings, our equals. It also says our software is dog food. I think as a kid, as an experiment, a few of us kids actually ate dog food. It's a vague memory, that must have some basis in reality. It makes me nauseous to think about it. And that's what I think about when I hear the term. Please, let's find another way of saying \"My software is good because I use it, and vice versa.\"",
|
||||
"created": "Wed, 21 Jun 2017 10:57:32 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060632"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/21.html#a070633",
|
||||
"description": "Yesterday I posted a <a href=\"http://scripting.com/images/2017/06/21/frozenBlogPost.png\">screen shot</a> of one of my posts on Facebook, to accolades from friends on Facebook. I deleted the post. I won't be doing it again. Facebook is not a place for blog posts. Not as long as they disable linking, styles, titles and podcasts. If you want to help Facebook destroy the open web, go for it. But I will not participate in that awful adventure.",
|
||||
"pubDate": "Wed, 21 Jun 2017 11:01:33 GMT",
|
||||
"guid": "http://scripting.com/2017/06/21.html#a070633",
|
||||
"source:outline": {
|
||||
"text": "Yesterday I posted a <a href=\"http://scripting.com/images/2017/06/21/frozenBlogPost.png\">screen shot</a> of one of my posts on Facebook, to accolades from friends on Facebook. I deleted the post. I won't be doing it again. Facebook is not a place for blog posts. Not as long as they disable linking, styles, titles and podcasts. If you want to help Facebook destroy the open web, go for it. But I will not participate in that awful adventure.",
|
||||
"created": "Wed, 21 Jun 2017 11:01:33 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a070633"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/21.html#a060600",
|
||||
"description": "On Facebook you are who the algorithm says you are. ",
|
||||
"pubDate": "Wed, 21 Jun 2017 10:53:00 GMT",
|
||||
"guid": "http://scripting.com/2017/06/21.html#a060600",
|
||||
"source:outline": {
|
||||
"text": "On Facebook you are who the algorithm says you are. ",
|
||||
"created": "Wed, 21 Jun 2017 10:53:00 GMT",
|
||||
"type": "outline",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060600"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "GitHub API example app",
|
||||
"link": "http://scripting.com/2017/06/21.html#a110614",
|
||||
"description": "<p>A simple <a href=\"http://scripting.com/misc/code/githubapi/directory.html\">web app</a> that travels through the <a href=\"https://github.com/scripting/river5\">River5 repository</a> in my GitHub account, producing a directory that reflects the structure of the repo.</p>\n<p>I couldn't <a href=\"https://duckduckgo.com/?q=github+api+example+app+directory+repository&t=h_&ia=web\">find</a> sample code that does this simple thing. Now I won't have to hunt for it, and neither will you. ;-) </p>\n<p>Here's the <a href=\"https://gist.github.com/scripting/f5e5b3a175265f47fda098cb5bddca2f\">source code</a>. </p>\n",
|
||||
"pubDate": "Wed, 21 Jun 2017 15:18:14 GMT",
|
||||
"guid": "http://scripting.com/2017/06/21.html#a110614",
|
||||
"source:outline": {
|
||||
"text": "GitHub API example app",
|
||||
"created": "Wed, 21 Jun 2017 15:18:14 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "A simple <a href=\"http://scripting.com/misc/code/githubapi/directory.html\">web app</a> that travels through the <a href=\"https://github.com/scripting/river5\">River5 repository</a> in my GitHub account, producing a directory that reflects the structure of the repo.",
|
||||
"created": "Wed, 21 Jun 2017 15:18:24 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a110624"
|
||||
},
|
||||
{
|
||||
"text": "I couldn't <a href=\"https://duckduckgo.com/?q=github+api+example+app+directory+repository&t=h_&ia=web\">find</a> sample code that does this simple thing. Now I won't have to hunt for it, and neither will you. ;-) ",
|
||||
"created": "Wed, 21 Jun 2017 15:18:36 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a110636"
|
||||
},
|
||||
{
|
||||
"text": "Here's the <a href=\"https://gist.github.com/scripting/f5e5b3a175265f47fda098cb5bddca2f\">source code</a>. ",
|
||||
"created": "Wed, 21 Jun 2017 15:19:39 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a110639"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a110614"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Bike video from two years ago",
|
||||
"link": "http://scripting.com/2017/06/21.html#a020637",
|
||||
"description": "<p><iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/iWvdXdW7GiI\" frameborder=\"0\" allowfullscreen></iframe></p>\n",
|
||||
"pubDate": "Wed, 21 Jun 2017 18:47:37 GMT",
|
||||
"guid": "http://scripting.com/2017/06/21.html#a020637",
|
||||
"source:outline": {
|
||||
"text": "Bike video from two years ago",
|
||||
"created": "Wed, 21 Jun 2017 18:47:37 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/iWvdXdW7GiI\" frameborder=\"0\" allowfullscreen></iframe>",
|
||||
"created": "Wed, 21 Jun 2017 18:47:50 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a020650"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a020637"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "An old friend: Richard MacManus",
|
||||
"link": "http://scripting.com/2017/06/21.html#a060651",
|
||||
"description": "<p>Richard is one of the old school bloggers. He started <a href=\"https://web.archive.org/web/20031014183603/http://readwriteweb.com\">ReadWriteWeb</a> in 2003. It started as a Radio UserLand project and grew into a leading tech publication, something which I'm personally proud of. </p>\n<p>He has a <a href=\"http://altplatform.org/2017/06/20/building-a-blogroll-in-2017/\">new blog</a> up and running. I've added it to my <a href=\"http://scripting.com/river.html\">personal river</a> here on Scripting News. He asks about where the blogrolls have gone, a topic I <a href=\"http://scripting.com/2017/06/18.html#a110612\">wrote about</a> a couple of days ago. Richard would certainly be in my blogroll.</p>\n<p>Maybe the subscription list <a href=\"http://scripting.com/2016/09/23/otherBlogsLikeScriptingNews.html\">for</a> my <a href=\"http://bloggers.scripting.com/\">blogger's river</a> would make a good start for my blogroll, or vice versa? Something we didn't do in the first iteration is make our rivers public. Nowadays I'm doing that routinely. A few examples are in the left sidebar here on <a href=\"http://scripting.com/\">Scripting News</a>. </p>\n<p>Richard has turned to <a href=\"https://indieweb.org/\">IndieWeb</a> for the latest on open web tech. That's fine, but you have to look elsewhere too, because as he's discovered, they only embrace part of the open web. It's too bad they chose such an inclusive name, but have an exclusive approach. For example, they have avoided <a href=\"http://cyber.law.harvard.edu/rss/rss.html\">RSS</a>, for reasons I'm sure I don't understand (I've listened, so no need to repeat the reasoning). We need all the advantages we can get because there are <a href=\"http://scripting.com/2017/06/18.html#a090658\">serious</a> <a href=\"http://scripting.com/2016/10/03/turnsOutLessFacebookIsOk.html\">headwinds</a> these days for blogging. RSS is serious open web technology. To not build on it is unthinkable, for me at least. </p>\n<p>Re integration between writing and reading, another topic of interest to Richard, all my rivers <a href=\"http://scripting.com/images/2017/06/21/postingFromARiver.png\">hook</a> into <a href=\"http://radio3.io/\">Radio3</a>, which is my latest <a href=\"http://scripting.com/links.html\">linkblogging</a> tool. For reading, I encouraged Richard to look at <a href=\"http://this.how/electricRiver\">Electric River</a>, it's the closest to what Radio UserLand did with aggregation in 2002. It runs on your Mac desktop, as the original did. When he wants to go all-in with rivers, nothing can take the place of <a href=\"https://github.com/scripting/river5\">River5</a>, which is getting both modular and deep. I'm doing more work on that. Rivers have not finished evolving as far as I'm concerned. </p>\n",
|
||||
"pubDate": "Wed, 21 Jun 2017 10:29:51 GMT",
|
||||
"guid": "http://scripting.com/2017/06/21.html#a060651",
|
||||
"source:outline": {
|
||||
"text": "An old friend: Richard MacManus",
|
||||
"created": "Wed, 21 Jun 2017 10:29:51 GMT",
|
||||
"type": "outline",
|
||||
"subs": [
|
||||
{
|
||||
"text": "Richard is one of the old school bloggers. He started <a href=\"https://web.archive.org/web/20031014183603/http://readwriteweb.com\">ReadWriteWeb</a> in 2003. It started as a Radio UserLand project and grew into a leading tech publication, something which I'm personally proud of. ",
|
||||
"created": "Wed, 21 Jun 2017 10:33:59 GMT",
|
||||
"image": "http://scripting.com/2016/03/10/clown.png",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060659"
|
||||
},
|
||||
{
|
||||
"text": "He has a <a href=\"http://altplatform.org/2017/06/20/building-a-blogroll-in-2017/\">new blog</a> up and running. I've added it to my <a href=\"http://scripting.com/river.html\">personal river</a> here on Scripting News. He asks about where the blogrolls have gone, a topic I <a href=\"http://scripting.com/2017/06/18.html#a110612\">wrote about</a> a couple of days ago. Richard would certainly be in my blogroll.",
|
||||
"created": "Wed, 21 Jun 2017 10:25:16 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060616"
|
||||
},
|
||||
{
|
||||
"text": "Maybe the subscription list <a href=\"http://scripting.com/2016/09/23/otherBlogsLikeScriptingNews.html\">for</a> my <a href=\"http://bloggers.scripting.com/\">blogger's river</a> would make a good start for my blogroll, or vice versa? Something we didn't do in the first iteration is make our rivers public. Nowadays I'm doing that routinely. A few examples are in the left sidebar here on \"Scripting News\". ",
|
||||
"created": "Wed, 21 Jun 2017 10:40:31 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060631"
|
||||
},
|
||||
{
|
||||
"text": "Richard has turned to <a href=\"https://indieweb.org/\">IndieWeb</a> for the latest on open web tech. That's fine, but you have to look elsewhere too, because as he's discovered, they only embrace part of the open web. It's too bad they chose such an inclusive name, but have an exclusive approach. For example, they have avoided \"RSS\", for reasons I'm sure I don't understand (I've listened, so no need to repeat the reasoning). We need all the advantages we can get because there are <a href=\"http://scripting.com/2017/06/18.html#a090658\">serious</a> <a href=\"http://scripting.com/2016/10/03/turnsOutLessFacebookIsOk.html\">headwinds</a> these days for blogging. RSS is serious open web technology. To not build on it is unthinkable, for me at least. ",
|
||||
"created": "Wed, 21 Jun 2017 10:30:33 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060633"
|
||||
},
|
||||
{
|
||||
"text": "Re integration between writing and reading, another topic of interest to Richard, all my rivers <a href=\"http://scripting.com/images/2017/06/21/postingFromARiver.png\">hook</a> into \"Radio3\", which is my latest <a href=\"http://scripting.com/links.html\">linkblogging</a> tool. For reading, I encouraged Richard to look at \"Electric River\", it's the closest to what Radio UserLand did with aggregation in 2002. It runs on your Mac desktop, as the original did. When he wants to go all-in with rivers, nothing can take the place of \"River5\", which is getting both modular and deep. I'm doing more work on that. Rivers have not finished evolving as far as I'm concerned. ",
|
||||
"created": "Wed, 21 Jun 2017 10:32:27 GMT",
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060627"
|
||||
}
|
||||
],
|
||||
"permalink": "http://scripting.com/2017/06/21.html#a060651"
|
||||
}
|
||||
},
|
||||
{
|
||||
"link": "http://scripting.com/2017/06/20.html#a100647",
|
||||
"description": "Anyone want to blog-debate about XML vs JSON? I've spent years using both, I think I have an objective view of the strengths of each. Imho, they are almost the same thing. XML has attributes and values, and that does make it more complex. Slightly. But you don't have to use the extra features. Look at <a href=\"http://dev.opml.org/spec2.html\">OPML</a> for an idea of a simple very JSON-like application of XML. Beyond that, there's really no difference. If you disagree, write a post, link to this and send me the link. I will read what you wrote, and respond, on <a href=\"http://scripting.com/\">my blog</a>, if I have something to say. There's been so much bullshit flying around. I'd like to cut through that. ",
|
||||
"pubDate": "Wed, 21 Jun 2017 02:56:47 GMT",
|
||||
"guid": "http://scripting.com/2017/06/20.html#a100647",
|
||||
"source:outline": {
|
||||
"text": "Anyone want to blog-debate about XML vs JSON? I've spent years using both, I think I have an objective view of the strengths of each. Imho, they are almost the same thing. XML has attributes and values, and that does make it more complex. Slightly. But you don't have to use the extra features. Look at \"OPML\" for an idea of a simple very JSON-like application of XML. Beyond that, there's really no difference. If you disagree, write a post, link to this and send me the link. I will read what you wrote, and respond, on <a href=\"http://scripting.com/\">my blog</a>, if I have something to say. There's been so much bullshit flying around. I'd like to cut through that. ",
|
||||
"created": "Wed, 21 Jun 2017 02:56:47 GMT",
|
||||
"type": "outline",
|
||||
"image": "http://scripting.com/images/2017/06/20/penny.png",
|
||||
"permalink": "http://scripting.com/2017/06/20.html#a100647"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<opml version="1.1">
|
||||
<head>
|
||||
<title>Subs</title>
|
||||
</head>
|
||||
<body>
|
||||
<outline text="Daring Fireball" title="Daring Fireball" description="" type="rss" version="RSS" htmlUrl="http://daringfireball.net/" xmlUrl="http://daringfireball.net/feeds/main"/>
|
||||
<outline text="Swift Blog - Apple Developer" title="Swift Blog - Apple Developer" description="" type="rss" version="RSS" htmlUrl="https://developer.apple.com/swift/blog/" xmlUrl="https://developer.apple.com/swift/blog/news.rss"/>
|
||||
<outline text="Julia Evans" title="Julia Evans" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://jvns.ca/atom.xml"/>
|
||||
<outline text="Erica Sadun" title="Erica Sadun" description="" type="rss" version="RSS" htmlUrl="http://ericasadun.com" xmlUrl="http://ericasadun.com/feed/"/>
|
||||
<outline text="chat & code" title="chat & code" description="" type="rss" version="RSS" htmlUrl="http://corinnekrych.blogspot.com/" xmlUrl="http://corinnekrych.blogspot.com/feeds/posts/default"/>
|
||||
<outline text="never a straight line" title="never a straight line" description="" type="rss" version="RSS" htmlUrl="http://www.blog.juliaferraioli.com/" xmlUrl="http://www.blog.juliaferraioli.com/feeds/posts/default"/>
|
||||
<outline text="jaimeejaimee" title="jaimeejaimee" description="" type="rss" version="RSS" htmlUrl="https://medium.com/@jaimeejaimee?source=rss-11d5cc4494a2------2" xmlUrl="https://medium.com/feed/@jaimeejaimee"/>
|
||||
<outline text="Accidentally in Code" title="Accidentally in Code" description="" type="rss" version="RSS" htmlUrl="https://cate.blog" xmlUrl="http://www.catehuston.com/blog/feed/"/>
|
||||
<outline text="Feral Scrutiny" title="Feral Scrutiny" description="" type="rss" version="RSS" htmlUrl="http://feralscrutiny.co" xmlUrl="http://feralscrutiny.co/feed/"/>
|
||||
<outline text="Doctor Who" title="Doctor Who" description="" type="rss" version="RSS" htmlUrl="http://www.bbc.co.uk/blogs/doctorwho" xmlUrl="http://www.bbc.co.uk/blogs/doctorwho/rss"/>
|
||||
<outline text="jessysaurusrex" title="jessysaurusrex" description="" type="rss" version="RSS" htmlUrl="https://jessysaurusrex.com" xmlUrl="http://jessysaurusrex.com/feed/"/>
|
||||
<outline text="One Foot Tsunami" title="One Foot Tsunami" description="" type="rss" version="RSS" htmlUrl="http://onefoottsunami.com" xmlUrl="http://onefoottsunami.com/feed/atom/"/>
|
||||
<outline text="Loop Insight" title="Loop Insight" description="" type="rss" version="RSS" htmlUrl="http://www.loopinsight.com" xmlUrl="http://www.loopinsight.com/feed/"/>
|
||||
<outline text="The World is not a desktop" title="The World is not a desktop" description="" type="rss" version="RSS" htmlUrl="http://caseorganic.com" xmlUrl="http://caseorganic.com/feed/"/>
|
||||
<outline text="Pointers Gone Wild" title="Pointers Gone Wild" description="" type="rss" version="RSS" htmlUrl="https://pointersgonewild.com" xmlUrl="http://pointersgonewild.com/feed/"/>
|
||||
<outline text="iMore" title="iMore" description="" type="rss" version="RSS" htmlUrl="http://www.imore.com/" xmlUrl="http://www.imore.com/rss.xml"/>
|
||||
<outline text="The Incrementalist." title="The Incrementalist." description="" type="rss" version="RSS" htmlUrl="https://incrementalistblog.wordpress.com" xmlUrl="https://incrementalistblog.wordpress.com/feed/"/>
|
||||
<outline text="Virginia Roberts" title="Virginia Roberts" description="" type="rss" version="RSS" htmlUrl="http://www.virginiaroberts.com" xmlUrl="http://www.virginiaroberts.com/feed/"/>
|
||||
<outline text="Inessential" title="Inessential" description="" type="rss" version="RSS" htmlUrl="http://inessential.com/" xmlUrl="http://inessential.com/xml/rss.xml"/>
|
||||
<outline text="scattered thoughts" title="scattered thoughts" description="" type="rss" version="RSS" htmlUrl="http://blog.nicoleblee.com" xmlUrl="http://blog.nicoleblee.com/feed/"/>
|
||||
<outline text="Rebecca Miller-Webster" title="Rebecca Miller-Webster" description="" type="rss" version="RSS" htmlUrl="http://www.rebeccamiller-webster.com" xmlUrl="http://www.rebeccamiller-webster.com/feed/"/>
|
||||
<outline text="Ellen's Blog" title="Ellen's Blog" description="" type="rss" version="RSS" htmlUrl="https://blog.ellenchisa.com?source=rss----da542b929da2---4" xmlUrl="http://blog.ellenchisa.com/feed/"/>
|
||||
<outline text="Backup Brain" title="Backup Brain" description="" type="rss" version="RSS" htmlUrl="http://www.backupbrain.com" xmlUrl="http://www.backupbrain.com/feed/"/>
|
||||
<outline text="Katie Floyd" title="Katie Floyd" description="" type="rss" version="RSS" htmlUrl="http://www.katiefloyd.com" xmlUrl="http://feed.katiefloyd.com"/>
|
||||
<outline text="The Shape of Everything" title="The Shape of Everything" description="" type="rss" version="RSS" htmlUrl="http://shapeof.com/" xmlUrl="http://shapeof.com/rss.xml"/>
|
||||
<outline text="Sasha Laundy" title="Sasha Laundy" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://blog.sashalaundy.com/atom.xml"/>
|
||||
<outline text="Feed: Thoughtbrain Bloggers" title="Feed: Thoughtbrain Bloggers" description="" type="rss" version="RSS" htmlUrl="http://blog.thoughtbrain.com" xmlUrl="http://blog.thoughtbrain.com/feed/"/>
|
||||
<outline text="MarcySutton.com" title="MarcySutton.com" description="" type="rss" version="RSS" htmlUrl="https://marcysutton.com" xmlUrl="http://marcysutton.com/feed/"/>
|
||||
<outline text="Aleen Mean" title="Aleen Mean" description="" type="rss" version="RSS" htmlUrl="https://aleenmean.com/" xmlUrl="http://www.aleenmean.com/feed.xml"/>
|
||||
<outline text="Michele Titolo's Blog" title="Michele Titolo's Blog" description="" type="rss" version="RSS" htmlUrl="http://michele.io/feed" xmlUrl="http://www.michele.io//feed"/>
|
||||
<outline text="Ashley Nelson-Hornstein" title="Ashley Nelson-Hornstein" description="" type="rss" version="RSS" htmlUrl="http://ashleynh.me:80/" xmlUrl="http://blog.ashleynh.me/rss/"/>
|
||||
<outline text="Veronica Ray on Medium" title="Veronica Ray on Medium" description="" type="rss" version="RSS" htmlUrl="https://medium.com/@nerdonica?source=rss-eaf18ccd367f------2" xmlUrl="https://medium.com/feed/@nerdonica"/>
|
||||
<outline text="Blog - App Camp For Girls" title="Blog - App Camp For Girls" description="" type="rss" version="RSS" htmlUrl="http://appcamp4girls.com/blog/" xmlUrl="http://appcamp4girls.com/blog?format=RSS"/>
|
||||
<outline text="The Future Is Now" title="The Future Is Now" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.mistys-internet.website/blog/atom.xml"/>
|
||||
<outline text="Learn Swift ↯" title="Learn Swift ↯" description="" type="rss" version="RSS" htmlUrl="http://swift.ayaka.me/" xmlUrl="http://swift.ayaka.me/posts?format=RSS"/>
|
||||
<outline text="Everything in Context" title="Everything in Context" description="" type="rss" version="RSS" htmlUrl="http://lambdamaphone.blogspot.com/" xmlUrl="http://lambdamaphone.blogspot.com/feeds/posts/default"/>
|
||||
<outline text="Natasha the Robot" title="Natasha the Robot" description="" type="rss" version="RSS" htmlUrl="https://www.natashatherobot.com" xmlUrl="https://www.natashatherobot.com/feed/"/>
|
||||
<outline text="Katie Floyd" title="Katie Floyd" description="" type="rss" version="RSS" htmlUrl="http://www.katiefloyd.com" xmlUrl="http://feed.katiefloyd.com/"/>
|
||||
<outline text="Meagan Waller" title="Meagan Waller" description="" type="rss" version="RSS" htmlUrl="http://meaganwaller.com" xmlUrl="http://meaganwaller.com/index.php/feed/"/>
|
||||
<outline text="Susan × Blog" title="Susan × Blog" description="" type="rss" version="RSS" htmlUrl="http://sketch.bysusanlin.com/" xmlUrl="http://sketch.bysusanlin.com/rss"/>
|
||||
<outline text="go ahead, mac my day" title="go ahead, mac my day" description="" type="rss" version="RSS" htmlUrl="http://www.nadynerichmond.com/blog" xmlUrl="http://www.nadynerichmond.com/blog/feed/"/>
|
||||
<outline text="don't panic" title="don't panic" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://timekl.com/atom.xml"/>
|
||||
<outline text="MechanicalGirl" title="MechanicalGirl" description="" type="rss" version="RSS" htmlUrl="http://www.MechanicalGirl.com/" xmlUrl="http://www.mechanicalgirl.com/feeds/all/"/>
|
||||
<outline text="BAD YEWEX" title="BAD YEWEX" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://feeds.feedburner.com/BadYewex"/>
|
||||
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" description="" type="rss" version="RSS" htmlUrl="http://beckyhansmeyer.com" xmlUrl="http://beckyhansmeyer.com/feed/"/>
|
||||
<outline text="KateHeddleston.com Blog Posts" title="KateHeddleston.com Blog Posts" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="https://kateheddleston.com/blog/feed.atom"/>
|
||||
<outline text="The Record" title="The Record" description="" type="rss" version="RSS" htmlUrl="http://therecord.co/" xmlUrl="http://therecord.co/xml/rss.xml"/>
|
||||
<outline text="Grok Swift" title="Grok Swift" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="https://grokswift.com/feed/index.xml"/>
|
||||
<outline text="Inspired Mouse" title="Inspired Mouse" description="" type="rss" version="RSS" htmlUrl="https://inspiredmouse.com" xmlUrl="https://inspiredmouse.com/feed/"/>
|
||||
<outline text="Tess Rinearson on Medium" title="Tess Rinearson on Medium" description="" type="rss" version="RSS" htmlUrl="https://medium.com/@tessr?source=rss-c16152863954------2" xmlUrl="https://medium.com/feed/@tessr"/>
|
||||
<outline text="Liz Marley on Medium" title="Liz Marley on Medium" description="" type="rss" version="RSS" htmlUrl="https://medium.com/@emarley?source=rss-b4981c59ffa5------2" xmlUrl="https://medium.com/feed/@emarley"/>
|
||||
<outline text="All The Flow" title="All The Flow" description="" type="rss" version="RSS" htmlUrl="http://blog.alltheflow.com/" xmlUrl="https://blog.alltheflow.com/rss/"/>
|
||||
<outline text="kt zine — Medium" title="kt zine — Medium" description="" type="rss" version="RSS" htmlUrl="https://ktzine.com?source=rss----7097752c9303---4" xmlUrl="https://ktzine.com/feed"/>
|
||||
<outline text="Daring Fireball" title="Daring Fireball" description="" type="rss" version="RSS" htmlUrl="http://daringfireball.net/" xmlUrl="http://daringfireball.net/index.xml"/>
|
||||
<outline text="Blog Posts About Stuff" title="Blog Posts About Stuff" description="" type="rss" version="RSS" htmlUrl="http://nothe.purplellamas.net/index.xml" xmlUrl="http://nothe.purplellamas.net/index.xml"/>
|
||||
<outline text="Six Colors" title="Six Colors" description="" type="rss" version="RSS" htmlUrl="https://www.sixcolors.com/" xmlUrl="http://feedpress.me/sixcolors"/>
|
||||
<outline text="mostgood" title="mostgood" description="" type="rss" version="RSS" htmlUrl="http://www.mostgood.net/" xmlUrl="http://www.mostgood.net/blog?format=RSS"/>
|
||||
<outline text="cocoa by the fire" title="cocoa by the fire" description="" type="rss" version="RSS" htmlUrl="http://blog.cocoabythefire.com/" xmlUrl="http://blog.cocoabythefire.com/rss"/>
|
||||
<outline text="ranchero.com" title="ranchero.com" description="" type="rss" version="RSS" htmlUrl="http://ranchero.com/" xmlUrl="http://ranchero.com/xml/rss.xml"/>
|
||||
<outline text="Linda Dong" title="Linda Dong" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.lindadong.com/blog?format=RSS"/>
|
||||
<outline text="Eryn Wells" title="Eryn Wells" description="" type="rss" version="RSS" htmlUrl="http://blog.erynwells.me/" xmlUrl="http://blog.erynwells.me/rss"/>
|
||||
<outline text="The Red Queen Coder" title="The Red Queen Coder" description="" type="rss" version="RSS" htmlUrl="http://redqueencoder.com" xmlUrl="http://redqueencoder.com/feed/"/>
|
||||
<outline text="nataliepo (posts on 'nataliepo' (rss 2.0))" title="nataliepo (posts on 'nataliepo' (rss 2.0))" description="" type="rss" version="RSS" htmlUrl="http://nataliepo.typepad.com/nataliepo/" xmlUrl="http://nataliepo.typepad.com/nataliepo/rss.xml"/>
|
||||
<outline text="Scripting News" title="Scripting News" description="" type="rss" version="RSS" htmlUrl="http://scripting.com/" xmlUrl="http://scripting.com/rss.xml"/>
|
||||
<outline text="Designated Nerd" title="Designated Nerd" description="" type="rss" version="RSS" htmlUrl="http://designatednerd.com" xmlUrl="http://designatednerd.com/feed/"/>
|
||||
<outline text="kristinathai.com" title="kristinathai.com" description="" type="rss" version="RSS" htmlUrl="https://kristina.io" xmlUrl="http://www.kristinathai.com/feed/"/>
|
||||
<outline text="Natasha The Robot" title="Natasha The Robot" description="" type="rss" version="RSS" htmlUrl="https://www.natashatherobot.com" xmlUrl="http://natashatherobot.com/feed/"/>
|
||||
<outline text="Samantha Marshall's Blog" title="Samantha Marshall's Blog" description="" type="rss" version="RSS" htmlUrl="http://pewpewthespells.com/" xmlUrl="http://pewpewthespells.com/feed.xml"/>
|
||||
<outline text="Ballard" title="Ballard" description="" type="rss" version="RSS" htmlUrl="http://www.myballard.com" xmlUrl="http://www.myballard.com/feed/"/>
|
||||
<outline text="Programming" title="Programming">
|
||||
<outline text="iOS Unit Testing" title="iOS Unit Testing" description="" type="rss" version="RSS" htmlUrl="http://iosunittesting.com" xmlUrl="http://iosunittesting.com/feed/"/>
|
||||
<outline text="A List Apart: The Full Feed" title="A List Apart: The Full Feed" description="" type="rss" version="RSS" htmlUrl="http://alistapart.com" xmlUrl="http://feeds.feedburner.com/alistapart/main"/>
|
||||
<outline text="Swift Programming — Medium" title="Swift Programming — Medium" description="" type="rss" version="RSS" htmlUrl="https://medium.com/swift-programming?source=rss----5396e0e8bc29---4" xmlUrl="https://medium.com/feed/swift-programming"/>
|
||||
<outline text="The Confusatory" title="The Confusatory" description="" type="rss" version="RSS" htmlUrl="http://confusatory.org/" xmlUrl="http://confusatory.org/rss"/>
|
||||
<outline text="Debuggers" title="Debuggers" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://debuggers.co/atom.xml"/>
|
||||
<outline text="We ❤ Swift" title="We ❤ Swift" description="" type="rss" version="RSS" htmlUrl="https://www.weheartswift.com" xmlUrl="http://www.weheartswift.com/feed/"/>
|
||||
<outline text="Airspeed Velocity" title="Airspeed Velocity" description="" type="rss" version="RSS" htmlUrl="https://airspeedvelocity.net" xmlUrl="http://airspeedvelocity.net/feed/"/>
|
||||
<outline text="The blog of Tony Arnold" title="The blog of Tony Arnold" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://tonyarnold.com/atom.xml"/>
|
||||
<outline text="Code by Kevin" title="Code by Kevin" description="" type="rss" version="RSS" htmlUrl="http://www.codebykevin.com/blosxom.cgi" xmlUrl="http://www.codebykevin.com/blosxom.cgi/index.rss"/>
|
||||
<outline text="Programming in the 21st Century" title="Programming in the 21st Century" description="" type="rss" version="RSS" htmlUrl="http://prog21.dadgum.com/" xmlUrl="http://prog21.dadgum.com/atom.xml"/>
|
||||
<outline text="What Amy Did" title="What Amy Did" description="" type="rss" version="RSS" htmlUrl="http://blog.amyworrall.com/" xmlUrl="http://blog.amyworrall.com/rss"/>
|
||||
<outline text="Russ Bishop (atom)" title="Russ Bishop (atom)" description="" type="rss" version="RSS" htmlUrl="http://www.russbishop.net" xmlUrl="http://www.russbishop.net/feed"/>
|
||||
<outline text="The Mental Blog" title="The Mental Blog" description="" type="rss" version="RSS" htmlUrl="http://mentalfaculty.tumblr.com/" xmlUrl="http://mentalfaculty.tumblr.com/rss"/>
|
||||
<outline text="Swift Studies" title="Swift Studies" description="" type="rss" version="RSS" htmlUrl="http://www.swift-studies.com/" xmlUrl="http://www.swift-studies.com/blog?format=RSS"/>
|
||||
<outline text="owensd.io - thoughts in and out - Articles" title="owensd.io - thoughts in and out - Articles" description="" type="rss" version="RSS" htmlUrl="https://owensd.io" xmlUrl="http://owensd.io/rss.xml"/>
|
||||
<outline text="Swift Yeti" title="Swift Yeti" description="" type="rss" version="RSS" htmlUrl="http://swiftyeti.com/" xmlUrl="http://swiftyeti.com/rss/"/>
|
||||
<outline text="Cocoaphony" title="Cocoaphony" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://robnapier.net/atom.xml"/>
|
||||
<outline text="Damien DeVille" title="Damien DeVille" description="" type="rss" version="RSS" htmlUrl="http://ddeville.me" xmlUrl="http://ddeville.me/feed.xml"/>
|
||||
<outline text="Cocoa Manifest" title="Cocoa Manifest" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://cocoamanifest.net/feeds/index.xml"/>
|
||||
<outline text="Indie Stack" title="Indie Stack" description="" type="rss" version="RSS" htmlUrl="http://indiestack.com" xmlUrl="http://indiestack.com/feed/"/>
|
||||
<outline text="[macoscope blog]" title="[macoscope blog]" description="" type="rss" version="RSS" htmlUrl="http://macoscope.com/blog" xmlUrl="http://macoscope.com/blog/feed/"/>
|
||||
<outline text="Ole Begemann: iOS Development" title="Ole Begemann: iOS Development" description="" type="rss" version="RSS" htmlUrl="https://oleb.net/blog/" xmlUrl="http://oleb.net/blog/atom.xml"/>
|
||||
<outline text="David J Peacock - iOS Blog" title="David J Peacock - iOS Blog" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://davidjpeacock.ca/atom.xml"/>
|
||||
<outline text="Ray Wenderlich" title="Ray Wenderlich" description="" type="rss" version="RSS" htmlUrl="https://www.raywenderlich.com" xmlUrl="http://www.raywenderlich.com/feed"/>
|
||||
<outline text="Peter Steinberger" title="Peter Steinberger" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://petersteinberger.com/atom.xml"/>
|
||||
<outline text="Cocoa Is My Girlfriend" title="Cocoa Is My Girlfriend" description="" type="rss" version="RSS" htmlUrl="http://www.cimgf.com" xmlUrl="http://www.cimgf.com/feed/"/>
|
||||
<outline text="Subjective-C" title="Subjective-C" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://subjc.com/atom.xml"/>
|
||||
<outline text="the Joy of Code" title="the Joy of Code" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://feeds.feedburner.com/thejoyofcode/"/>
|
||||
<outline text="New Yankee Codeshop" title="New Yankee Codeshop" description="" type="rss" version="RSS" htmlUrl="http://newyankeecodeshop.tumblr.com/" xmlUrl="http://newyankeecodeshop.tumblr.com/rss"/>
|
||||
<outline text="Borkware Miniblog" title="Borkware Miniblog" description="" type="rss" version="RSS" htmlUrl="https://borkwarellc.wordpress.com" xmlUrl="http://borkware.com/miniblog/rss/rss.xml"/>
|
||||
<outline text="NSHipster" title="NSHipster" description="" type="rss" version="RSS" htmlUrl="http://nshipster.com" xmlUrl="http://nshipster.com/feed.xml"/>
|
||||
<outline text="Pilky.me" title="Pilky.me" description="" type="rss" version="RSS" htmlUrl="http://pilky.me/" xmlUrl="http://feeds.feedburner.com/pilkyme"/>
|
||||
<outline text="Big Nerd Ranch Blog" title="Big Nerd Ranch Blog" description="" type="rss" version="RSS" htmlUrl="https://www.bignerdranch.com/" xmlUrl="http://blog.bignerdranch.com/feed/"/>
|
||||
</outline>
|
||||
<outline text="Macintosh" title="Macintosh">
|
||||
<outline text="Macalope" title="Macalope" description="" type="rss" version="RSS" htmlUrl="http://www.macalope.com" xmlUrl="http://www.macalope.com/feed/"/>
|
||||
<outline text="9to5Mac" title="9to5Mac" description="" type="rss" version="RSS" htmlUrl="https://9to5mac.com" xmlUrl="http://9to5mac.com/feed/"/>
|
||||
<outline text="Macdrifter" title="Macdrifter" description="" type="rss" version="RSS" htmlUrl="http://www.macdrifter.com/" xmlUrl="http://www.macdrifter.com/feeds/all.atom.xml"/>
|
||||
<outline text="TidBITS: Apple News for the Rest of Us" title="TidBITS: Apple News for the Rest of Us" description="" type="rss" version="RSS" htmlUrl="http://tidbits.com/" xmlUrl="http://tidbits.com/feeds/tidbits_blurb.rss"/>
|
||||
<outline text="The Flying Meat Weblog" title="The Flying Meat Weblog" description="" type="rss" version="RSS" htmlUrl="http://flyingmeat.com/blog/" xmlUrl="http://flyingmeat.com/blog/atom.xml"/>
|
||||
</outline>
|
||||
<outline text="Weblogs" title="Weblogs">
|
||||
<outline text="Clark's Tech Blog" title="Clark's Tech Blog" description="" type="rss" version="RSS" htmlUrl="http://www.libertypages.com/clarktech" xmlUrl="http://www.libertypages.com/clarktech/?feed=rss2"/>
|
||||
<outline text="NSBlog" title="NSBlog" description="" type="rss" version="RSS" htmlUrl="http://www.mikeash.com/pyblog/" xmlUrl="http://www.mikeash.com/pyblog/rss.py"/>
|
||||
<outline text="metablog" title="metablog" description="" type="rss" version="RSS" htmlUrl="http://blog.metaobject.com/" xmlUrl="http://blog.metaobject.com/feeds/posts/default"/>
|
||||
<outline text="Better Elevation" title="Better Elevation" description="" type="rss" version="RSS" htmlUrl="http://betterelevation.com" xmlUrl="http://betterelevation.com/feed/"/>
|
||||
<outline text="Allen Pike" title="Allen Pike" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.allenpike.com/feed/"/>
|
||||
<outline text="Secure Mac Programming" title="Secure Mac Programming" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://blog.securemacprogramming.com/feed/"/>
|
||||
<outline text="iPhone Developer News" title="iPhone Developer News" description="" type="rss" version="RSS" htmlUrl="https://developer.apple.com/news/" xmlUrl="https://developer.apple.com/news/rss/news.rss"/>
|
||||
<outline text="David Smith" title="David Smith" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://david-smith.org/atom.xml"/>
|
||||
<outline text="Liss is More" title="Liss is More" description="" type="rss" version="RSS" htmlUrl="https://www.caseyliss.com" xmlUrl="http://www.caseyliss.com/rss"/>
|
||||
<outline text="Codeplease" title="Codeplease" description="" type="rss" version="RSS" htmlUrl="http://codeplease.io/" xmlUrl="http://codeplease.io/rss/"/>
|
||||
<outline text="furbo.org" title="furbo.org" description="" type="rss" version="RSS" htmlUrl="http://furbo.org" xmlUrl="http://furbo.org/feed/"/>
|
||||
<outline text="Very Web. Such Blog. Wow." title="Very Web. Such Blog. Wow." description="" type="rss" version="RSS" htmlUrl="http://brian-webster.tumblr.com/" xmlUrl="http://brian-webster.tumblr.com/rss"/>
|
||||
<outline text="bryan i/o" title="bryan i/o" description="" type="rss" version="RSS" htmlUrl="http://bryan.io/" xmlUrl="http://bryan.io/rss"/>
|
||||
<outline text="Nick Bradbury" title="Nick Bradbury" description="" type="rss" version="RSS" htmlUrl="https://nickbradbury.com" xmlUrl="http://nickbradbury.com/feed/"/>
|
||||
<outline text="Gordon Meyer (posts on 'gordon meyer' (atom))" title="Gordon Meyer (posts on 'gordon meyer' (atom))" description="" type="rss" version="RSS" htmlUrl="http://www.gordonmeyer.com/" xmlUrl="http://www.gordonmeyer.com/atom.xml"/>
|
||||
<outline text="Rhonabwy" title="Rhonabwy" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.rhonabwy.com/wp/feed/"/>
|
||||
<outline text="Typeset In The Future" title="Typeset In The Future" description="" type="rss" version="RSS" htmlUrl="https://typesetinthefuture.com" xmlUrl="http://typesetinthefuture.com/feed/"/>
|
||||
<outline text="Neglected Potential" title="Neglected Potential" description="" type="rss" version="RSS" htmlUrl="http://www.neglectedpotential.com" xmlUrl="http://www.neglectedpotential.com/feed/"/>
|
||||
<outline text="Informal Protocol" title="Informal Protocol" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://informalprotocol.com/atom.xml"/>
|
||||
<outline text="NSHipster" title="NSHipster" description="" type="rss" version="RSS" htmlUrl="http://nshipster.com" xmlUrl="http://feeds.feedburner.com/NSHipster"/>
|
||||
<outline text="Daniel Jalkut" title="Daniel Jalkut" description="" type="rss" version="RSS" htmlUrl="http://bitsplitting.org" xmlUrl="http://bitsplitting.org/feed/"/>
|
||||
<outline text="Ash Furrow" title="Ash Furrow" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://ashfurrow.com/blog?format=rss"/>
|
||||
<outline text="Jared Sinclair" title="Jared Sinclair" description="" type="rss" version="RSS" htmlUrl="http://blog.jaredsinclair.com/" xmlUrl="http://blog.jaredsinclair.com/rss?1"/>
|
||||
<outline text="Doug Russell" title="Doug Russell" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.takingnotes.co/atom.xml"/>
|
||||
<outline text="JakeSavin.com" title="JakeSavin.com" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.jakesavin.com/xml/rss.xml"/>
|
||||
<outline text="Apple Outsider" title="Apple Outsider" description="" type="rss" version="RSS" htmlUrl="http://www.appleoutsider.com" xmlUrl="http://www.appleoutsider.com/feed/"/>
|
||||
<outline text="Nackblog" title="Nackblog" description="" type="rss" version="RSS" htmlUrl="http://jnack.com/blog" xmlUrl="http://jnack.com/blog/?feed=rss2"/>
|
||||
<outline text="ignorethecode.net" title="ignorethecode.net" description="" type="rss" version="RSS" htmlUrl="http://ignorethecode.net" xmlUrl="http://ignorethecode.net/blog/rss/"/>
|
||||
<outline text="Sheila's Weblog" title="Sheila's Weblog" description="" type="rss" version="RSS" htmlUrl="https://sheilasweblog.wordpress.com" xmlUrl="http://sheilasweblog.wordpress.com/feed/"/>
|
||||
<outline text="The Fine Edge" title="The Fine Edge" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://nicemohawk.com/atom.xml"/>
|
||||
<outline text="Rands In Repose" title="Rands In Repose" description="" type="rss" version="RSS" htmlUrl="http://randsinrepose.com" xmlUrl="http://www.randsinrepose.com/index.xml"/>
|
||||
<outline text="John Nack on Adobe (rss (feedburner))" title="John Nack on Adobe (rss (feedburner))" description="" type="rss" version="RSS" htmlUrl="http://blogs.adobe.com/jnack" xmlUrl="http://feeds2.feedburner.com/adobe/jnack"/>
|
||||
<outline text="Dan Gillmor" title="Dan Gillmor" description="" type="rss" version="RSS" htmlUrl="http://dangillmor.com" xmlUrl="http://dangillmor.com/feed/"/>
|
||||
<outline text="Corporation Unknown" title="Corporation Unknown" description="" type="rss" version="RSS" htmlUrl="http://corporationunknown.com/blog" xmlUrl="http://corporationunknown.com/blog/feed/"/>
|
||||
<outline text="Dalton Caldwell" title="Dalton Caldwell" description="" type="rss" version="RSS" htmlUrl="http://daltoncaldwell.com" xmlUrl="http://daltoncaldwell.com/feed"/>
|
||||
<outline text="level of indirection" title="level of indirection" description="" type="rss" version="RSS" htmlUrl="http://www.levelofindirection.com/journal/" xmlUrl="http://www.levelofindirection.com/journal/rss.xml"/>
|
||||
<outline text="I Am Simme" title="I Am Simme" description="" type="rss" version="RSS" htmlUrl="http://iamsim.me" xmlUrl="http://iamsim.me/rss/"/>
|
||||
<outline text="Collin Donnell (collin donnell » feed)" title="Collin Donnell (collin donnell » feed)" description="" type="rss" version="RSS" htmlUrl="http://collindonnell.com" xmlUrl="http://feedpress.me/collindonnell"/>
|
||||
<outline text="Sci-Fi Hi-Fi: Weblog" title="Sci-Fi Hi-Fi: Weblog" description="" type="rss" version="RSS" htmlUrl="http://log.scifihifi.com/" xmlUrl="http://log.scifihifi.com/rss"/>
|
||||
<outline text="Adventures in Newfield" title="Adventures in Newfield" description="" type="rss" version="RSS" htmlUrl="http://adventuresinnewfield.blogspot.com/" xmlUrl="http://adventuresinnewfield.blogspot.com/feeds/posts/default"/>
|
||||
<outline text="The Desolation of Blog" title="The Desolation of Blog" description="" type="rss" version="RSS" htmlUrl="http://lapcatsoftware.com/articles/index.html" xmlUrl="http://lapcatsoftware.com/articles/atom.xml"/>
|
||||
<outline text="Jesper" title="Jesper" description="" type="rss" version="RSS" htmlUrl="http://stmts.net" xmlUrl="http://stmts.net/feed/"/>
|
||||
<outline text="RatHole" title="RatHole" description="" type="rss" version="RSS" htmlUrl="http://rathole.tumblr.com/" xmlUrl="http://rathole.tumblr.com/rss"/>
|
||||
<outline text="Jeff McLeman" title="Jeff McLeman" description="" type="rss" version="RSS" htmlUrl="http://www.jeffmcleman.com/blog" xmlUrl="http://www.jeffmcleman.com/blog/feed/"/>
|
||||
<outline text="frozendevil" title="frozendevil" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://frozendevil.com/atom.xml"/>
|
||||
<outline text="Zathras.de - Uli's most useless blog in the World" title="Zathras.de - Uli's most useless blog in the World" description="" type="rss" version="RSS" htmlUrl="http://orangejuiceliberationfront.com" xmlUrl="http://www.zathras.de/angelweb/BlogRSSFeed.rss"/>
|
||||
<outline text="Monday Note" title="Monday Note" description="" type="rss" version="RSS" htmlUrl="https://mondaynote.com?source=rss----c537d80ed0a---4" xmlUrl="http://www.mondaynote.com/feed/"/>
|
||||
<outline text="Michael Tsai" title="Michael Tsai" description="" type="rss" version="RSS" htmlUrl="http://mjtsai.com/blog" xmlUrl="http://mjtsai.com/blog/feed/"/>
|
||||
<outline text="James Dempsey" title="James Dempsey" description="" type="rss" version="RSS" htmlUrl="http://jamesdempsey.net" xmlUrl="http://jamesdempsey.net/feed/"/>
|
||||
<outline text="Red Sweater" title="Red Sweater" description="" type="rss" version="RSS" htmlUrl="https://red-sweater.com/blog" xmlUrl="http://www.red-sweater.com/blog/feed"/>
|
||||
<outline text="Peter Hosey" title="Peter Hosey" description="" type="rss" version="RSS" htmlUrl="http://boredzo.org/blog" xmlUrl="http://feeds.feedburner.com/domainofthebored"/>
|
||||
<outline text="Use Your Loaf" title="Use Your Loaf" description="" type="rss" version="RSS" htmlUrl="http://useyourloaf.com/blog/" xmlUrl="http://useyourloaf.com/blog/rss.xml"/>
|
||||
<outline text="The Main Thread" title="The Main Thread" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://themainthread.com/feed.xml"/>
|
||||
<outline text="Awkward Hare" title="Awkward Hare" description="" type="rss" version="RSS" htmlUrl="http://awkwardhare.com/" xmlUrl="http://awkwardhare.com/rss"/>
|
||||
<outline text="Journal (atom)" title="Journal (atom)" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.curtclifton.net/journal/atom.xml"/>
|
||||
<outline text="Blog - Jury.me" title="Blog - Jury.me" description="" type="rss" version="RSS" htmlUrl="http://jury.me/blog/" xmlUrl="http://jury.me/blog?format=rss"/>
|
||||
<outline text="Call Me Fishmeal." title="Call Me Fishmeal." description="" type="rss" version="RSS" htmlUrl="http://blog.wilshipley.com/" xmlUrl="http://blog.wilshipley.com/rss.xml"/>
|
||||
<outline text="Hal Mueller's Blog" title="Hal Mueller's Blog" description="" type="rss" version="RSS" htmlUrl="https://halmueller.wordpress.com" xmlUrl="http://halmueller.wordpress.com/feed/"/>
|
||||
<outline text="And now it’s all this" title="And now it’s all this" description="" type="rss" version="RSS" htmlUrl="http://leancrew.com/all-this" xmlUrl="http://www.leancrew.com/all-this/feed/"/>
|
||||
<outline text="OneThirtySeven" title="OneThirtySeven" description="" type="rss" version="RSS" htmlUrl="http://one37.net/blog/" xmlUrl="http://one37.net/blog?format=rss"/>
|
||||
<outline text="rentzsch.tumblr.com" title="rentzsch.tumblr.com" description="" type="rss" version="RSS" htmlUrl="http://rentzsch.tumblr.com/" xmlUrl="http://rentzsch.tumblr.com/rss"/>
|
||||
<outline text="⌥⇧K" title="⌥⇧K" description="" type="rss" version="RSS" htmlUrl="http://optshiftk.com" xmlUrl="http://optshiftk.com/feed/"/>
|
||||
<outline text="Sam Ruby" title="Sam Ruby" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://intertwingly.net/blog/index.atom"/>
|
||||
<outline text="literator.me" title="literator.me" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://literator.me/rss"/>
|
||||
<outline text="upbeat.it" title="upbeat.it" description="" type="rss" version="RSS" htmlUrl="http://www.upbeat.it/" xmlUrl="http://www.upbeat.it/atom.xml"/>
|
||||
<outline text="Minutes to Midnight" title="Minutes to Midnight" description="" type="rss" version="RSS" htmlUrl="http://minutestomidnight.net/" xmlUrl="http://minutestomidnight.net/blog?format=RSS"/>
|
||||
<outline text="John Moltz's Very Nice Web Site" title="John Moltz's Very Nice Web Site" description="" type="rss" version="RSS" htmlUrl="http://verynicewebsite.net" xmlUrl="http://verynicewebsite.net/feed/atom/"/>
|
||||
<outline text="Matt Mullenweg" title="Matt Mullenweg" description="" type="rss" version="RSS" htmlUrl="https://ma.tt" xmlUrl="http://ma.tt/feed/"/>
|
||||
<outline text="BrettTerpstra.com" title="BrettTerpstra.com" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://brettterpstra.com/atom.xml"/>
|
||||
<outline text="jwz" title="jwz" description="" type="rss" version="RSS" htmlUrl="https://www.jwz.org/blog/" xmlUrl="http://www.jwz.org/blog/feed/"/>
|
||||
<outline text="Maniacal Rage" title="Maniacal Rage" description="" type="rss" version="RSS" htmlUrl="http://log.maniacalrage.net/" xmlUrl="http://log.maniacalrage.net/rss"/>
|
||||
<outline text="Shawn Blanc" title="Shawn Blanc" description="" type="rss" version="RSS" htmlUrl="http://shawnblanc.net" xmlUrl="http://shawnblanc.net/feed/"/>
|
||||
<outline text="carpeaqua" title="carpeaqua" description="" type="rss" version="RSS" htmlUrl="https://carpeaqua.com" xmlUrl="http://feeds.feedburner.com/carpeaqua"/>
|
||||
<outline text="Doug Russell" title="Doug Russell" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.getitdownonpaper.com/atom.xml"/>
|
||||
<outline text="bbum's weblog-o-mat" title="bbum's weblog-o-mat" description="" type="rss" version="RSS" htmlUrl="http://www.friday.com/bbum" xmlUrl="http://www.friday.com/bbum/feed/"/>
|
||||
<outline text="Fraser Speirs" title="Fraser Speirs" description="" type="rss" version="RSS" htmlUrl="http://www.speirs.org/" xmlUrl="http://speirs.org/feed/"/>
|
||||
<outline text="ridiculous_fish" title="ridiculous_fish" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://pammon.webfactional.com/blog/feed/"/>
|
||||
<outline text="Hypercritical" title="Hypercritical" description="" type="rss" version="RSS" htmlUrl="http://hypercritical.co/" xmlUrl="http://hypercritical.co/feeds/main"/>
|
||||
<outline text="Manton Reece" title="Manton Reece" description="" type="rss" version="RSS" htmlUrl="http://www.manton.org" xmlUrl="http://manton.org/rss.xml"/>
|
||||
<outline text="Gus's weblog" title="Gus's weblog" description="" type="rss" version="RSS" htmlUrl="http://shapeof.com/" xmlUrl="http://gusmueller.com/blog/atom.xml"/>
|
||||
<outline text="Dustin Curtis" title="Dustin Curtis" description="" type="rss" version="RSS" htmlUrl="https://dcurt.is" xmlUrl="http://feeds.feedburner.com/dcurtis"/>
|
||||
<outline text="Waffle" title="Waffle" description="" type="rss" version="RSS" htmlUrl="http://waffle.wootest.net" xmlUrl="http://waffle.wootest.net/feed/atom/"/>
|
||||
<outline text="Mistitled" title="Mistitled" description="" type="rss" version="RSS" htmlUrl="http://rms2.tumblr.com/" xmlUrl="http://rms2.tumblr.com/rss"/>
|
||||
<outline text="Don Melton" title="Don Melton" description="" type="rss" version="RSS" htmlUrl="https://donmelton.com" xmlUrl="http://donmelton.com/rss.xml"/>
|
||||
<outline text="Anil Dash" title="Anil Dash" description="" type="rss" version="RSS" htmlUrl="http://anildash.com/" xmlUrl="http://feeds.dashes.com/AnilDash"/>
|
||||
<outline text="Neven Mrgan's tumbl" title="Neven Mrgan's tumbl" description="" type="rss" version="RSS" htmlUrl="http://mrgan.tumblr.com/" xmlUrl="http://mrgan.tumblr.com/rss"/>
|
||||
<outline text="alexking.org: Blog" title="alexking.org: Blog" description="" type="rss" version="RSS" htmlUrl="http://alexking.org" xmlUrl="http://alexking.org/blog/feed"/>
|
||||
<outline text="ParisLemon" title="ParisLemon" description="" type="rss" version="RSS" htmlUrl="http://parislemon.com/" xmlUrl="http://parislemon.com/rss"/>
|
||||
<outline text="Black Pixel" title="Black Pixel" description="" type="rss" version="RSS" htmlUrl="https://blackpixel.com/writing/" xmlUrl="http://blackpixel.com/blog/atom.xml"/>
|
||||
<outline text="Marco.org" title="Marco.org" description="" type="rss" version="RSS" htmlUrl="https://marco.org/" xmlUrl="http://www.marco.org/rss"/>
|
||||
<outline text="The Guinea Pig in the Cocoa Mine" title="The Guinea Pig in the Cocoa Mine" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://cocoamine.net/atom.xml"/>
|
||||
<outline text="Blog | Mike Abdullah" title="Blog | Mike Abdullah" description="" type="rss" version="RSS" htmlUrl="http://mikeabdullah.net/" xmlUrl="http://www.mikeabdullah.net/index.xml"/>
|
||||
<outline text="Kickingbear Blog" title="Kickingbear Blog" description="" type="rss" version="RSS" htmlUrl="http://kickingbear.com/blog" xmlUrl="http://kickingbear.com/blog/feed"/>
|
||||
<outline text="Stuart Hall" title="Stuart Hall" description="" type="rss" version="RSS" htmlUrl="http://stuartkhall.com/" xmlUrl="http://feeds.feedburner.com/stuartkhall"/>
|
||||
<outline text="ongoing" title="ongoing" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="http://www.tbray.org/ongoing/ongoing.atom"/>
|
||||
<outline text="Stratēchery" title="Stratēchery" description="" type="rss" version="RSS" htmlUrl="https://stratechery.com" xmlUrl="http://stratechery.com/feed/"/>
|
||||
<outline text="Mark Bernstein" title="Mark Bernstein" description="" type="rss" version="RSS" htmlUrl="http://markbernstein.org/" xmlUrl="http://www.markbernstein.org/news.rss"/>
|
||||
</outline>
|
||||
<outline text="Writers" title="Writers">
|
||||
<outline text="Whatever" title="Whatever" description="" type="rss" version="RSS" htmlUrl="http://whatever.scalzi.com" xmlUrl="http://whatever.scalzi.com/feed/"/>
|
||||
<outline text="Charlie's Diary" title="Charlie's Diary" description="" type="rss" version="RSS" htmlUrl="http://www.antipope.org/charlie/blog-static/" xmlUrl="http://www.antipope.org/charlie/blog-static/atom.xml"/>
|
||||
<outline text="Gerrold" title="Gerrold" description="" type="rss" version="RSS" htmlUrl="http://www.gerrold.com" xmlUrl="http://www.gerrold.com/feed/"/>
|
||||
</outline>
|
||||
</body>
|
||||
</opml>
|
|
@ -1,237 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<opml version="2.0">
|
||||
<head><!-- <editor>
|
||||
<sidebar/>
|
||||
<column name="text" width="150"/>
|
||||
<column name="description" width="150"/>
|
||||
<column name="type" width="150"/>
|
||||
<column name="version" width="150"/>
|
||||
<column name="htmlUrl" width="150"/>
|
||||
<column name="xmlUrl" width="150"/>
|
||||
</editor> -->
|
||||
<title>Subs</title>
|
||||
<expansionState></expansionState>
|
||||
<vertScrollState>0</vertScrollState>
|
||||
<windowTop>472</windowTop>
|
||||
<windowLeft>502</windowLeft>
|
||||
<windowRight>1182</windowRight>
|
||||
<windowBottom>1252</windowBottom>
|
||||
</head>
|
||||
<body>
|
||||
<outline text="Daring Fireball" type="rss" version="RSS" htmlUrl="http://daringfireball.net/" xmlUrl="http://daringfireball.net/feeds/main"/>
|
||||
<outline text="Swift Blog - Apple Developer" type="rss" version="RSS" htmlUrl="https://developer.apple.com/swift/blog/" xmlUrl="https://developer.apple.com/swift/blog/news.rss"/>
|
||||
<outline text="Julia Evans" type="rss" version="RSS" xmlUrl="http://jvns.ca/atom.xml"/>
|
||||
<outline text="Erica Sadun" type="rss" version="RSS" htmlUrl="http://ericasadun.com" xmlUrl="http://ericasadun.com/feed/"/>
|
||||
<outline text="chat & code" type="rss" version="RSS" htmlUrl="http://corinnekrych.blogspot.com/" xmlUrl="http://corinnekrych.blogspot.com/feeds/posts/default"/>
|
||||
<outline text="never a straight line" type="rss" version="RSS" htmlUrl="http://www.blog.juliaferraioli.com/" xmlUrl="http://www.blog.juliaferraioli.com/feeds/posts/default"/>
|
||||
<outline text="jaimeejaimee" type="rss" version="RSS" htmlUrl="https://medium.com/@jaimeejaimee?source=rss-11d5cc4494a2------2" xmlUrl="https://medium.com/feed/@jaimeejaimee"/>
|
||||
<outline text="Accidentally in Code" type="rss" version="RSS" htmlUrl="https://cate.blog" xmlUrl="http://www.catehuston.com/blog/feed/"/>
|
||||
<outline text="Feral Scrutiny" type="rss" version="RSS" htmlUrl="http://feralscrutiny.co" xmlUrl="http://feralscrutiny.co/feed/"/>
|
||||
<outline text="Doctor Who" type="rss" version="RSS" htmlUrl="http://www.bbc.co.uk/blogs/doctorwho" xmlUrl="http://www.bbc.co.uk/blogs/doctorwho/rss"/>
|
||||
<outline text="jessysaurusrex" type="rss" version="RSS" htmlUrl="https://jessysaurusrex.com" xmlUrl="http://jessysaurusrex.com/feed/"/>
|
||||
<outline text="One Foot Tsunami" type="rss" version="RSS" htmlUrl="http://onefoottsunami.com" xmlUrl="http://onefoottsunami.com/feed/atom/"/>
|
||||
<outline text="Loop Insight" type="rss" version="RSS" htmlUrl="http://www.loopinsight.com" xmlUrl="http://www.loopinsight.com/feed/"/>
|
||||
<outline text="The World is not a desktop" type="rss" version="RSS" htmlUrl="http://caseorganic.com" xmlUrl="http://caseorganic.com/feed/"/>
|
||||
<outline text="Pointers Gone Wild" type="rss" version="RSS" htmlUrl="https://pointersgonewild.com" xmlUrl="http://pointersgonewild.com/feed/"/>
|
||||
<outline text="iMore" type="rss" version="RSS" htmlUrl="http://www.imore.com/" xmlUrl="http://www.imore.com/rss.xml"/>
|
||||
<outline text="The Incrementalist." type="rss" version="RSS" htmlUrl="https://incrementalistblog.wordpress.com" xmlUrl="https://incrementalistblog.wordpress.com/feed/"/>
|
||||
<outline text="Virginia Roberts" type="rss" version="RSS" htmlUrl="http://www.virginiaroberts.com" xmlUrl="http://www.virginiaroberts.com/feed/"/>
|
||||
<outline text="Inessential" type="rss" version="RSS" htmlUrl="http://inessential.com/" xmlUrl="http://inessential.com/xml/rss.xml"/>
|
||||
<outline text="scattered thoughts" type="rss" version="RSS" htmlUrl="http://blog.nicoleblee.com" xmlUrl="http://blog.nicoleblee.com/feed/"/>
|
||||
<outline text="Rebecca Miller-Webster" type="rss" version="RSS" htmlUrl="http://www.rebeccamiller-webster.com" xmlUrl="http://www.rebeccamiller-webster.com/feed/"/>
|
||||
<outline text="Ellen's Blog" type="rss" version="RSS" htmlUrl="https://blog.ellenchisa.com?source=rss----da542b929da2---4" xmlUrl="http://blog.ellenchisa.com/feed/"/>
|
||||
<outline text="Backup Brain" type="rss" version="RSS" htmlUrl="http://www.backupbrain.com" xmlUrl="http://www.backupbrain.com/feed/"/>
|
||||
<outline text="Katie Floyd" type="rss" version="RSS" htmlUrl="http://www.katiefloyd.com" xmlUrl="http://feed.katiefloyd.com"/>
|
||||
<outline text="The Shape of Everything" type="rss" version="RSS" htmlUrl="http://shapeof.com/" xmlUrl="http://shapeof.com/rss.xml"/>
|
||||
<outline text="Sasha Laundy" type="rss" version="RSS" xmlUrl="http://blog.sashalaundy.com/atom.xml"/>
|
||||
<outline text="Feed: Thoughtbrain Bloggers" type="rss" version="RSS" htmlUrl="http://blog.thoughtbrain.com" xmlUrl="http://blog.thoughtbrain.com/feed/"/>
|
||||
<outline text="MarcySutton.com" type="rss" version="RSS" htmlUrl="https://marcysutton.com" xmlUrl="http://marcysutton.com/feed/"/>
|
||||
<outline text="Aleen Mean" type="rss" version="RSS" htmlUrl="https://aleenmean.com/" xmlUrl="http://www.aleenmean.com/feed.xml"/>
|
||||
<outline text="Michele Titolo's Blog" type="rss" version="RSS" htmlUrl="http://michele.io/feed" xmlUrl="http://www.michele.io//feed"/>
|
||||
<outline text="Ashley Nelson-Hornstein" type="rss" version="RSS" htmlUrl="http://ashleynh.me:80/" xmlUrl="http://blog.ashleynh.me/rss/"/>
|
||||
<outline text="Veronica Ray on Medium" type="rss" version="RSS" htmlUrl="https://medium.com/@nerdonica?source=rss-eaf18ccd367f------2" xmlUrl="https://medium.com/feed/@nerdonica"/>
|
||||
<outline text="Blog - App Camp For Girls" type="rss" version="RSS" htmlUrl="http://appcamp4girls.com/blog/" xmlUrl="http://appcamp4girls.com/blog?format=RSS"/>
|
||||
<outline text="The Future Is Now" type="rss" version="RSS" xmlUrl="http://www.mistys-internet.website/blog/atom.xml"/>
|
||||
<outline text="Learn Swift ↯" type="rss" version="RSS" htmlUrl="http://swift.ayaka.me/" xmlUrl="http://swift.ayaka.me/posts?format=RSS"/>
|
||||
<outline text="Everything in Context" type="rss" version="RSS" htmlUrl="http://lambdamaphone.blogspot.com/" xmlUrl="http://lambdamaphone.blogspot.com/feeds/posts/default"/>
|
||||
<outline text="Natasha the Robot" type="rss" version="RSS" htmlUrl="https://www.natashatherobot.com" xmlUrl="https://www.natashatherobot.com/feed/"/>
|
||||
<outline text="Katie Floyd" type="rss" version="RSS" htmlUrl="http://www.katiefloyd.com" xmlUrl="http://feed.katiefloyd.com/"/>
|
||||
<outline text="Meagan Waller" type="rss" version="RSS" htmlUrl="http://meaganwaller.com" xmlUrl="http://meaganwaller.com/index.php/feed/"/>
|
||||
<outline text="Susan × Blog" type="rss" version="RSS" htmlUrl="http://sketch.bysusanlin.com/" xmlUrl="http://sketch.bysusanlin.com/rss"/>
|
||||
<outline text="go ahead, mac my day" type="rss" version="RSS" htmlUrl="http://www.nadynerichmond.com/blog" xmlUrl="http://www.nadynerichmond.com/blog/feed/"/>
|
||||
<outline text="don't panic" type="rss" version="RSS" xmlUrl="http://timekl.com/atom.xml"/>
|
||||
<outline text="MechanicalGirl" type="rss" version="RSS" htmlUrl="http://www.MechanicalGirl.com/" xmlUrl="http://www.mechanicalgirl.com/feeds/all/"/>
|
||||
<outline text="BAD YEWEX" type="rss" version="RSS" xmlUrl="http://feeds.feedburner.com/BadYewex"/>
|
||||
<outline text="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="http://beckyhansmeyer.com" xmlUrl="http://beckyhansmeyer.com/feed/"/>
|
||||
<outline text="KateHeddleston.com Blog Posts" type="rss" version="RSS" xmlUrl="https://kateheddleston.com/blog/feed.atom"/>
|
||||
<outline text="The Record" type="rss" version="RSS" htmlUrl="http://therecord.co/" xmlUrl="http://therecord.co/xml/rss.xml"/>
|
||||
<outline text="Grok Swift" type="rss" version="RSS" xmlUrl="https://grokswift.com/feed/index.xml"/>
|
||||
<outline text="Inspired Mouse" type="rss" version="RSS" htmlUrl="https://inspiredmouse.com" xmlUrl="https://inspiredmouse.com/feed/"/>
|
||||
<outline text="Tess Rinearson on Medium" type="rss" version="RSS" htmlUrl="https://medium.com/@tessr?source=rss-c16152863954------2" xmlUrl="https://medium.com/feed/@tessr"/>
|
||||
<outline text="Liz Marley on Medium" type="rss" version="RSS" htmlUrl="https://medium.com/@emarley?source=rss-b4981c59ffa5------2" xmlUrl="https://medium.com/feed/@emarley"/>
|
||||
<outline text="All The Flow" type="rss" version="RSS" htmlUrl="http://blog.alltheflow.com/" xmlUrl="https://blog.alltheflow.com/rss/"/>
|
||||
<outline text="kt zine — Medium" type="rss" version="RSS" htmlUrl="https://ktzine.com?source=rss----7097752c9303---4" xmlUrl="https://ktzine.com/feed"/>
|
||||
<outline text="Daring Fireball" type="rss" version="RSS" htmlUrl="http://daringfireball.net/" xmlUrl="http://daringfireball.net/index.xml"/>
|
||||
<outline text="Blog Posts About Stuff" type="rss" version="RSS" htmlUrl="http://nothe.purplellamas.net/index.xml" xmlUrl="http://nothe.purplellamas.net/index.xml"/>
|
||||
<outline text="Six Colors" type="rss" version="RSS" htmlUrl="https://www.sixcolors.com/" xmlUrl="http://feedpress.me/sixcolors"/>
|
||||
<outline text="mostgood" type="rss" version="RSS" htmlUrl="http://www.mostgood.net/" xmlUrl="http://www.mostgood.net/blog?format=RSS"/>
|
||||
<outline text="cocoa by the fire" type="rss" version="RSS" htmlUrl="http://blog.cocoabythefire.com/" xmlUrl="http://blog.cocoabythefire.com/rss"/>
|
||||
<outline text="ranchero.com" type="rss" version="RSS" htmlUrl="http://ranchero.com/" xmlUrl="http://ranchero.com/xml/rss.xml"/>
|
||||
<outline text="Linda Dong" type="rss" version="RSS" xmlUrl="http://www.lindadong.com/blog?format=RSS"/>
|
||||
<outline text="Eryn Wells" type="rss" version="RSS" htmlUrl="http://blog.erynwells.me/" xmlUrl="http://blog.erynwells.me/rss"/>
|
||||
<outline text="The Red Queen Coder" type="rss" version="RSS" htmlUrl="http://redqueencoder.com" xmlUrl="http://redqueencoder.com/feed/"/>
|
||||
<outline text="nataliepo (posts on 'nataliepo' (rss 2.0))" type="rss" version="RSS" htmlUrl="http://nataliepo.typepad.com/nataliepo/" xmlUrl="http://nataliepo.typepad.com/nataliepo/rss.xml"/>
|
||||
<outline text="Scripting News" type="rss" version="RSS" htmlUrl="http://scripting.com/" xmlUrl="http://scripting.com/rss.xml"/>
|
||||
<outline text="Designated Nerd" type="rss" version="RSS" htmlUrl="http://designatednerd.com" xmlUrl="http://designatednerd.com/feed/"/>
|
||||
<outline text="kristinathai.com" type="rss" version="RSS" htmlUrl="https://kristina.io" xmlUrl="http://www.kristinathai.com/feed/"/>
|
||||
<outline text="Natasha The Robot" type="rss" version="RSS" htmlUrl="https://www.natashatherobot.com" xmlUrl="http://natashatherobot.com/feed/"/>
|
||||
<outline text="Samantha Marshall's Blog" type="rss" version="RSS" htmlUrl="http://pewpewthespells.com/" xmlUrl="http://pewpewthespells.com/feed.xml"/>
|
||||
<outline text="Ballard" type="rss" version="RSS" htmlUrl="http://www.myballard.com" xmlUrl="http://www.myballard.com/feed/"/>
|
||||
<outline text="Programming">
|
||||
<outline text="iOS Unit Testing" type="rss" version="RSS" htmlUrl="http://iosunittesting.com" xmlUrl="http://iosunittesting.com/feed/"/>
|
||||
<outline text="A List Apart: The Full Feed" type="rss" version="RSS" htmlUrl="http://alistapart.com" xmlUrl="http://feeds.feedburner.com/alistapart/main"/>
|
||||
<outline text="Swift Programming — Medium" type="rss" version="RSS" htmlUrl="https://medium.com/swift-programming?source=rss----5396e0e8bc29---4" xmlUrl="https://medium.com/feed/swift-programming"/>
|
||||
<outline text="The Confusatory" type="rss" version="RSS" htmlUrl="http://confusatory.org/" xmlUrl="http://confusatory.org/rss"/>
|
||||
<outline text="Debuggers" type="rss" version="RSS" xmlUrl="http://debuggers.co/atom.xml"/>
|
||||
<outline text="We ❤ Swift" type="rss" version="RSS" htmlUrl="https://www.weheartswift.com" xmlUrl="http://www.weheartswift.com/feed/"/>
|
||||
<outline text="Airspeed Velocity" type="rss" version="RSS" htmlUrl="https://airspeedvelocity.net" xmlUrl="http://airspeedvelocity.net/feed/"/>
|
||||
<outline text="The blog of Tony Arnold" type="rss" version="RSS" xmlUrl="http://tonyarnold.com/atom.xml"/>
|
||||
<outline text="Code by Kevin" type="rss" version="RSS" htmlUrl="http://www.codebykevin.com/blosxom.cgi" xmlUrl="http://www.codebykevin.com/blosxom.cgi/index.rss"/>
|
||||
<outline text="Programming in the 21st Century" type="rss" version="RSS" htmlUrl="http://prog21.dadgum.com/" xmlUrl="http://prog21.dadgum.com/atom.xml"/>
|
||||
<outline text="What Amy Did" type="rss" version="RSS" htmlUrl="http://blog.amyworrall.com/" xmlUrl="http://blog.amyworrall.com/rss"/>
|
||||
<outline text="Russ Bishop (atom)" type="rss" version="RSS" htmlUrl="http://www.russbishop.net" xmlUrl="http://www.russbishop.net/feed"/>
|
||||
<outline text="The Mental Blog" type="rss" version="RSS" htmlUrl="http://mentalfaculty.tumblr.com/" xmlUrl="http://mentalfaculty.tumblr.com/rss"/>
|
||||
<outline text="Swift Studies" type="rss" version="RSS" htmlUrl="http://www.swift-studies.com/" xmlUrl="http://www.swift-studies.com/blog?format=RSS"/>
|
||||
<outline text="owensd.io - thoughts in and out - Articles" type="rss" version="RSS" htmlUrl="https://owensd.io" xmlUrl="http://owensd.io/rss.xml"/>
|
||||
<outline text="Swift Yeti" type="rss" version="RSS" htmlUrl="http://swiftyeti.com/" xmlUrl="http://swiftyeti.com/rss/"/>
|
||||
<outline text="Cocoaphony" type="rss" version="RSS" xmlUrl="http://robnapier.net/atom.xml"/>
|
||||
<outline text="Damien DeVille" type="rss" version="RSS" htmlUrl="http://ddeville.me" xmlUrl="http://ddeville.me/feed.xml"/>
|
||||
<outline text="Cocoa Manifest" type="rss" version="RSS" xmlUrl="http://cocoamanifest.net/feeds/index.xml"/>
|
||||
<outline text="Indie Stack" type="rss" version="RSS" htmlUrl="http://indiestack.com" xmlUrl="http://indiestack.com/feed/"/>
|
||||
<outline text="[macoscope blog]" type="rss" version="RSS" htmlUrl="http://macoscope.com/blog" xmlUrl="http://macoscope.com/blog/feed/"/>
|
||||
<outline text="Ole Begemann: iOS Development" type="rss" version="RSS" htmlUrl="https://oleb.net/blog/" xmlUrl="http://oleb.net/blog/atom.xml"/>
|
||||
<outline text="David J Peacock - iOS Blog" type="rss" version="RSS" xmlUrl="http://davidjpeacock.ca/atom.xml"/>
|
||||
<outline text="Ray Wenderlich" type="rss" version="RSS" htmlUrl="https://www.raywenderlich.com" xmlUrl="http://www.raywenderlich.com/feed"/>
|
||||
<outline text="Peter Steinberger" type="rss" version="RSS" xmlUrl="http://petersteinberger.com/atom.xml"/>
|
||||
<outline text="Cocoa Is My Girlfriend" type="rss" version="RSS" htmlUrl="http://www.cimgf.com" xmlUrl="http://www.cimgf.com/feed/"/>
|
||||
<outline text="Subjective-C" type="rss" version="RSS" xmlUrl="http://subjc.com/atom.xml"/>
|
||||
<outline text="the Joy of Code" type="rss" version="RSS" xmlUrl="http://feeds.feedburner.com/thejoyofcode/"/>
|
||||
<outline text="New Yankee Codeshop" type="rss" version="RSS" htmlUrl="http://newyankeecodeshop.tumblr.com/" xmlUrl="http://newyankeecodeshop.tumblr.com/rss"/>
|
||||
<outline text="Borkware Miniblog" type="rss" version="RSS" htmlUrl="https://borkwarellc.wordpress.com" xmlUrl="http://borkware.com/miniblog/rss/rss.xml"/>
|
||||
<outline text="NSHipster" type="rss" version="RSS" htmlUrl="http://nshipster.com" xmlUrl="http://nshipster.com/feed.xml"/>
|
||||
<outline text="Pilky.me" type="rss" version="RSS" htmlUrl="http://pilky.me/" xmlUrl="http://feeds.feedburner.com/pilkyme"/>
|
||||
<outline text="Big Nerd Ranch Blog" type="rss" version="RSS" htmlUrl="https://www.bignerdranch.com/" xmlUrl="http://blog.bignerdranch.com/feed/"/>
|
||||
</outline>
|
||||
<outline text="Macintosh">
|
||||
<outline text="Macalope" type="rss" version="RSS" htmlUrl="http://www.macalope.com" xmlUrl="http://www.macalope.com/feed/"/>
|
||||
<outline text="9to5Mac" type="rss" version="RSS" htmlUrl="https://9to5mac.com" xmlUrl="http://9to5mac.com/feed/"/>
|
||||
<outline text="Macdrifter" type="rss" version="RSS" htmlUrl="http://www.macdrifter.com/" xmlUrl="http://www.macdrifter.com/feeds/all.atom.xml"/>
|
||||
<outline text="TidBITS: Apple News for the Rest of Us" type="rss" version="RSS" htmlUrl="http://tidbits.com/" xmlUrl="http://tidbits.com/feeds/tidbits_blurb.rss"/>
|
||||
<outline text="The Flying Meat Weblog" type="rss" version="RSS" htmlUrl="http://flyingmeat.com/blog/" xmlUrl="http://flyingmeat.com/blog/atom.xml"/>
|
||||
</outline>
|
||||
<outline text="Weblogs">
|
||||
<outline text="Clark's Tech Blog" type="rss" version="RSS" htmlUrl="http://www.libertypages.com/clarktech" xmlUrl="http://www.libertypages.com/clarktech/?feed=rss2"/>
|
||||
<outline text="NSBlog" type="rss" version="RSS" htmlUrl="http://www.mikeash.com/pyblog/" xmlUrl="http://www.mikeash.com/pyblog/rss.py"/>
|
||||
<outline text="metablog" type="rss" version="RSS" htmlUrl="http://blog.metaobject.com/" xmlUrl="http://blog.metaobject.com/feeds/posts/default"/>
|
||||
<outline text="Better Elevation" type="rss" version="RSS" htmlUrl="http://betterelevation.com" xmlUrl="http://betterelevation.com/feed/"/>
|
||||
<outline text="Allen Pike" type="rss" version="RSS" xmlUrl="http://www.allenpike.com/feed/"/>
|
||||
<outline text="Secure Mac Programming" type="rss" version="RSS" xmlUrl="http://blog.securemacprogramming.com/feed/"/>
|
||||
<outline text="iPhone Developer News" type="rss" version="RSS" htmlUrl="https://developer.apple.com/news/" xmlUrl="https://developer.apple.com/news/rss/news.rss"/>
|
||||
<outline text="David Smith" type="rss" version="RSS" xmlUrl="http://david-smith.org/atom.xml"/>
|
||||
<outline text="Liss is More" type="rss" version="RSS" htmlUrl="https://www.caseyliss.com" xmlUrl="http://www.caseyliss.com/rss"/>
|
||||
<outline text="Codeplease" type="rss" version="RSS" htmlUrl="http://codeplease.io/" xmlUrl="http://codeplease.io/rss/"/>
|
||||
<outline text="furbo.org" type="rss" version="RSS" htmlUrl="http://furbo.org" xmlUrl="http://furbo.org/feed/"/>
|
||||
<outline text="Very Web. Such Blog. Wow." type="rss" version="RSS" htmlUrl="http://brian-webster.tumblr.com/" xmlUrl="http://brian-webster.tumblr.com/rss"/>
|
||||
<outline text="bryan i/o" type="rss" version="RSS" htmlUrl="http://bryan.io/" xmlUrl="http://bryan.io/rss"/>
|
||||
<outline text="Nick Bradbury" type="rss" version="RSS" htmlUrl="https://nickbradbury.com" xmlUrl="http://nickbradbury.com/feed/"/>
|
||||
<outline text="Gordon Meyer (posts on 'gordon meyer' (atom))" type="rss" version="RSS" htmlUrl="http://www.gordonmeyer.com/" xmlUrl="http://www.gordonmeyer.com/atom.xml"/>
|
||||
<outline text="Rhonabwy" type="rss" version="RSS" xmlUrl="http://www.rhonabwy.com/wp/feed/"/>
|
||||
<outline text="Typeset In The Future" type="rss" version="RSS" htmlUrl="https://typesetinthefuture.com" xmlUrl="http://typesetinthefuture.com/feed/"/>
|
||||
<outline text="Neglected Potential" type="rss" version="RSS" htmlUrl="http://www.neglectedpotential.com" xmlUrl="http://www.neglectedpotential.com/feed/"/>
|
||||
<outline text="Informal Protocol" type="rss" version="RSS" xmlUrl="http://informalprotocol.com/atom.xml"/>
|
||||
<outline text="NSHipster" type="rss" version="RSS" htmlUrl="http://nshipster.com" xmlUrl="http://feeds.feedburner.com/NSHipster"/>
|
||||
<outline text="Daniel Jalkut" type="rss" version="RSS" htmlUrl="http://bitsplitting.org" xmlUrl="http://bitsplitting.org/feed/"/>
|
||||
<outline text="Ash Furrow" type="rss" version="RSS" xmlUrl="http://ashfurrow.com/blog?format=rss"/>
|
||||
<outline text="Jared Sinclair" type="rss" version="RSS" htmlUrl="http://blog.jaredsinclair.com/" xmlUrl="http://blog.jaredsinclair.com/rss?1"/>
|
||||
<outline text="Doug Russell" type="rss" version="RSS" xmlUrl="http://www.takingnotes.co/atom.xml"/>
|
||||
<outline text="JakeSavin.com" type="rss" version="RSS" xmlUrl="http://www.jakesavin.com/xml/rss.xml"/>
|
||||
<outline text="Apple Outsider" type="rss" version="RSS" htmlUrl="http://www.appleoutsider.com" xmlUrl="http://www.appleoutsider.com/feed/"/>
|
||||
<outline text="Nackblog" type="rss" version="RSS" htmlUrl="http://jnack.com/blog" xmlUrl="http://jnack.com/blog/?feed=rss2"/>
|
||||
<outline text="ignorethecode.net" type="rss" version="RSS" htmlUrl="http://ignorethecode.net" xmlUrl="http://ignorethecode.net/blog/rss/"/>
|
||||
<outline text="Sheila's Weblog" type="rss" version="RSS" htmlUrl="https://sheilasweblog.wordpress.com" xmlUrl="http://sheilasweblog.wordpress.com/feed/"/>
|
||||
<outline text="The Fine Edge" type="rss" version="RSS" xmlUrl="http://nicemohawk.com/atom.xml"/>
|
||||
<outline text="Rands In Repose" type="rss" version="RSS" htmlUrl="http://randsinrepose.com" xmlUrl="http://www.randsinrepose.com/index.xml"/>
|
||||
<outline text="John Nack on Adobe (rss (feedburner))" type="rss" version="RSS" htmlUrl="http://blogs.adobe.com/jnack" xmlUrl="http://feeds2.feedburner.com/adobe/jnack"/>
|
||||
<outline text="Dan Gillmor" type="rss" version="RSS" htmlUrl="http://dangillmor.com" xmlUrl="http://dangillmor.com/feed/"/>
|
||||
<outline text="Corporation Unknown" type="rss" version="RSS" htmlUrl="http://corporationunknown.com/blog" xmlUrl="http://corporationunknown.com/blog/feed/"/>
|
||||
<outline text="Dalton Caldwell" type="rss" version="RSS" htmlUrl="http://daltoncaldwell.com" xmlUrl="http://daltoncaldwell.com/feed"/>
|
||||
<outline text="level of indirection" type="rss" version="RSS" htmlUrl="http://www.levelofindirection.com/journal/" xmlUrl="http://www.levelofindirection.com/journal/rss.xml"/>
|
||||
<outline text="I Am Simme" type="rss" version="RSS" htmlUrl="http://iamsim.me" xmlUrl="http://iamsim.me/rss/"/>
|
||||
<outline text="Collin Donnell (collin donnell » feed)" type="rss" version="RSS" htmlUrl="http://collindonnell.com" xmlUrl="http://feedpress.me/collindonnell"/>
|
||||
<outline text="Sci-Fi Hi-Fi: Weblog" type="rss" version="RSS" htmlUrl="http://log.scifihifi.com/" xmlUrl="http://log.scifihifi.com/rss"/>
|
||||
<outline text="Adventures in Newfield" type="rss" version="RSS" htmlUrl="http://adventuresinnewfield.blogspot.com/" xmlUrl="http://adventuresinnewfield.blogspot.com/feeds/posts/default"/>
|
||||
<outline text="The Desolation of Blog" type="rss" version="RSS" htmlUrl="http://lapcatsoftware.com/articles/index.html" xmlUrl="http://lapcatsoftware.com/articles/atom.xml"/>
|
||||
<outline text="Jesper" type="rss" version="RSS" htmlUrl="http://stmts.net" xmlUrl="http://stmts.net/feed/"/>
|
||||
<outline text="RatHole" type="rss" version="RSS" htmlUrl="http://rathole.tumblr.com/" xmlUrl="http://rathole.tumblr.com/rss"/>
|
||||
<outline text="Jeff McLeman" type="rss" version="RSS" htmlUrl="http://www.jeffmcleman.com/blog" xmlUrl="http://www.jeffmcleman.com/blog/feed/"/>
|
||||
<outline text="frozendevil" type="rss" version="RSS" xmlUrl="http://frozendevil.com/atom.xml"/>
|
||||
<outline text="Zathras.de - Uli's most useless blog in the World" type="rss" version="RSS" htmlUrl="http://orangejuiceliberationfront.com" xmlUrl="http://www.zathras.de/angelweb/BlogRSSFeed.rss"/>
|
||||
<outline text="Monday Note" type="rss" version="RSS" htmlUrl="https://mondaynote.com?source=rss----c537d80ed0a---4" xmlUrl="http://www.mondaynote.com/feed/"/>
|
||||
<outline text="Michael Tsai" type="rss" version="RSS" htmlUrl="http://mjtsai.com/blog" xmlUrl="http://mjtsai.com/blog/feed/"/>
|
||||
<outline text="James Dempsey" type="rss" version="RSS" htmlUrl="http://jamesdempsey.net" xmlUrl="http://jamesdempsey.net/feed/"/>
|
||||
<outline text="Red Sweater" type="rss" version="RSS" htmlUrl="https://red-sweater.com/blog" xmlUrl="http://www.red-sweater.com/blog/feed"/>
|
||||
<outline text="Peter Hosey" type="rss" version="RSS" htmlUrl="http://boredzo.org/blog" xmlUrl="http://feeds.feedburner.com/domainofthebored"/>
|
||||
<outline text="Use Your Loaf" type="rss" version="RSS" htmlUrl="http://useyourloaf.com/blog/" xmlUrl="http://useyourloaf.com/blog/rss.xml"/>
|
||||
<outline text="The Main Thread" type="rss" version="RSS" xmlUrl="http://themainthread.com/feed.xml"/>
|
||||
<outline text="Awkward Hare" type="rss" version="RSS" htmlUrl="http://awkwardhare.com/" xmlUrl="http://awkwardhare.com/rss"/>
|
||||
<outline text="Journal (atom)" type="rss" version="RSS" xmlUrl="http://www.curtclifton.net/journal/atom.xml"/>
|
||||
<outline text="Blog - Jury.me" type="rss" version="RSS" htmlUrl="http://jury.me/blog/" xmlUrl="http://jury.me/blog?format=rss"/>
|
||||
<outline text="Call Me Fishmeal." type="rss" version="RSS" htmlUrl="http://blog.wilshipley.com/" xmlUrl="http://blog.wilshipley.com/rss.xml"/>
|
||||
<outline text="Hal Mueller's Blog" type="rss" version="RSS" htmlUrl="https://halmueller.wordpress.com" xmlUrl="http://halmueller.wordpress.com/feed/"/>
|
||||
<outline text="And now it’s all this" type="rss" version="RSS" htmlUrl="http://leancrew.com/all-this" xmlUrl="http://www.leancrew.com/all-this/feed/"/>
|
||||
<outline text="OneThirtySeven" type="rss" version="RSS" htmlUrl="http://one37.net/blog/" xmlUrl="http://one37.net/blog?format=rss"/>
|
||||
<outline text="rentzsch.tumblr.com" type="rss" version="RSS" htmlUrl="http://rentzsch.tumblr.com/" xmlUrl="http://rentzsch.tumblr.com/rss"/>
|
||||
<outline text="⌥⇧K" type="rss" version="RSS" htmlUrl="http://optshiftk.com" xmlUrl="http://optshiftk.com/feed/"/>
|
||||
<outline text="Sam Ruby" type="rss" version="RSS" xmlUrl="http://intertwingly.net/blog/index.atom"/>
|
||||
<outline text="literator.me" type="rss" version="RSS" xmlUrl="http://literator.me/rss"/>
|
||||
<outline text="upbeat.it" type="rss" version="RSS" htmlUrl="http://www.upbeat.it/" xmlUrl="http://www.upbeat.it/atom.xml"/>
|
||||
<outline text="Minutes to Midnight" type="rss" version="RSS" htmlUrl="http://minutestomidnight.net/" xmlUrl="http://minutestomidnight.net/blog?format=RSS"/>
|
||||
<outline text="John Moltz's Very Nice Web Site" type="rss" version="RSS" htmlUrl="http://verynicewebsite.net" xmlUrl="http://verynicewebsite.net/feed/atom/"/>
|
||||
<outline text="Matt Mullenweg" type="rss" version="RSS" htmlUrl="https://ma.tt" xmlUrl="http://ma.tt/feed/"/>
|
||||
<outline text="BrettTerpstra.com" type="rss" version="RSS" xmlUrl="http://brettterpstra.com/atom.xml"/>
|
||||
<outline text="jwz" type="rss" version="RSS" htmlUrl="https://www.jwz.org/blog/" xmlUrl="http://www.jwz.org/blog/feed/"/>
|
||||
<outline text="Maniacal Rage" type="rss" version="RSS" htmlUrl="http://log.maniacalrage.net/" xmlUrl="http://log.maniacalrage.net/rss"/>
|
||||
<outline text="Shawn Blanc" type="rss" version="RSS" htmlUrl="http://shawnblanc.net" xmlUrl="http://shawnblanc.net/feed/"/>
|
||||
<outline text="carpeaqua" type="rss" version="RSS" htmlUrl="https://carpeaqua.com" xmlUrl="http://feeds.feedburner.com/carpeaqua"/>
|
||||
<outline text="Doug Russell" type="rss" version="RSS" xmlUrl="http://www.getitdownonpaper.com/atom.xml"/>
|
||||
<outline text="bbum's weblog-o-mat" type="rss" version="RSS" htmlUrl="http://www.friday.com/bbum" xmlUrl="http://www.friday.com/bbum/feed/"/>
|
||||
<outline text="Fraser Speirs" type="rss" version="RSS" htmlUrl="http://www.speirs.org/" xmlUrl="http://speirs.org/feed/"/>
|
||||
<outline text="ridiculous_fish" type="rss" version="RSS" xmlUrl="http://pammon.webfactional.com/blog/feed/"/>
|
||||
<outline text="Hypercritical" type="rss" version="RSS" htmlUrl="http://hypercritical.co/" xmlUrl="http://hypercritical.co/feeds/main"/>
|
||||
<outline text="Manton Reece" type="rss" version="RSS" htmlUrl="http://www.manton.org" xmlUrl="http://manton.org/rss.xml"/>
|
||||
<outline text="Gus's weblog" type="rss" version="RSS" htmlUrl="http://shapeof.com/" xmlUrl="http://gusmueller.com/blog/atom.xml"/>
|
||||
<outline text="Dustin Curtis" type="rss" version="RSS" htmlUrl="https://dcurt.is" xmlUrl="http://feeds.feedburner.com/dcurtis"/>
|
||||
<outline text="Waffle" type="rss" version="RSS" htmlUrl="http://waffle.wootest.net" xmlUrl="http://waffle.wootest.net/feed/atom/"/>
|
||||
<outline text="Mistitled" type="rss" version="RSS" htmlUrl="http://rms2.tumblr.com/" xmlUrl="http://rms2.tumblr.com/rss"/>
|
||||
<outline text="Don Melton" type="rss" version="RSS" htmlUrl="https://donmelton.com" xmlUrl="http://donmelton.com/rss.xml"/>
|
||||
<outline text="Anil Dash" type="rss" version="RSS" htmlUrl="http://anildash.com/" xmlUrl="http://feeds.dashes.com/AnilDash"/>
|
||||
<outline text="Neven Mrgan's tumbl" type="rss" version="RSS" htmlUrl="http://mrgan.tumblr.com/" xmlUrl="http://mrgan.tumblr.com/rss"/>
|
||||
<outline text="alexking.org: Blog" type="rss" version="RSS" htmlUrl="http://alexking.org" xmlUrl="http://alexking.org/blog/feed"/>
|
||||
<outline text="ParisLemon" type="rss" version="RSS" htmlUrl="http://parislemon.com/" xmlUrl="http://parislemon.com/rss"/>
|
||||
<outline text="Black Pixel" type="rss" version="RSS" htmlUrl="https://blackpixel.com/writing/" xmlUrl="http://blackpixel.com/blog/atom.xml"/>
|
||||
<outline text="Marco.org" type="rss" version="RSS" htmlUrl="https://marco.org/" xmlUrl="http://www.marco.org/rss"/>
|
||||
<outline text="The Guinea Pig in the Cocoa Mine" type="rss" version="RSS" xmlUrl="http://cocoamine.net/atom.xml"/>
|
||||
<outline text="Blog | Mike Abdullah" type="rss" version="RSS" htmlUrl="http://mikeabdullah.net/" xmlUrl="http://www.mikeabdullah.net/index.xml"/>
|
||||
<outline text="Kickingbear Blog" type="rss" version="RSS" htmlUrl="http://kickingbear.com/blog" xmlUrl="http://kickingbear.com/blog/feed"/>
|
||||
<outline text="Stuart Hall" type="rss" version="RSS" htmlUrl="http://stuartkhall.com/" xmlUrl="http://feeds.feedburner.com/stuartkhall"/>
|
||||
<outline text="ongoing" type="rss" version="RSS" xmlUrl="http://www.tbray.org/ongoing/ongoing.atom"/>
|
||||
<outline text="Stratēchery" type="rss" version="RSS" htmlUrl="https://stratechery.com" xmlUrl="http://stratechery.com/feed/"/>
|
||||
<outline text="Mark Bernstein" type="rss" version="RSS" htmlUrl="http://markbernstein.org/" xmlUrl="http://www.markbernstein.org/news.rss"/>
|
||||
</outline>
|
||||
<outline text="Writers">
|
||||
<outline text="Whatever" type="rss" version="RSS" htmlUrl="http://whatever.scalzi.com" xmlUrl="http://whatever.scalzi.com/feed/"/>
|
||||
<outline text="Charlie's Diary" type="rss" version="RSS" htmlUrl="http://www.antipope.org/charlie/blog-static/" xmlUrl="http://www.antipope.org/charlie/blog-static/atom.xml"/>
|
||||
<outline text="Gerrold" type="rss" version="RSS" htmlUrl="http://www.gerrold.com" xmlUrl="http://www.gerrold.com/feed/"/>
|
||||
</outline>
|
||||
</body>
|
||||
</opml>
|
File diff suppressed because one or more lines are too long
|
@ -1,710 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
|
||||
>
|
||||
<channel>
|
||||
<title>Aktuality.sk - aktuálne spravodajstvo</title>
|
||||
<link>https://www.aktuality.sk</link>
|
||||
<pubDate>Sun, 17 Jan 2021 23:46:22 +0100</pubDate>
|
||||
<description>Denne aktualizované spravodajstvo z domova i zo sveta. Správy z oblasti ekonomiky,
|
||||
kultúry a spoločenského života. Predpoveď počasia, horoskopy, TV program a kurzový lístok.</description>
|
||||
<dc:language>sk</dc:language>
|
||||
<dc:creator>Ringier Axel Springer SK</dc:creator>
|
||||
<sy:updatePeriod>hourly</sy:updatePeriod>
|
||||
<sy:updateFrequency>1</sy:updateFrequency>
|
||||
|
||||
<item>
|
||||
<title>Analytici Goldman Sachs zvýšili odhad rastu ekonomiky USA</title>
|
||||
<link>https://www.aktuality.sk/clanok/856577/analytici-goldman-sachs-zvysili-odhad-rastu-ekonomiky-usa/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/vUw5YHzvQKDCkNw6PtXtAg.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=OcgUSVfIJOH9QPOW-nqQxQ&e=2145916800&v=3</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Analytici Goldman Sachs zvýšili odhad rastu ekonomiky USA
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856577/analytici-goldman-sachs-zvysili-odhad-rastu-ekonomiky-usa/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/vUw5YHzvQKDCkNw6PtXtAg.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=OcgUSVfIJOH9QPOW-nqQxQ&e=2145916800&v=3" />
|
||||
<br/>
|
||||
Očakávajú totiž, že plán fiškálnych stimulov novozvoleného prezidenta Joea Bidena urýchli zotavovanie hospodárstva po pandémii ochorenia COVID-19.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 19:27:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Koronavírus: Cielené testovanie zachytilo v Banskej Bystrici desiatky pozitívnych</title>
|
||||
<link>https://www.aktuality.sk/clanok/856600/koronavirus-cielene-testovanie-zachytilo-v-banskej-bystrici-desiatky-pozitivnych/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/uimnlYXjRubxkNz9qHHvKw.320~Pr-prava-odberov-v-Banskej-Bystrici.jpg?t=LzE1OHgxMDYvc21hcnQ&h=o5WAHblMYtS3LhTEGJqP5w&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Koronavírus: Cielené testovanie zachytilo v Banskej Bystrici desiatky pozitívnych
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856600/koronavirus-cielene-testovanie-zachytilo-v-banskej-bystrici-desiatky-pozitivnych/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/uimnlYXjRubxkNz9qHHvKw.320~Pr-prava-odberov-v-Banskej-Bystrici.jpg?t=LzE1OHgxMDYvc21hcnQ&h=o5WAHblMYtS3LhTEGJqP5w&e=2145916800&v=2" />
|
||||
<br/>
|
||||
V krajskom meste otestovali viac ako 8-tisíc ľudí, ktorí nemôžu pracovať z domu.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 19:25:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Koronavírus: Slovensko čaká 9-dňové plošné testovanie, predĺžil sa zákaz vychádzania</title>
|
||||
<link>https://www.aktuality.sk/clanok/856582/koronavirus-slovensko-caka-9-dnove-plosne-testovanie-predlzi-sa-aj-zakaz-vychadzania/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/fXiDsLeeR2uP3wTcEQMcxg.320~Igor-Matovi-hovor-o-novom-pl-ne-pre-Slovensko-v-s-vislosti-s-koronav-rusom.jpg?t=LzE1OHgxMDYvc21hcnQ&h=UitS_uvS5fVTyL9_MCD3vw&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Koronavírus: Slovensko čaká 9-dňové plošné testovanie, predĺžil sa zákaz vychádzania
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856582/koronavirus-slovensko-caka-9-dnove-plosne-testovanie-predlzi-sa-aj-zakaz-vychadzania/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/fXiDsLeeR2uP3wTcEQMcxg.320~Igor-Matovi-hovor-o-novom-pl-ne-pre-Slovensko-v-s-vislosti-s-koronav-rusom.jpg?t=LzE1OHgxMDYvc21hcnQ&h=UitS_uvS5fVTyL9_MCD3vw&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Celoslovenské plošné testovanie začne od zajtra a potrvá do 26. januára. Povinnosť mať certifikát o negatívnom teste na Covid-19 začne platiť od 27 januára. Otestovať sa budete môcť v mobilných odberných miestach.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 19:06:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Komentár Ľubomíra Jaška: Pellegrini prerazil dno, ale Matovič mu v tom výrazne pomohol</title>
|
||||
<link>https://www.aktuality.sk/clanok/856603/komentar-lubomira-jaska-pellegrini-prerazil-dno-ale-matovic-mu-v-tom-vyrazne-pomohol/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/vjCQaQs5S-XhbgVKbM6O_A.320~Predseda-Hlasu-SD-Peter-Pellegrini.jpg?t=LzE1OHgxMDYvc21hcnQ&h=HbiWYr3kBM3gZZFg-66E8Q&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Komentár Ľubomíra Jaška: Pellegrini prerazil dno, ale Matovič mu v tom výrazne pomohol
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856603/komentar-lubomira-jaska-pellegrini-prerazil-dno-ale-matovic-mu-v-tom-vyrazne-pomohol/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/vjCQaQs5S-XhbgVKbM6O_A.320~Predseda-Hlasu-SD-Peter-Pellegrini.jpg?t=LzE1OHgxMDYvc21hcnQ&h=HbiWYr3kBM3gZZFg-66E8Q&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Atómová bomba plošného testovania môže Pellegriniho zásluhou vybuchnúť úplne inak, ako si predstavuje najväčší fanúšik tyčiniek v nose.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 19:00:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Británia vyzvala Čínu, aby umožnila inšpektorom z OSN navštíviť ujgurskú oblasť</title>
|
||||
<link>https://www.aktuality.sk/clanok/856579/britania-vyzvala-cinu-aby-umoznila-inspektorom-z-osn-navstivit-ujgursku-oblast/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/o7lvPB9UQa7nc2DDLRcIkg.320~Brit-nia-sa-dostala-na-zoznam-zelen-ch-kraj-n-Slov-kov-m-e-od-britskej-karant-ny-oslobodi-testovanie-na-letisku.jpg?t=LzE1OHgxMDYvc21hcnQ&h=AvL1vkAMoW9iJUmagwaVDg&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Británia vyzvala Čínu, aby umožnila inšpektorom z OSN navštíviť ujgurskú oblasť
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856579/britania-vyzvala-cinu-aby-umoznila-inspektorom-z-osn-navstivit-ujgursku-oblast/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/o7lvPB9UQa7nc2DDLRcIkg.320~Brit-nia-sa-dostala-na-zoznam-zelen-ch-kraj-n-Slov-kov-m-e-od-britskej-karant-ny-oslobodi-testovanie-na-letisku.jpg?t=LzE1OHgxMDYvc21hcnQ&h=AvL1vkAMoW9iJUmagwaVDg&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Británia obvinila v utorok Čínu z porušovania ľudských práv ujgurskej menšiny a zaviedla obmedzenia pre dovoz tovaru, pri ktorom existuje podozrenie, že bol výsledkom nútenej práce.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 18:47:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Guatemalská polícia použila slzotvorný plyn proti migrantom smerujúcim do USA</title>
|
||||
<link>https://www.aktuality.sk/clanok/856584/guatemalska-policia-pouzila-slzotvorny-plyn-proti-migrantom-smerujucim-do-usa/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/BWTlMtRQR9O3m3h1AVzYAQ.320~Guatemalsk-pol-cia-pou-ila-v-nede-u-slzotvorn-plyn-pri-pokuse-o-rozpt-lenie-tis-cov-migrantov-ktor-smeruj-do-Spojen-ch-t-tov.jpg?t=LzE1OHgxMDYvc21hcnQ&h=fgEYpogX5r_3yr9PP83DaQ&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Guatemalská polícia použila slzotvorný plyn proti migrantom smerujúcim do USA
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856584/guatemalska-policia-pouzila-slzotvorny-plyn-proti-migrantom-smerujucim-do-usa/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/BWTlMtRQR9O3m3h1AVzYAQ.320~Guatemalsk-pol-cia-pou-ila-v-nede-u-slzotvorn-plyn-pri-pokuse-o-rozpt-lenie-tis-cov-migrantov-ktor-smeruj-do-Spojen-ch-t-tov.jpg?t=LzE1OHgxMDYvc21hcnQ&h=fgEYpogX5r_3yr9PP83DaQ&e=2145916800&v=2" />
|
||||
<br/>
|
||||
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 18:16:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Lietadlo s Navaľným na palube pristálo v Moskve. Zadržali ho pri pasovej kontrole</title>
|
||||
<link>https://www.aktuality.sk/clanok/856578/v-rade-cislo-13-a-v-obkluceni-novinarov-lietadlo-s-navalnym-na-palube-pristalo-v-moskve/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/rQR7fDWdSUCrhoDxG5E-4w.320~Nava-nyj-v-obk-en-novin-rov-na-palube-lietadla-ruskej-spolo-nosti-Pobeda.jpg?t=LzE1OHgxMDYvc21hcnQ&h=ltlql5GozBU4I7ojtvlq_Q&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Lietadlo s Navaľným na palube pristálo v Moskve. Zadržali ho pri pasovej kontrole
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856578/v-rade-cislo-13-a-v-obkluceni-novinarov-lietadlo-s-navalnym-na-palube-pristalo-v-moskve/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/rQR7fDWdSUCrhoDxG5E-4w.320~Nava-nyj-v-obk-en-novin-rov-na-palube-lietadla-ruskej-spolo-nosti-Pobeda.jpg?t=LzE1OHgxMDYvc21hcnQ&h=ltlql5GozBU4I7ojtvlq_Q&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Na moskovskom letisku Vnukovo, kde malo pristáť lietadlo s Navaľným na palube, poriadkové sily zadržiavali ľudí. Lietadlo nakoniec odklonili na letisko Šeremetievo. Ruského opozičníka zadržali pri pasovej kontrole.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 18:09:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Zima ako u Mrázika. Na ruskú zimu neplatí ani vodka</title>
|
||||
<link>https://www.aktuality.sk/clanok/850383/zima-ako-u-mrazika-na-rusku-zimu-neplati-ani-vodka/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/ycvFEc7sRqDRN4gwgHe7RQ.320~Vyhod-vriacu-vodu-a-dopad-sneh-Ojmiakon-Rusko.jpg?t=LzE1OHgxMDYvc21hcnQ&h=EgmzER7E9H4nRn45dFNKMg&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Zima ako u Mrázika. Na ruskú zimu neplatí ani vodka
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/850383/zima-ako-u-mrazika-na-rusku-zimu-neplati-ani-vodka/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/ycvFEc7sRqDRN4gwgHe7RQ.320~Vyhod-vriacu-vodu-a-dopad-sneh-Ojmiakon-Rusko.jpg?t=LzE1OHgxMDYvc21hcnQ&h=EgmzER7E9H4nRn45dFNKMg&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Vedeli ste, že ani Rusi nezaháňajú zimu vodkou? Cestovateľ Martin Navrátil bol v meste Ojmiakon, kde bola nameraná najnižšia teplota na svete mimo Antarktídy a vypočul si to priamo od domácich.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 18:00:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Bielorusko: V niekoľkých mestách pokračovali protesty proti Lukašenkovi</title>
|
||||
<link>https://www.aktuality.sk/clanok/856576/bielorusko-v-niekolkych-mestach-pokracovali-protesty-proti-lukasenkovi/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/0CMFQVwiS3-kV3J90Uv1gg.320~D-chodcovia-protestuj-proti-Alexandarovi-Luka-enkovi.jpg?t=LzB4MzozMjB4MTgzLzE1OHgxMDYvc21hcnQ&h=prxFwHNADbX61294ZAVPPg&e=2145916800&v=3</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Bielorusko: V niekoľkých mestách pokračovali protesty proti Lukašenkovi
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856576/bielorusko-v-niekolkych-mestach-pokracovali-protesty-proti-lukasenkovi/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/0CMFQVwiS3-kV3J90Uv1gg.320~D-chodcovia-protestuj-proti-Alexandarovi-Luka-enkovi.jpg?t=LzB4MzozMjB4MTgzLzE1OHgxMDYvc21hcnQ&h=prxFwHNADbX61294ZAVPPg&e=2145916800&v=3" />
|
||||
<br/>
|
||||
Denné demonštrácie sa konajú v Bielorusku od prezidentských volieb zo začiatku vlaňajšieho augusta.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 17:49:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>K Slovensku sa približuje arktický vzduch</title>
|
||||
<link>https://www.aktuality.sk/clanok/856587/k-slovensku-sa-priblizuje-arkticky-vzduch/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/p2tnVTC8Qxa26IHd5-AhPg.320~Ilustra-n-foto-J-no-kov-diery.jpg?t=LzE1OHgxMDYvc21hcnQ&h=7caByfMIXHR9QDBy1Lqk2g&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
K Slovensku sa približuje arktický vzduch
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856587/k-slovensku-sa-priblizuje-arkticky-vzduch/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/p2tnVTC8Qxa26IHd5-AhPg.320~Ilustra-n-foto-J-no-kov-diery.jpg?t=LzE1OHgxMDYvc21hcnQ&h=7caByfMIXHR9QDBy1Lqk2g&e=2145916800&v=2" />
|
||||
<br/>
|
||||
SHMÚ vydal na nedeľnú noc aj výstrahy pred nízkymi teplotami. Pri výstrahe prvého stupňa môžu teploty klesnúť až do mínus 17 stupňov Celzia.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 17:22:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Vodička skončila so Škodou Felicia v Kunovskej priehrade</title>
|
||||
<link>https://www.aktuality.sk/clanok/856575/policia-vodicka-skoncila-so-skodou-felicia-v-kunovskej-priehrade/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/mEabrdgHQ5H-YvTISg66wg.320~Vodi-ka-skon-ila-so-kodou-Felicia-v-Kunovskej-priehrade.jpg?t=LzE1OHgxMDYvc21hcnQ&h=s85yNFKC_KFi3hT26TWlCw&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Vodička skončila so Škodou Felicia v Kunovskej priehrade
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856575/policia-vodicka-skoncila-so-skodou-felicia-v-kunovskej-priehrade/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/mEabrdgHQ5H-YvTISg66wg.320~Vodi-ka-skon-ila-so-kodou-Felicia-v-Kunovskej-priehrade.jpg?t=LzE1OHgxMDYvc21hcnQ&h=s85yNFKC_KFi3hT26TWlCw&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Asi po dvoch hodinách sa jej podarilo z auta dostať a požiadala o pomoc okoloidúce auto.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 17:13:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>V čínskej bani, v ktorej zavalilo 22 baníkov, zrejme objavili známky života</title>
|
||||
<link>https://www.aktuality.sk/clanok/856556/cina-v-bani-v-ktorej-zavalilo-22-banikov-zrejme-objavili-znamky-zivota/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/P2BOOeGhSDO7oW2gT7Q_4g.320~V-buch-v-bani-v-meste-chi-sia-vo-v-chodo-nskej-provincii-an-tung.jpg?t=LzE1OHgxMDYvc21hcnQ&h=KmrXmPCrbOr5vJo0SSZa0w&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
V čínskej bani, v ktorej zavalilo 22 baníkov, zrejme objavili známky života
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856556/cina-v-bani-v-ktorej-zavalilo-22-banikov-zrejme-objavili-znamky-zivota/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/P2BOOeGhSDO7oW2gT7Q_4g.320~V-buch-v-bani-v-meste-chi-sia-vo-v-chodo-nskej-provincii-an-tung.jpg?t=LzE1OHgxMDYvc21hcnQ&h=KmrXmPCrbOr5vJo0SSZa0w&e=2145916800&v=2" />
|
||||
<br/>
|
||||
V bani v čase nehody prebiehali stavebné práce.
|
||||
|
||||
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 16:49:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Pri pobreží Turecka sa potopila nákladná loď s Ukrajincami a Rusmi, hlásia obete</title>
|
||||
<link>https://www.aktuality.sk/clanok/856570/pri-pobrezi-turecka-sa-potopila-nakladna-lod-s-ukrajincami-a-rusmi-hlasia-obete/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/H795qTJHRZaBIQjZoBwNyw.320~ilustra-n-foto.jpg?t=LzB4MjozMjB4MTgyLzE1OHgxMDYvc21hcnQ&h=evSZWlnugRGixpDhm7f-9A&e=2145916800&v=1</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Pri pobreží Turecka sa potopila nákladná loď s Ukrajincami a Rusmi, hlásia obete
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856570/pri-pobrezi-turecka-sa-potopila-nakladna-lod-s-ukrajincami-a-rusmi-hlasia-obete/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/H795qTJHRZaBIQjZoBwNyw.320~ilustra-n-foto.jpg?t=LzB4MjozMjB4MTgyLzE1OHgxMDYvc21hcnQ&h=evSZWlnugRGixpDhm7f-9A&e=2145916800&v=1" />
|
||||
<br/>
|
||||
Na palube bolo 12 členov posádky - dvaja Rusi a desať Ukrajincov, vyplýva z aktualizovaných údajov tureckého ministerstva dopravy.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 16:12:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>OĽANO: Z poslancov hnutia sa prednostne očkovať nebude nikto</title>
|
||||
<link>https://www.aktuality.sk/clanok/856546/olano-z-poslancov-hnutia-sa-prednostne-ockovat-nebude-nikto/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/Iir7W_-ZRHuRYKjFfGvQNw.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=tTKDjpcr-WCck0ii_K7hPw&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
OĽANO: Z poslancov hnutia sa prednostne očkovať nebude nikto
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856546/olano-z-poslancov-hnutia-sa-prednostne-ockovat-nebude-nikto/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/Iir7W_-ZRHuRYKjFfGvQNw.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=tTKDjpcr-WCck0ii_K7hPw&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Ponuku predsedu NR SR Borisa Kollára na očkovanie organizované NR SR nevyužijú ani poslanci z klubu OĽANO nad 65 rokov či poslanci s chronickým ochorením, uviedlo hnutie.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 15:45:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Ministerka Kolíková pripustila možné rozloženie plošného testovania na viac dní</title>
|
||||
<link>https://www.aktuality.sk/clanok/856564/monitor-m-kolikova-pripustila-mozne-rozlozenie-plosneho-testovania-na-viac-dni/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/oiH-LtzgSfKgzwjIo3P-xg.320~M-ria-Kol-kov.jpg?t=LzE1OHgxMDYvc21hcnQ&h=EmcC1VWiBAcwU58K_vHI4w&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Ministerka Kolíková pripustila možné rozloženie plošného testovania na viac dní
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856564/monitor-m-kolikova-pripustila-mozne-rozlozenie-plosneho-testovania-na-viac-dni/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/oiH-LtzgSfKgzwjIo3P-xg.320~M-ria-Kol-kov.jpg?t=LzE1OHgxMDYvc21hcnQ&h=EmcC1VWiBAcwU58K_vHI4w&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Diskusia vo vláde sa podľa Kolíkovej týka aj toho, či treba viac sprísniť opatrenia a čo by to pre Slovensko znamenalo.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 15:13:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Pellegrini pripúšťa referendovú kampaň pri testovaní, Kolíková hovorí o zneužívaní situácie</title>
|
||||
<link>https://www.aktuality.sk/clanok/856555/monitor-pellegrini-pripusta-referendovu-kampan-pri-testovani-kolikova-namieta/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/DFeuPDb7TKnoEHGyUmPxog.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=EwBwCDY2OSSoh2F18-MowQ&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Pellegrini pripúšťa referendovú kampaň pri testovaní, Kolíková hovorí o zneužívaní situácie
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856555/monitor-pellegrini-pripusta-referendovu-kampan-pri-testovani-kolikova-namieta/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/DFeuPDb7TKnoEHGyUmPxog.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=EwBwCDY2OSSoh2F18-MowQ&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Ministerka spravodlivosti Mária Kolíková (Za ľudí) reagovala, že ak to Pellegrini ako expremiér urobí, bude to hrubé podkopávanie dôvery v štát, čo v tejto situácii nikomu nepomôže
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 14:46:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Infektológ Krčméry: Viem si predstaviť, že by boli seniori zaočkovaní do Veľkej noci</title>
|
||||
<link>https://www.aktuality.sk/clanok/856557/infektolog-krcmery-viem-si-predstavit-ze-by-boli-seniori-zaockovani-do-velkej-noci/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/-bkXS45NRwvx3oDMN4lryw.320.jpg?t=LzB4MTU6MzIweDE5NS8xNTh4MTA2L3NtYXJ0&h=wQoXY1pwdk-Qnto_a4zB4Q&e=2145916800&v=5</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Infektológ Krčméry: Viem si predstaviť, že by boli seniori zaočkovaní do Veľkej noci
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856557/infektolog-krcmery-viem-si-predstavit-ze-by-boli-seniori-zaockovani-do-velkej-noci/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/-bkXS45NRwvx3oDMN4lryw.320.jpg?t=LzB4MTU6MzIweDE5NS8xNTh4MTA2L3NtYXJ0&h=wQoXY1pwdk-Qnto_a4zB4Q&e=2145916800&v=5" />
|
||||
<br/>
|
||||
Na margo toho, prečo sprísnenie opatrení neprišlo ešte pred Vianocami, Krčméry uviedol, že ústredný krízový štáb sa nevedel dohodnúť, či bude skôr plošné testovanie a potom lockdown, alebo naopak.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 14:36:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Maroš Šefčovič: Vakcín bude dostatok na zaočkovanie vyše 80 % Európanov</title>
|
||||
<link>https://www.aktuality.sk/clanok/856544/maros-sefcovic-vakcin-bude-dostatok-na-zaockovanie-vyse-80-europanov/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/JxrtwXnyTF20fvkTGgU86g.320~Maro-ef-ovi.jpg?t=LzB4MzozMjB4MTgzLzE1OHgxMDYvc21hcnQ&h=0w-3LT932CbyFOGMzXGXDQ&e=2145916800&v=3</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Maroš Šefčovič: Vakcín bude dostatok na zaočkovanie vyše 80 % Európanov
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856544/maros-sefcovic-vakcin-bude-dostatok-na-zaockovanie-vyse-80-europanov/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/JxrtwXnyTF20fvkTGgU86g.320~Maro-ef-ovi.jpg?t=LzB4MzozMjB4MTgzLzE1OHgxMDYvc21hcnQ&h=0w-3LT932CbyFOGMzXGXDQ&e=2145916800&v=3" />
|
||||
<br/>
|
||||
Na pôde ministerskej rady by sa malo v pondelok debatovať aj o tom, že by bolo dobré, keby všetky krajiny Únie minimálne dvakrát týždenne zverejňovali údaje o tom, koľko dostala ktorá krajina vakcín a koľko je v danom štáte zaočkovaných ľudí.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 14:12:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Boris Kollár: Vakcinovať sa budú len poslanci v rizikovom veku a skupine</title>
|
||||
<link>https://www.aktuality.sk/clanok/856533/monitor-b-kollar-vakcinovat-sa-budu-len-poslanci-v-rizikovom-veku-a-skupine/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/Y824Wf-fRgHcRzr7rdsKkg.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=Ie6ihB3wClBg2c0UvZ-_-A&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Boris Kollár: Vakcinovať sa budú len poslanci v rizikovom veku a skupine
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856533/monitor-b-kollar-vakcinovat-sa-budu-len-poslanci-v-rizikovom-veku-a-skupine/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/Y824Wf-fRgHcRzr7rdsKkg.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=Ie6ihB3wClBg2c0UvZ-_-A&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Zároveň uviedol, že hoci dostal výzvu na očkovanie poslancov, požiadal poslanecké kluby, aby možnosť využili poslanci v rizikovom veku či rizikovej skupine.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 13:43:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Prieskum: Voľby by v januári vyhrala strana Hlas-SD, druhé je OĽANO</title>
|
||||
<link>https://www.aktuality.sk/clanok/856542/prieskum-volby-by-v-januari-vyhrala-strana-hlas-sd-druhe-je-olano/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/YBZLlaA5RUiWH1Xey7vgTw.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=_fZRRxFKwp3tQj23o_PX-w&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Prieskum: Voľby by v januári vyhrala strana Hlas-SD, druhé je OĽANO
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856542/prieskum-volby-by-v-januari-vyhrala-strana-hlas-sd-druhe-je-olano/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/YBZLlaA5RUiWH1Xey7vgTw.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=_fZRRxFKwp3tQj23o_PX-w&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Podľa prieskumu by voliť na začiatku januára prišlo 49 percent ľudí, možnosť, že by skôr prišli voliť, označilo 18 percent opýtaných.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 13:14:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>V sudánskom regióne Dárfúr sa opäť rozhoreli násilnosti, hlásia desiatky obetí</title>
|
||||
<link>https://www.aktuality.sk/clanok/856523/v-sudanskom-regione-darfur-sa-opat-rozhoreli-nasilnosti-hlasia-desiatky-obeti/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/uA_I-efVQYGQoErTU3JSsA.320~Nepokoje-v-Sud-ne.jpg?t=LzE1OHgxMDYvc21hcnQ&h=uT7a6qvCQm9EkLuNU13bMw&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
V sudánskom regióne Dárfúr sa opäť rozhoreli násilnosti, hlásia desiatky obetí
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856523/v-sudanskom-regione-darfur-sa-opat-rozhoreli-nasilnosti-hlasia-desiatky-obeti/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/uA_I-efVQYGQoErTU3JSsA.320~Nepokoje-v-Sud-ne.jpg?t=LzE1OHgxMDYvc21hcnQ&h=uT7a6qvCQm9EkLuNU13bMw&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Krvavé udalosti, ktoré prebiehajú od soboty rána, zanechali aj 97 zranených.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 12:49:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Prezidentka Čaputová o Matovičovom riadení krízy: Nevidím posun k lepšiemu</title>
|
||||
<link>https://www.aktuality.sk/clanok/856538/prezidentka-caputova-o-matovicovom-riadeni-krizy-nevidim-posun-k-lepsiemu/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/Er6pQuhATBTjzXx08cJn1Q.320~Prezidentka-Zuzana-ap-tov.jpg?t=LzE1OHgxMDYvc21hcnQ&h=I-C1vf5dqVT9MFPZjeJWXw&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Prezidentka Čaputová o Matovičovom riadení krízy: Nevidím posun k lepšiemu
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856538/prezidentka-caputova-o-matovicovom-riadeni-krizy-nevidim-posun-k-lepsiemu/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/Er6pQuhATBTjzXx08cJn1Q.320~Prezidentka-Zuzana-ap-tov.jpg?t=LzE1OHgxMDYvc21hcnQ&h=I-C1vf5dqVT9MFPZjeJWXw&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Podľa prezidentky je každý, kto má otázky k plošnému testovaniu, v súčasnosti označovaný ako triedny nepriateľ a sabotér.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 12:45:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Kollár verí, že vláda prijme kroky, Pellegrini hovorí o telenovele</title>
|
||||
<link>https://www.aktuality.sk/clanok/856527/monitor-b-kollar-veri-ze-vlada-prijme-kroky-p-pellegrini-hovori-o-telenovele/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/ivwyvG2YT9HIpfqqYuuvew.320~Boris-Koll-r-o-vy-etrovan-smrti-Lu-ansk-ho-a-d-vere-tefanovi-Hol-mu.jpg?t=LzE1OHgxMDYvc21hcnQ&h=-_6e5uQd2es41cufI1CA5w&e=2145916800&v=5</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Kollár verí, že vláda prijme kroky, Pellegrini hovorí o telenovele
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856527/monitor-b-kollar-veri-ze-vlada-prijme-kroky-p-pellegrini-hovori-o-telenovele/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/ivwyvG2YT9HIpfqqYuuvew.320~Boris-Koll-r-o-vy-etrovan-smrti-Lu-ansk-ho-a-d-vere-tefanovi-Hol-mu.jpg?t=LzE1OHgxMDYvc21hcnQ&h=-_6e5uQd2es41cufI1CA5w&e=2145916800&v=5" />
|
||||
<br/>
|
||||
Oponent v diskusii, nezaradený poslanec a líder strany Hlas Peter Pellegrini pripomenul, že vláda zatiaľ celý týždeň neúspešne rokuje o opatreniach a podobe plošného testovania.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 12:18:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Koronavírus: Spresnili odkiaľ pochádzajú nové prípady</title>
|
||||
<link>https://www.aktuality.sk/clanok/856515/koronavirus-spresnili-odkial-pochadzaju-nove-pripady/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/mEOdTGA1TlWbm6imAFlPEw.320~Ilustra-n-foto.jpg?t=LzE1OHgxMDYvc21hcnQ&h=Hf45ewlfaxiXMQG7Ywv3dg&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Koronavírus: Spresnili odkiaľ pochádzajú nové prípady
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856515/koronavirus-spresnili-odkial-pochadzaju-nove-pripady/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/mEOdTGA1TlWbm6imAFlPEw.320~Ilustra-n-foto.jpg?t=LzE1OHgxMDYvc21hcnQ&h=Hf45ewlfaxiXMQG7Ywv3dg&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Z ochorenia sa vyliečilo ďalších 2360 pacientov.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 12:09:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Parkoviská na bratislavskej Kolibe či Kamzíku sú aj dnes plné</title>
|
||||
<link>https://www.aktuality.sk/clanok/856513/parkoviska-na-bratislavskej-kolibe-ci-kamziku-su-aj-dnes-plne/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/doYZGE76SR7ykX8pn3p9VQ.320~Policajti-reguluj-parkovanie-na-pr-jazdovej-ceste-na-Kolibu.jpg?t=LzE1OHgxMDYvc21hcnQ&h=P2lm47a3Id2SrLXP2GWuMQ&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Parkoviská na bratislavskej Kolibe či Kamzíku sú aj dnes plné
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856513/parkoviska-na-bratislavskej-kolibe-ci-kamziku-su-aj-dnes-plne/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/doYZGE76SR7ykX8pn3p9VQ.320~Policajti-reguluj-parkovanie-na-pr-jazdovej-ceste-na-Kolibu.jpg?t=LzE1OHgxMDYvc21hcnQ&h=P2lm47a3Id2SrLXP2GWuMQ&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Polícia vyzvala ľudí, aby využili na cestovanie mestskú kromadnú dopravu.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 12:04:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Koronavírus: Záujem o testovanie je v Košiciach o polovicu nižší ako minule, výsledok príde aj ako sms</title>
|
||||
<link>https://www.aktuality.sk/clanok/856519/koronavirus-zaujem-o-testovanie-je-v-kosiciach-o-polovicu-nizsi-ako-minule-vysledok-pride-aj-ako-sms/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/RJuke6ltS8fy-BBNv29AxA.320~Otestova-sa-dobrovo-ne-m-u-aj-obyvatelia-Lun-ka-IX.jpg?t=LzE1OHgxMDYvc21hcnQ&h=8-YvSNilwUwKDCk8gM3mng&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Koronavírus: Záujem o testovanie je v Košiciach o polovicu nižší ako minule, výsledok príde aj ako sms
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856519/koronavirus-zaujem-o-testovanie-je-v-kosiciach-o-polovicu-nizsi-ako-minule-vysledok-pride-aj-ako-sms/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/RJuke6ltS8fy-BBNv29AxA.320~Otestova-sa-dobrovo-ne-m-u-aj-obyvatelia-Lun-ka-IX.jpg?t=LzE1OHgxMDYvc21hcnQ&h=8-YvSNilwUwKDCk8gM3mng&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Výsledok z testovania prostredníctvom SMS správy príde Košičanom za necelú polhodinu. Záujem je o päťdesiat percent nižší, ako pred týždňom.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 11:55:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Okresný súd Bratislava I vytýči termín s Liborom J. až v roku 2022</title>
|
||||
<link>https://www.aktuality.sk/clanok/856502/okresny-sud-bratislava-i-vytyci-termin-s-liborom-j-az-v-roku-2022/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/6GfwoHLnQs2J6wPGmLowLA.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=G18EoWTPBkpKPgzUWPTOLA&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Okresný súd Bratislava I vytýči termín s Liborom J. až v roku 2022
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856502/okresny-sud-bratislava-i-vytyci-termin-s-liborom-j-az-v-roku-2022/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/6GfwoHLnQs2J6wPGmLowLA.320.jpg?t=LzE1OHgxMDYvc21hcnQ&h=G18EoWTPBkpKPgzUWPTOLA&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Situáciu skomplikovalo, že doterajší sudca OS Bratislava I Roland Kemény je dlhodobo práceneschopný.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 11:41:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>V najvyšších polohách Tatier a Malej Fatre je zvýšené lavínové nebezpečenstvo</title>
|
||||
<link>https://www.aktuality.sk/clanok/856510/v-najvyssich-polohach-tatier-a-malej-fatre-je-zvysene-lavinove-nebezpecenstvo/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/EExpK-jPSxrOPHsafoJ5TA.320~Ilustra-n-sn-mka.jpg?t=LzE1OHgxMDYvc21hcnQ&h=ATWtH9xK3yzYtDkJzyfc8g&e=2145916800&v=2</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
V najvyšších polohách Tatier a Malej Fatre je zvýšené lavínové nebezpečenstvo
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856510/v-najvyssich-polohach-tatier-a-malej-fatre-je-zvysene-lavinove-nebezpecenstvo/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/EExpK-jPSxrOPHsafoJ5TA.320~Ilustra-n-sn-mka.jpg?t=LzE1OHgxMDYvc21hcnQ&h=ATWtH9xK3yzYtDkJzyfc8g&e=2145916800&v=2" />
|
||||
<br/>
|
||||
Vyššie stupne lavínového nebezpečenstva sú dôsledkom poslednej periódy sneženia, počas ktorej napadlo do 80 centimetrov nového snehu. Ten padal navyše za súčinnosti vetra.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 11:13:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Ako znášate rádioaktívne žiarenie? Odpoveď máte možno v čreve</title>
|
||||
<link>https://www.aktuality.sk/clanok/856293/ako-znasate-radioaktivne-ziarenie-odpoved-mate-mozno-v-creve/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/BgJ1bO2HS2vmxHdvHCDkVQ.320~Ilustra-n-foto.jpg?t=LzB4MDozMjB4MTc5LzE1OHgxMDYvc21hcnQ&h=rRV1VDNSgKQ8ABz2O1APxQ&e=2145916800&v=1</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Ako znášate rádioaktívne žiarenie? Odpoveď máte možno v čreve
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856293/ako-znasate-radioaktivne-ziarenie-odpoved-mate-mozno-v-creve/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/BgJ1bO2HS2vmxHdvHCDkVQ.320~Ilustra-n-foto.jpg?t=LzB4MDozMjB4MTc5LzE1OHgxMDYvc21hcnQ&h=rRV1VDNSgKQ8ABz2O1APxQ&e=2145916800&v=1" />
|
||||
<br/>
|
||||
Niektoré baktérie v čreve nás môžu chrániť aj pred rádioaktívnym žiarením, tvrdia vedci.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 11:00:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Poslanci OĽANO navrhujú individuálne vzdelávanie umožniť i na druhom stupni</title>
|
||||
<link>https://www.aktuality.sk/clanok/856497/nrsr-poslanci-olano-navrhuju-individualne-vzdelavanie-umoznit-i-na-druhom-stupni/</link>
|
||||
<image>
|
||||
<url>https://t.aimg.sk/magaziny/NsNzYd7LSPStEHj2hYMmvA.320~Ilustra-n-foto.jpg?t=LzB4MTA6MzIweDE5MC8xNTh4MTA2L3NtYXJ0&h=MTq5wFYGv2Dv22UBS0-jAg&e=2145916800&v=4</url>
|
||||
<title>
|
||||
<![CDATA[
|
||||
Poslanci OĽANO navrhujú individuálne vzdelávanie umožniť i na druhom stupni
|
||||
]]>
|
||||
</title>
|
||||
<link>https://www.aktuality.sk/clanok/856497/nrsr-poslanci-olano-navrhuju-individualne-vzdelavanie-umoznit-i-na-druhom-stupni/</link>
|
||||
<width>150</width>
|
||||
<height>150</height>
|
||||
</image>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<img src="https://t.aimg.sk/magaziny/NsNzYd7LSPStEHj2hYMmvA.320~Ilustra-n-foto.jpg?t=LzB4MTA6MzIweDE5MC8xNTh4MTA2L3NtYXJ0&h=MTq5wFYGv2Dv22UBS0-jAg&e=2145916800&v=4" />
|
||||
<br/>
|
||||
Cieľom návrhu zákona je vyriešenie doterajšieho stavu, v ktorom nie je umožnené individuálne vzdelávanie žiakov na druhom stupni základných škôl.
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Sun, 17 Jan 2021 10:45:00 +0100</pubDate>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
File diff suppressed because one or more lines are too long
|
@ -1,520 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
|
||||
>
|
||||
|
||||
<channel>
|
||||
<title>And now it’s all this</title>
|
||||
<atom:link href="http://leancrew.com/all-this/feed/" rel="self" type="application/rss+xml" />
|
||||
<link>http://leancrew.com/all-this</link>
|
||||
<description>I just said what I said and it was wrong. Or was taken wrong.</description>
|
||||
<lastBuildDate>Thu, 23 Nov 2017 21:08:29 +0000</lastBuildDate>
|
||||
<language>en-US</language>
|
||||
<sy:updatePeriod>hourly</sy:updatePeriod>
|
||||
<sy:updateFrequency>1</sy:updateFrequency>
|
||||
<generator>http://wordpress.org/?v=4.0</generator>
|
||||
<atom:link rel="hub" href="http://pubsubhubbub.appspot.com"/>
|
||||
<atom:link rel="hub" href="http://aniat.superfeedr.com"/>
|
||||
|
||||
<item>
|
||||
<title>Last thoughts on modifier keys</title>
|
||||
<link>http://leancrew.com/all-this/2017/11/last-thoughts-on-modifier-keys/</link>
|
||||
<pubDate>Thu, 23 Nov 2017 21:08:29 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/11/last-thoughts-on-modifier-keys/</guid>
|
||||
<description>
|
||||
<![CDATA[When I wrote the post about <a href="http://leancrew.com/all-this/2017/11/modifier-key-order/">ordering Mac modifier keys</a> a few days ago, I was thinking primarily about the proper order of the <em>symbols</em> when writing about a keyboard shortcut, like ⌃⌥⌘P.<sup id="fnref:preview"><a href="#fn:preview" rel="footnote">1</a></sup>. I mentioned parenthetically that this order isn’t always observed when people speak about keyboard shortcuts or when they write the names of the keys out fully, as in “Command-Shift-3 takes a screenshot.”]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>When I wrote the post about <a href="http://leancrew.com/all-this/2017/11/modifier-key-order/">ordering Mac modifier keys</a> a few days ago, I was thinking primarily about the proper order of the <em>symbols</em> when writing about a keyboard shortcut, like ⌃⌥⌘P.<sup id="fnref:preview"><a href="#fn:preview" rel="footnote">1</a></sup>. I mentioned parenthetically that this order isn’t always observed when people speak about keyboard shortcuts or when they write the names of the keys out fully, as in “Command-Shift-3 takes a screenshot.”</p>
|
||||
<p>Jason Snell, in both <a href="https://sixcolors.com/link/2017/11/the-order-of-modifier-keys-on-the-mac/">a post at Six Colors</a> and in conversation with John Siracusa on <a href="https://www.relay.fm/upgrade/168">the lastest episode of Upgrade</a>, took a stand against Apple’s ordering:<sup id="fnref:hig"><a href="#fn:hig" rel="footnote">2</a></sup></p>
|
||||
<blockquote>
|
||||
<p>Command is the commander! Command is the monarch of all keys! Command always comes first, in my book.</p>
|
||||
</blockquote>
|
||||
<p>Siracusa agreed, and so do I. The ⌘ key is, and has always been, the key that signals a keyboard shortcut. While other modifier keys are sometimes used without ⌘—in cursor control and text selection, for example—I can’t think of any Apple applications that don’t use ⌘ to signal a keyboard shortcut for a menu item. And that primacy in shortcuts to menu items is, I think, why Apple puts it last rather than first.</p>
|
||||
<p>Keyboard shortcuts are always presented right-justified along the right edge of the menu. The most common shortcuts are just ⌘ and a letter, like ⌘N to start a new document, for example. It’s typically the variations on the basic command that get additional modifier keys, like ⌥⌘N to start a new project. If that were presented in a menu as ⌘⌥N, the menu would look wrong because the ⌘ symbols wouldn’t line up.</p>
|
||||
<p>Here’s the <span class="menu">File</span> menu in Safari:</p>
|
||||
<p><img alt="Safari File menu" class="ss" src="http://leancrew.com/all-this/images2017/20171123-Safari%20File%20menu.png" title="Safari File menu"/></p>
|
||||
<p>There are two different <span class="menu">New</span> commands and three different <span class="menu">Close</span> commands. This, in Apple’s opinion (and mine), wouldn’t be right:</p>
|
||||
<p><img alt="Altered Safari File menu" class="ss" src="http://leancrew.com/all-this/images2017/20171123-Altered%20Safari%20File%20menu.png" title="Altered Safari File menu"/></p>
|
||||
<p>It’s not just having the ⌘ symbols aligned. The additional modifier symbols go in front <em>because</em> ⌘ is king and must sit next to the N or the W. The importance of the modifier decreases as you move away from the letter.</p>
|
||||
<p>It should go without saying—but I’ll say it anyway—that the letter (or number or whatever) key is the most important because <em>nothing</em> happens until it’s pressed.</p>
|
||||
<p>Having said all this, and despite agreeing with Apple’s symbol ordering, my ear for shortcut ordering works just like Jason’s and John’s. The main reason I use keyboard shortcut symbols in my posts instead of words is that I can read ⌥⇧⌘W and not be bothered because I don’t “hear” it as I read the symbols. “Option-Shift-Command-W,” on the other hand, gets sounded out in my head, and it sounds wrong.</p>
|
||||
<p>I suspect that’s why Apple’s own documentation sometimes gets the order wrong when the modifiers get written out as words. In speaking out the keys, “Command” is natural to put first because it announces that what’s coming is a keyboard shortcut.</p>
|
||||
<div class="footnotes">
|
||||
<hr/>
|
||||
<ol>
|
||||
<li id="fn:preview">
|
||||
<p>Which happens to be the shortcut I use for previewing a blog post locally before publishing it. <a href="#fnref:preview" rev="footnote">↩︎</a></p>
|
||||
</li>
|
||||
<li id="fn:hig">
|
||||
<p>In the original post, I said I didn’t know where the order was documented. A few people pointed me to both the <a href="https://developer.apple.com/macos/human-interface-guidelines/user-interaction/keyboard/">Human Interface Guidelines</a> and the <a href="https://itunes.apple.com/us/book/apple-style-guide/id1161855204?mt=11">Style Guide</a>, where Apple gives the proper order explicitly. <a href="#fnref:hig" rev="footnote">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/11/last-thoughts-on-modifier-keys/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>My next Mac</title>
|
||||
<link>http://leancrew.com/all-this/2017/11/my-next-mac/</link>
|
||||
<pubDate>Wed, 22 Nov 2017 22:04:57 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/11/my-next-mac/</guid>
|
||||
<description>
|
||||
<![CDATA[Will probably be an iMac. I guess that spoils the suspense, doesn’t it?]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>Will probably be an iMac. I guess that spoils the suspense, doesn’t it?</p>
|
||||
<p>My iMac at work is the <a href="https://support.apple.com/kb/sp667?locale=en_US">27″ Late 2012 model</a>, the one that came out one step before Retina came to the iMac. I don’t regret buying it, as my previous iMac (a <a href="https://everymac.com/systems/apple/imac/specs/imac-core-2-duo-2.16-24-inch-specs.html">2006 model</a>, I think) was absolutely on its last legs—constantly swapping to hard disk and running hot. I hadn’t meant to wait so long to replace it, but there was <a href="https://buyersguide.macrumors.com/">a long delay</a> before that 2012 model came out, and I didn’t want to buy something that would be last year’s model almost as soon as I set it up.</p>
|
||||
<p>My home Mac is the venerable 2010 13″ MacBook Air, the first good Air. In the normal course of things, this would be the Mac I replace next, and I’ve been expecting to do so for a few years now. but…</p>
|
||||
<p>But Apple never came out with a Retina MacBook Air, choosing instead to go with the MacBook, which I find a little too far on the portable side of the portability/power spectrum. A couple of years ago, I had a crisis when <a href="http://leancrew.com/all-this/2015/06/beep-beep-beep/">my Air crapped out on me</a>. It seemed wrong to put money into a five-year-old machine, but I wasn’t enthused about any of the MacBooks in the lineup at the time. I didn’t know the just-released 2015 MacBook Pro would turn out to be the <a href="https://marco.org/2017/11/14/best-laptop-ever">best laptop ever made</a>, I didn’t want to spend MacBook Pro money on my home/travel machine.</p>
|
||||
<p>The $280 logic board upgrade turned out to be a good investment, as I’m now 2½ years into my rejuvenated Air. Yes, it takes a while to wake up when I open the lid. Yes, its 128 GB SSD is tiny. No, it can’t take advantage of many of the iOS integration features that newer Macs can. And no, I don’t think it’s a good idea to install High Sierra on it. But it’s given me 30 months of faithful use, much more than I expected at the time.</p>
|
||||
<p>The announcement of the Touch Bar last year made me certain I’d be getting a MacBook Pro with it. A software-configurable set of controls seemed perfect for someone who’s always ginning up little scripts. But no one seems to like it, possibly because its configurability isn’t especially open to users. Bummer.</p>
|
||||
<p>I’ve delayed the decision on my home Mac for such a long time that now my office Mac is long in the tooth, too. Still working fine for most tasks, but just a Core 2 Duo machine that often <a href="http://leancrew.com/all-this/2015/10/dealing-with-a-recalcitrant-pdf/">makes me wait</a> to scroll through long PDFs of scanned engineering and architectural drawings, something I need to do at work quite often. And no Retina.</p>
|
||||
<p>So it looks like my best bet is to buy a new iMac for work and bring my current office iMac home. This will put the power where I need it the most and will give me extra ooomph here at home. Especially with disk space (3 TB vs. 128 GB) and RAM (24GB vs. 4GB).</p>
|
||||
<p>It will be weird, though, as I haven’t had a desktop computer here at home in a dozen years. Will I enjoy being tethered to one spot in the house? And what about a travel computer?</p>
|
||||
<p>Both of these questions are made less pressing by the device I’m typing this on: a 9.7″ iPad Pro. While I agree with <a href="http://www.macdrifter.com/2017/11/the-mac-still-feels-like-home.html">Gabe</a> that it is by no means a Mac substitute, it can handle a lot of what I do at home and virtually everything I need to do on the road.<sup id="fnref:gabe"><a href="#fn:gabe" rel="footnote">1</a></sup></p>
|
||||
<p>As for which iMac, I think I’ll settle on a middle-of-the-road 27″ configuration with a 3TB Fusion drive. Sort of the 2017 of what I bought in 2012.</p>
|
||||
<div class="footnotes">
|
||||
<hr/>
|
||||
<ol>
|
||||
<li id="fn:gabe">
|
||||
<p>I bought the iPad Pro last year as a sort of experiment to find out how comfortable I’d be working on it. I intend to write a full post about the results of that experiment soon, but in the meantime, you really should read <a href="http://www.macdrifter.com/2017/11/the-mac-still-feels-like-home.html">Gabe’s post over at Macdrifter</a>. I’ll probably use his post as a jumping-off point. And there may be a quiz. <a href="#fnref:gabe" rev="footnote">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/11/my-next-mac/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Modifier key order</title>
|
||||
<link>http://leancrew.com/all-this/2017/11/modifier-key-order/</link>
|
||||
<pubDate>Mon, 20 Nov 2017 02:22:59 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/11/modifier-key-order/</guid>
|
||||
<description>
|
||||
<![CDATA[If you write about Mac keyboard shortcuts, as I did yesterday, you should know how to do it right. Just as there’s a <a href="https://dictionary.cambridge.org/grammar/british-grammar/adjectives-order">proper order for adjectives</a> in English, there’s a proper order for listing the modifier keys in a shortcut.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>If you write about Mac keyboard shortcuts, as I did yesterday, you should know how to do it right. Just as there’s a <a href="https://dictionary.cambridge.org/grammar/british-grammar/adjectives-order">proper order for adjectives</a> in English, there’s a proper order for listing the modifier keys in a shortcut.</p>
|
||||
<p>I haven’t found any documentation for this, but Apple’s preferred order is clear in how they show the modifiers in menus and how they’re displayed in the Keyboard Shortcuts Setting.</p>
|
||||
<p><img alt="Canonical Mac modifier key order" class="ss" src="http://leancrew.com/all-this/images2017/20171119-Canonical%20Mac%20modifier%20key%20order.png" title="Canonical Mac modifier key order"/></p>
|
||||
<p>The order is similar to how you see them down at the bottom left of your keyboard.</p>
|
||||
<p><img alt="Modifier keys" class="ss" src="http://leancrew.com/all-this/images2017/20171119-Modifier%20keys.jpg" title="Modifier keys"/></p>
|
||||
<p>Control (⌃), Option (⌥), and Command (⌘) always go in that order. The oddball is the Shift(⇧) key, which sneaks in just in front of Command.</p>
|
||||
<p>Keyboard Maestro recognizes this standard order and presents its “hot key” shortcut the same way.</p>
|
||||
<p><img alt="Keyboard Maestro hot key field" class="ss" src="http://leancrew.com/all-this/images2017/20171119-Keyboard%20Maestro%20hot%20key%20field.png" title="Keyboard Maestro hot key field"/></p>
|
||||
<p>(When people speak about keyboard shortcuts, it’s not uncommon to put Command first, e.g., “Command-Shift-3 takes a screenshot.” I’ve seen it written out that way, too. Apple is usually pretty careful to use the same order when using words as when using symbols. <a href="https://support.apple.com/en-us/HT201361">This page</a>, for example, uses “Shift-Command-3,” to match the ⇧⌘3 you’d see in the Keyboard Shortcut Settings. But even Apple slips up. On <a href="https://support.apple.com/en-us/HT201236">the grand Mac keyboard shortcut page</a>, there are a few instances of “Command-Shift” instead of “Shift-Command.”)</p>
|
||||
<p>The last bit of standard syntax is that the letter key in the shortcut (if there is a letter) is always presented as a capital, even when the Shift key isn’t used. I suspect this was a serious topic of discussion at Bandley 3 back in the early 80s as the Mac was being developed. They got it right. When entering a keyboard shortcut, you’re not typing a letter, you’re pressing a set of physical keys on the keyboard in front of you. The symbols on the letter keys are capitals, so that’s the appropriate way to identify those keys.</p>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/11/modifier-key-order/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Command-E</title>
|
||||
<link>http://leancrew.com/all-this/2017/11/command-e/</link>
|
||||
<pubDate>Sun, 19 Nov 2017 02:02:50 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/11/command-e/</guid>
|
||||
<description>
|
||||
<![CDATA[Earlier this evening, Merlin tweeted out some advice we should all heed:]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>Earlier this evening, Merlin tweeted out some advice we should all heed:</p>
|
||||
<blockquote class="twitter-tweet" data-lang="en"><p dir="ltr" lang="en">Per today's <a href="https://twitter.com/thetalkshow?ref_src=twsrc%5Etfw">@thetalkshow</a>, here's an Apple Support doc that can change your life. I dare you not to find something new here.<a href="https://t.co/37FuPPUbYK">https://t.co/37FuPPUbYK</a></p>— Merlin Mann (@hotdogsladies) <a href="https://twitter.com/hotdogsladies/status/932031370800529408?ref_src=twsrc%5Etfw">November 18, 2017</a></blockquote>
|
||||
<script async="" charset="utf-8" src="https://platform.twitter.com/widgets.js"></script>
|
||||
<p>Even though I’ve been using a Mac for 32 years (albeit with an 8-year Linux hiatus) there were a few keyboard shortcuts in there I didn’t know about. And, just as important, several that I once knew but had fallen out of the habit of using.</p>
|
||||
<p>But one of my favorites—and a big convenience when writing code or prose—isn’t on the <a href="https://support.apple.com/en-ae/HT201236">Mac keyboard shortcut page</a>. I wonder if the folks at Apple have forgotten it.</p>
|
||||
<p>It has to do with finding text in a document. You know about ⌘F to specify a <span class="menu">Find</span>. That usually brings up a window or other control into which you can type (or paste) the text you’re going to be searching for. Often, there are options for controlling how the search will be performed and for replacement text. Here’s the Find window from BBEdit,</p>
|
||||
<p><img alt="BBEdit Find window" class="ss" src="http://leancrew.com/all-this/images2017/20171118-BBEdit%20Find%20window.png" title="BBEdit Find window"/></p>
|
||||
<p>here’s the Find window from Pages,</p>
|
||||
<p><img alt="Pages Find window" class="ss" src="http://leancrew.com/all-this/images2017/20171118-Pages%20Find%20window.png" title="Pages Find window"/></p>
|
||||
<p>and here’s TextEdit, which pops up a little Find section in the main window below the ruler,</p>
|
||||
<p><img alt="TextEdit with Find section" class="ss" src="http://leancrew.com/all-this/images2017/20171118-TextEdit%20with%20Find%20section.png" title="TextEdit with Find section"/></p>
|
||||
<p>You probably also know about ⌘G, which is usually given the menu name <span class="menu">Find Next</span>. It repeats the last <span class="menu">Find</span> command, allowing you to step through all the occurrences of that text in the document. There are usually other ways to cycle through all the found instances—Pages and TextEdit have arrow buttons you can click on—but ⌘G is the traditional way.<sup id="fnref:page"><a href="#fn:page" rel="footnote">1</a></sup></p>
|
||||
<p>A common situation, especially when looking through long documents or source code files, is to see a particular string of text and want to find other instances of it. You may, for example, see the definition of a function and want to search out where that function gets called later in the code. One way to do that would be to select the function name, copy it, ⌘F to bring up the text entry field, paste, and then ⌘G your way through the rest of the document.<sup id="fnref:all"><a href="#fn:all" rel="footnote">2</a></sup> But the copy/⌘F/paste dance is a little clumsy.</p>
|
||||
<p>Which is where ⌘E, which is typically given a menu name like <span class="menu">Use Selection for Find</span>, comes in.</p>
|
||||
<p><img alt="BBEdit Search menu" class="ss" src="http://leancrew.com/all-this/images2017/20171118-BBEdit%20Search%20menu.png" title="BBEdit Search menu"/></p>
|
||||
<p>Select the text you want to search for and hit ⌘E. That turns the selection into the search text, and you can go straight to ⌘G to walk through all the other occurrences.</p>
|
||||
<p>I use ⌘E all the time when searching through code for function and variable names, as in the example above. It also works in Safari, Chrome, Preview, and PDF Expert, so I do similar quick searching on words I come across as I read news articles, blogs, reports, papers, etc.</p>
|
||||
<p>⌘E works in other well-behaved applications, too, so you should give it a try. Note that “well-behaved applications” excludes MS Word. In Word, ⌘E centers the paragraph of text containing the selection. Of course.</p>
|
||||
<div class="update">
|
||||
<p><strong>Update Nov 19, 2017 8:40 AM</strong><br/>
|
||||
A couple of things I forgot to include:</p>
|
||||
<p>First, ⌘E in the Finder has, somewhat ironically, no relationship to finding. It ejects or unmounts the selected disk, assuming there is a selected disk. This shortcut is listed on Apple’s <a href="https://support.apple.com/en-ae/HT201236">Mac shortcut page</a> (which is why I was going to mention it), and a couple of people reminded me of it after the post was published. I guess I forgot about it because it’s a command I never use; I eject by right-clicking and selecting <span class="menu">Eject</span> from the menu that pops up.</p>
|
||||
<p>Second, editors and word processors on iOS have not inherited this useful shortcut from the Mac. There is, of course, no ⌘ key on the software keyboard, but many of the traditional Mac shortcuts that use ⌘, ⌥, and ⇧ for moving the cursor and selecting text do work on iOS when you use an external keyboard. ⌘F and ⌘G (and its backward cousin, ⇧⌘G) often work in iOS, too, but ⌘E didn’t make the cut. Too bad.</p>
|
||||
<p><img alt="Keyboard shortcuts in iOS Notes" class="ss" src="http://leancrew.com/all-this/images2017/20171119-Keyboard%20shortcuts%20in%20iOS%20Notes.jpg" title="Keyboard shortcuts in iOS Notes" width="60%"/></p>
|
||||
</div>
|
||||
<div class="footnotes">
|
||||
<hr/>
|
||||
<ol>
|
||||
<li id="fn:page">
|
||||
<p>I believe ⌘G was originally used as the shortcut for <span class="menu">Go To Page…</span> in MacWrite, but it’s been <span class="menu">Find Next</span> for ages. <a href="#fnref:page" rev="footnote">↩</a></p>
|
||||
</li>
|
||||
<li id="fn:all">
|
||||
<p>Yes, source code editors in general, and BBEdit in particular, have other ways to find all the instances of a string of text, and those ways may be more appropriate in some situations. But stepping your way through each instance in turn is often the best way to see the string in full context. <a href="#fnref:all" rev="footnote">↩</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/11/command-e/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Converting fractions to decimal values</title>
|
||||
<link>http://leancrew.com/all-this/2017/11/converting-fractions-to-decimal-values/</link>
|
||||
<pubDate>Mon, 13 Nov 2017 04:35:39 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/11/converting-fractions-to-decimal-values/</guid>
|
||||
<description>
|
||||
<![CDATA[Recently, I’ve been getting Excel spreadsheets from clients that contain measurements in inches and/or fractions of an inch. In some cases, the cells with these measurements actually contain numbers but are displayed as fractions using one of the many formatting options Excel has.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>Recently, I’ve been getting Excel spreadsheets from clients that contain measurements in inches and/or fractions of an inch. In some cases, the cells with these measurements actually contain numbers but are displayed as fractions using one of the many formatting options Excel has.</p>
|
||||
<p><img alt="Excel cell formatting options" class="ss" src="http://leancrew.com/all-this/images2017/20171112-Excel%20cell%20formatting%20options.png" title="Excel cell formatting options"/></p>
|
||||
<p>In these cases, it’s easy for me to change the formatting to a standard decimal representation before exporting the sheet as a CSV file.</p>
|
||||
<div class="sidebar">
|
||||
<p><strong>Aside</strong><br/>
|
||||
The only reason I own Excel is to be able to open spreadsheets that other people send me and export them into some other usable format. This wasn’t always the case. I used the hell out of Excel in the late 80s and early 90s, when it was still a proper Mac app. But somewhere there in the early to mid 90s, Microsoft turned Excel into a Windows app that ran on the Mac and ruined its user interface. I stopped using it shortly thereafter. In the quarter-century since, it’s only gotten worse. Note, for example, the absurd layout of the dialog box above.</p>
|
||||
<p>Small Excel spreadsheets usually get moved over to Numbers, a considerably less powerful spreadsheet, but one that works the way a normal Mac app works. Large Excel spreadsheets get converted to CSV (often after a short visit to Numbers, as I’ve had trouble with Excel’s CSV conversion) so I can query and manipulate the data with <a href="http://pandas.pydata.org/">Pandas</a>. Why don’t I just open the Excel file in Pandas <a href="https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_excel.html">directly</a>? Because the spreadsheets I get are so filled with cruft, <code>read_excel</code> can’t be trusted.</p>
|
||||
</div>
|
||||
<p>Often, though, the measurements are entered as text. This is done by prefixing the data with a single quote mark, e.g.,</p>
|
||||
<pre><code>'2 5/16
|
||||
</code></pre>
|
||||
<p>or maybe</p>
|
||||
<pre><code>'2-5/16
|
||||
</code></pre>
|
||||
<p>These need to be converted into a decimal representation before exporting to CSV, so Pandas will recognize them as numbers rather than strings.<sup id="fnref:pandas"><a href="#fn:pandas" rel="footnote">1</a></sup></p>
|
||||
<p>To handle this situation, I created a Keyboard Maestro called Floatize. To use Floatize, I</p>
|
||||
<ul>
|
||||
<li>Select the column of data I want to convert.</li>
|
||||
<li>Copy it to the clipboard.</li>
|
||||
<li>Run Floatize, which converts the clipboard from a fractional representation to decimal.</li>
|
||||
<li>Paste the result back into the column.</li>
|
||||
</ul>
|
||||
<p>Here’s the macro:</p>
|
||||
<p><img alt="Floatize Keyboard Maestro macro" class="ss" src="http://leancrew.com/all-this/images2017/20171112-Floatize%20Keyboard%20Maestro%20macro.png" title="Floatize Keyboard Maestro macro" width="80%"/></p>
|
||||
<p>It takes the clipboard, sends it to a script as standard input, and then puts the output back into the clipboard. Here’s the Python script it runs:</p>
|
||||
<pre><code>python:
|
||||
1: #!/usr/bin/env python
|
||||
2:
|
||||
3: from fractions import Fraction
|
||||
4: import re
|
||||
5: import sys
|
||||
6:
|
||||
7: def floatize(s):
|
||||
8: if s in ['', 'n/a', 'none']:
|
||||
9: return ''
|
||||
10: else:
|
||||
11: try:
|
||||
12: num = float(s)
|
||||
13: except ValueError:
|
||||
14: t = re.sub(r'(.*\d) *- *(\d.*)', r'\1 \2', s)
|
||||
15: if t[0] == '-':
|
||||
16: num = -float(sum(Fraction(x) for x in t[1:].split(None, 1)))
|
||||
17: else:
|
||||
18: num = float(sum(Fraction(x) for x in t.split(None, 1)))
|
||||
19: return "{:f}".format(num)
|
||||
20:
|
||||
21: for line in sys.stdin.read().splitlines():
|
||||
22: item = line.strip().lower()
|
||||
23: print floatize(item)
|
||||
</code></pre>
|
||||
<p>The bulk of the fraction parsing is done by the <a href="https://docs.python.org/2.7/library/fractions.html"><code>fractions</code> module</a> in either Line 16 or Line 18. The rest of the script is an attempt to manipulate the oddball types of data I get into a format that <code>fractions</code> understands. Here’s what it can handle:</p>
|
||||
<ul>
|
||||
<li>Blank, <code>n/a</code> or <code>none</code> entries get turned into an empty string (Lines 8–9). Note that entries are converted to lower case in Line 22, before being passed to the <code>floatize</code> function, so this handles <code>N/A</code> and <code>None</code>, too.</li>
|
||||
<li>Entries already in decimal format get converted to a <code>float</code> (Line 12) and then output in decimal format (Line 19). This is basically a pass-through.</li>
|
||||
<li>Entries with a dash between the whole and fractional parts get the dash removed (Line 14).</li>
|
||||
<li>Negative entries (Line 15) have their whole and fractional parts added together with the sign reversed (Line 16), and then output in decimal format (Line 19).</li>
|
||||
<li>Positive entries have their whole and fractional parts added together (Line 18), and then output in decimal format (Line 19).</li>
|
||||
<li>Entries that don’t look like any of the above cause an error.</li>
|
||||
</ul>
|
||||
<p>The macro works perfectly in Numbers and BBEdit, but not in Excel. For some reason, nothing happens to the clipboard if I run the Floatize macro while Excel is the active application. I can, however, copy the column of fractions, activate another app, run Floatize, switch back to Excel and paste the converted clipboard. Another example of Excel not acting like a proper Mac app and another reason I move data out of Excel as quickly as I can.</p>
|
||||
<p>Despite its inability to work inside Excel, this macro has been very handy over the past couple of weeks. I’ve used it to clean up several data sets that were loaded with fractions.</p>
|
||||
<p>By the way, I will block anyone who tweets that I wouldn’t have this problem if I used the metric system.</p>
|
||||
<div class="update">
|
||||
<p><strong>Update Nov 13, 2017 10:04 PM</strong>
|
||||
The Twitter consensus is my problem in using this macro in Excel stems from Excel’s clipboard dumbassery. It apparently uses a private clipboard instead of the system clipboard until you bring another application to the front. Hence the macro’s inability to convert the clipboard unless I switched away from Excel.</p>
|
||||
<p>Jimmy Hartington suggested this workaround:</p>
|
||||
<div class="bbpBox" id="t929938579085807616">
|
||||
<blockquote>
|
||||
<span class="twContent"><a href="http://twitter.com/drdrang">@drdrang</a> Excel does something weird with the clipboard as you noticed. <br/>Try this. Copy in Excel. Press Escape. Run the macro.<br/>
|
||||
By pressing Escape I think Excel stops messing with the clipboard.
|
||||
<br/><span class="twDecoration">— </span><span class="twRealName">Jimmy Hartington</span><span class="twDecoration"> (</span><a href="http://twitter.com/jimmyhartington"><span class="twScreenName">@jimmyhartington</span></a><span class="twDecoration">) </span><a href="https://twitter.com/jimmyhartington/status/929938579085807616"><span class="twTimeStamp">Nov 12 2017 11:06 PM</span></a><span class="twDecoration"></span></span>
|
||||
</blockquote>
|
||||
</div>
|
||||
<p>This works perfectly and pointed me in the direction to make a better version of the macro, one that does all the copying, converting, and pasting:</p>
|
||||
<p><img alt="New Floatize Keyboard Maestro macro" class="ss" src="http://leancrew.com/all-this/images2017/20171113-New%20Floatize%20Keyboard%20Maestro%20macro.png" title="New Floatize Keyboard Maestro macro" width="80%"/></p>
|
||||
<p>Note the conditional action that simulates the pressing of the Escape key only if Excel is the front application. This is necessary because Escape causes Numbers to deselect the column, which screws up the paste action at the end of the macro.</p>
|
||||
<p>With this macro I can now select the column that needs converting and just press ⌃⌥⌘F to do the conversion in a single step. Thanks, Jimmy!</p>
|
||||
</div>
|
||||
<div class="footnotes">
|
||||
<hr/>
|
||||
<ol>
|
||||
<li id="fn:pandas">
|
||||
<p>There are probably ways to get Pandas to do the conversion as it imports the data, but I feel more comfortable—more in control—if I do it ahead of time. <a href="#fnref:pandas" rev="footnote">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/11/converting-fractions-to-decimal-values/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>A modest proposal</title>
|
||||
<link>http://leancrew.com/all-this/2017/11/a-modest-proposal/</link>
|
||||
<pubDate>Tue, 07 Nov 2017 03:27:11 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/11/a-modest-proposal/</guid>
|
||||
<description>
|
||||
<![CDATA[With Apple sales and graphing <a href="http://leancrew.com/all-this/2017/11/apple-sales-graphs-and-the-iphone-7/">on my mind</a>, I’d like to make a small complaint and suggestion to the folks at MacStories: some of your <a href="https://www.macstories.net/news/apple-q4-2017-results-52-6-billion-revenue-46-7-million-iphones-10-3-million-ipads-sold/">quarterly graphs</a> could use a little scrubbing; they’d be much easier on the eyes if they were cleaned up a bit.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>With Apple sales and graphing <a href="http://leancrew.com/all-this/2017/11/apple-sales-graphs-and-the-iphone-7/">on my mind</a>, I’d like to make a small complaint and suggestion to the folks at MacStories: some of your <a href="https://www.macstories.net/news/apple-q4-2017-results-52-6-billion-revenue-46-7-million-iphones-10-3-million-ipads-sold/">quarterly graphs</a> could use a little scrubbing; they’d be much easier on the eyes if they were cleaned up a bit.</p>
|
||||
<p>I’m talking about the ones that look like this:</p>
|
||||
<p><img alt="MacStories unit sales graph" class="ss" src="http://leancrew.com/all-this/images2017/20171106-MacStories%20unit%20sales%20graph.jpg" title="MacStories unit sales graph"/></p>
|
||||
<p>My complaint is with the clutter along the x-axis. That uniform gray mass of text detracts from the data above it. And although putting a label at each quarter seems to provide more information to the reader, the reader actually gets less out of it because it’s too hard to read. The axis would be improved if it had just yearly Q1 labels. Maybe even one label every other year. Fewer labels would mean they could be turned back horizontal, which would be easier to read and would give more vertical space for the data.</p>
|
||||
<p>I suspect every data point is labeled because that’s the way Numbers wants to do it, and MacStories’ automated system for generating these graphs follows the Numbers defaults. But if you were making these graphs by hand, you’d never label the x-axis this way. An automated system shouldn’t, either.</p>
|
||||
<p>On the Mac, Numbers has a way to skip labels on the x-axis by selecting Custom Category Intervals from the Category Labels popup menu and then choosing how often to put the labels.</p>
|
||||
<p><img alt="Mac Numbers axis options" class="ss" src="http://leancrew.com/all-this/images2017/20171106-Mac%20Numbers%20axis%20options.png" title="Mac Numbers axis options"/></p>
|
||||
<p>Sadly, I can’t find that option in the iOS version. Maybe it’s tucked away in a less obvious place.</p>
|
||||
<p><img alt="iOS Numbers axis options" class="ss" src="http://leancrew.com/all-this/images2017/20171106-iOS%20Numbers%20axis%20options.jpg" title="iOS Numbers axis options" width="50%"/></p>
|
||||
<p>Personally, I’m not a fan of either version of Numbers when it comes to making graphs because I like way more control than they give. But Numbers (like most graphing programs and libraries) can make nice graphs if you’re willing to break away from its defaults.</p>
|
||||
<p>I don’t really want to pick on MacStories. Their graphs are as good as, if not better than, what you typically find in blogs and news sites. Most graphs, unfortunately, are based on the default settings of the software that made them, and come out looking a little clumsy.</p>
|
||||
<p>I read MacStories because it has stylish writing. I’d like its graphs to be just as stylish.</p>
|
||||
<div class="update">
|
||||
<p><strong>Update Nov 7, 2017 8:55 PM</strong><br/>
|
||||
I was informed on Twitter by two authoritative sources (<a href="https://twitter.com/greyham/status/927795367361441793">this one</a> and <a href="https://twitter.com/viticci/status/927830126481608705">this one</a>) that the MacStories graphs are made with Excel, not Numbers. I haven’t used Excel for graphing since the mid-90s, but even then it had more customization options than Numbers has now. So I’m crossing my fingers for cleaner x-axes in January’s graphs.</p>
|
||||
</div>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/11/a-modest-proposal/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Apple sales graphs and the iPhone 7</title>
|
||||
<link>http://leancrew.com/all-this/2017/11/apple-sales-graphs-and-the-iphone-7/</link>
|
||||
<pubDate>Mon, 06 Nov 2017 02:53:19 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/11/apple-sales-graphs-and-the-iphone-7/</guid>
|
||||
<description>
|
||||
<![CDATA[Last week Apple released its 2017 Q4 (which everyone else’s calendar says is 2017 Q3) <a href="https://www.apple.com/newsroom/2017/11/apple-reports-fourth-quarter-results/">sales and revenue figures</a>, which means a boatload of graphs from all your fave rave Apple-oriented bloggers. I’m a few days late, as usual, despite having long ago written scripts to generate these graphs.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>Last week Apple released its 2017 Q4 (which everyone else’s calendar says is 2017 Q3) <a href="https://www.apple.com/newsroom/2017/11/apple-reports-fourth-quarter-results/">sales and revenue figures</a>, which means a boatload of graphs from all your fave rave Apple-oriented bloggers. I’m a few days late, as usual, despite having long ago written scripts to generate these graphs.</p>
|
||||
<p>I don’t make as many graphs as everyone else, partly because I’m lazy, but mainly because don’t care about revenue. My concern is the popularity of the devices I use, not how expensive they are. I want third-party developers to keep writing apps for them, and that means there has to be a market for them out there.</p>
|
||||
<p>Here’s the summary graph, with the sales figures for the three main Apple products. The dots are the quarterly sales and the lines are the four-quarter moving averages.</p>
|
||||
<p><img alt="Apple sales" class="ss" src="http://leancrew.com/all-this/images2017/20171105-Apple%20sales.png" title="Apple sales"/></p>
|
||||
<p>The iPhone dominates the scale of this graphs, so it’s helpful to also see the iPad and Mac on their own graphs.</p>
|
||||
<p><img alt="iPad sales" class="ss" src="http://leancrew.com/all-this/images2017/20171105-iPad%20sales.png" title="iPad sales"/></p>
|
||||
<p><img alt="Mac sales" class="ss" src="http://leancrew.com/all-this/images2017/20171105-Mac%20sales.png" title="Mac sales"/></p>
|
||||
<p>The good news is the second straight quarter of year-over-year increase in iPad sales after its long, well-documented slide since 2013. Additional good news is the Mac’s slow but consistent rise over the past year—four straight quarters of year-over-year sales increases. Imagine how much better the figures would be if people really liked the Touch Bar.</p>
|
||||
<p>I think its fair to say, though, that the iPhone 7 was something of a dud. It had a good start, but its non-intro quarters had almost exactly the same sales as the 6S had. Look at the final three quarters of each.</p>
|
||||
<p><img alt="Apple sales annotated" class="ss" src="http://leancrew.com/all-this/images2017/20171105-Apple%20sales%20annotated.png" title="Apple sales annotated"/></p>
|
||||
<p><a href="http://leancrew.com/all-this/2016/07/obligatory-apple-sales-post/">As I’ve argued before</a>, the iPhone 6S sales looked lackluster only in comparison to the gangbusters popularity of the iPhone 6. If one ignored the year of the 6, the 6S’s sales were more or less on the same upward trend as the previous three editions. The iPhone 7 was on that same trend for its first quarter but went dead flat after that.</p>
|
||||
<p>Apple’s report in January will be very interesting.</p>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/11/apple-sales-graphs-and-the-iphone-7/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Another one-off Keyboard Maestro macro</title>
|
||||
<link>http://leancrew.com/all-this/2017/11/another-one-off-keyboard-maestro-macro/</link>
|
||||
<pubDate>Fri, 03 Nov 2017 02:02:41 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/11/another-one-off-keyboard-maestro-macro/</guid>
|
||||
<description>
|
||||
<![CDATA[Do you use Keyboard Maestro (or AppleScript or whatever) for one-time, throwaway macros as often as you should? I know I don’t, but I did put one together a couple of days ago and used a feature I’d never tried before.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>Do you use Keyboard Maestro (or AppleScript or whatever) for one-time, throwaway macros as often as you should? I know I don’t, but I did put one together a couple of days ago and used a feature I’d never tried before.</p>
|
||||
<p>I had one of those <a href="http://leancrew.com/all-this/2015/10/dealing-with-a-recalcitrant-pdf/">recalcitrant PDFs</a> that I often get from clients. This one was 25–30 pages long, each an E-sized floor plan drawing for a building. The drawings were all black-and-white, but the PDF had color annotations added. I needed to add my own annotations to most of the pages, but something about the format of the file made it very cumbersome to work with. I tried Preview, PDF Expert, and PDfpen Pro, and they all were glacially slow when panning, zooming, and switching pages.</p>
|
||||
<p>So I broke the file up into individual pages using <a href="https://www.pdflabs.com/tools/pdftk-server/">PDFtk</a>:</p>
|
||||
<pre><code>pdftk drawings.pdf burst
|
||||
</code></pre>
|
||||
<p>The single-page files didn’t make me wait for the spinning beach ball, so I was able to add my annotations quickly in <a href="https://pdfexpert.com/">PDF Expert</a>. Then came an impasse.</p>
|
||||
<p>I wanted to email the drawings with my annotations back to the client and to some other parties, but they were too big to fit in an email. I could use multiple emails, but that’s a recipe for losing some of the files. I could use a Dropbox link, but I had a sense that one of the other parties wouldn’t understand how that worked. What seemed best was to convert the files to JPEGs at the lowest legible resolution, zip them together, and send the zipped file in a single email.<sup id="fnref:zip"><a href="#fn:zip" rel="footnote">1</a></sup></p>
|
||||
<p>My normal practice would be to use <a href="https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/sips.1.html"><code>sips</code></a> for this, because I can issue a single command to convert any number of files. But I soon learned that <code>sips</code> doesn’t handle annotations properly when converting the format of a file from PDF to JPEG. In my brief testing, I found that neither my annotations or the ones that came from the client were visible in the converted JPEGs.</p>
|
||||
<p>Preview, though, <em>can</em> export a PDF as a JPEG with the annotations intact and visible. Which presumably means that <code>sips</code> and Preview are using different code bases for the conversion. Whatever.</p>
|
||||
<p>The problem with using Preview is I’d have to convert every file by hand. Not the most burdensome job I’ve ever had, but one that’s boring and susceptible to error. Enter Keyboard Maestro.</p>
|
||||
<p>Here’s the macro that exports the current file in Preview to a JPEG and closes it:</p>
|
||||
<p><img alt="Convert to JPEG macro" class="ss" src="http://leancrew.com/all-this/images2017/20171101-Convert%20to%20JPEG%20macro.png" title="Convert to JPEG macro" width="80%"/></p>
|
||||
<p>You’ll note there’s no step for setting the resolution for the exported file. That’s because once it’s set, it doesn’t change from one export to the next.</p>
|
||||
<p>I didn’t try to have the macro open each PDF in turn, because I didn’t trust myself to do that right. I just opened all of them and then ran the macro by pressing ⌃⌥⌘J repeatedly. The macro closed each file after exporting it, leaving the next window ready to be operated on.</p>
|
||||
<p>The new (for me) thing with this macro was the “and drag to” part at the bottom of the third step. That’s what moved the popup menu selection from <span class="menu">PDF</span> up to <span class="menu">JPEG</span>.</p>
|
||||
<p><img alt="Preview export sheet" class="ss" src="http://leancrew.com/all-this/images2017/20171102-Preview%20export%20sheet.png" title="Preview export sheet"/></p>
|
||||
<p>I figured out how much to drag by taking a screenshot like the one above and using a selection in <a href="https://flyingmeat.com/acorn/">Acorn</a> to measure the vertical distance from the center of <span class="menu">PDF</span> to the center of <span class="menu">JPEG</span>. No trial and error.</p>
|
||||
<p>With 25–30 files to convert, it’s possible I did save time with this macro. But the main reason I made it was to avoid the tedium and the likelihood of error on my part. These are not independent—I’m far more prone to make errors when the task is repetitive and doesn’t maintain my attention.</p>
|
||||
<div class="footnotes">
|
||||
<hr/>
|
||||
<ol>
|
||||
<li id="fn:zip">
|
||||
<p>No, a Dropbox link is really no more complicated than a zip file, but zip files are more familiar to more people. And although zipping JPEGs doesn’t make them smaller, it does package them up in a way that naive Windows users are usually comfortable with. It would be lovely if I always worked with people whose computer skills were trustworthy, but that’s not the world I live in. <a href="#fnref:zip" rev="footnote">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/11/another-one-off-keyboard-maestro-macro/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Binomial baseball</title>
|
||||
<link>http://leancrew.com/all-this/2017/10/binomial-baseball/</link>
|
||||
<pubDate>Mon, 30 Oct 2017 18:14:47 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/10/binomial-baseball/</guid>
|
||||
<description>
|
||||
<![CDATA[While reading a recap of last night’s World Series games, I saw this statistic: of the 65 Series that have had a sixth game, the team with the 3–2 lead has won the Series 43 times. This was, I think, intended to show us that the Astros have a strong chance to beat out the Dodgers for the title. And they do. But not as strong as you might expect.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>While reading a recap of last night’s World Series games, I saw this statistic: of the 65 Series that have had a sixth game, the team with the 3–2 lead has won the Series 43 times. This was, I think, intended to show us that the Astros have a strong chance to beat out the Dodgers for the title. And they do. But not as strong as you might expect.</p>
|
||||
<p>If the teams were evenly balanced and each game independent of the others, we would expect the team with the 3–2 lead to win 75% of the time. 50% of the time they’d win the sixth game and the Series would be over; 25% of the time (50% of the other 50%) they’d win in the seventh game. So the leading team “should” have won the Series 48 or 49 times out of 65, not 43 times.</p>
|
||||
<p>Is this 5 or 6 game difference meaningful? For that we need to do some calculations using the <a href="https://en.wikipedia.org/wiki/Binomial_distribution">binomial distribution</a>. Python’s SciPy set of libraries has a subsection of statistical modules, including one for <a href="https://docs.scipy.org/doc/scipy-0.19.1/reference/generated/scipy.stats.binom.html">binomial distribution calculations</a>. We can import it this way:</p>
|
||||
<pre><code>python:
|
||||
from scipy.stats import binom
|
||||
</code></pre>
|
||||
<p>Let’s start by figuring out the probability that the leading team would win 43 times in 65 trials. With a 75% probability of winning the Series in each trial, the probability of 43 Series wins in 65 chances is calculated through</p>
|
||||
<pre><code>python:
|
||||
binom.pmf(43, 65, .75)
|
||||
</code></pre>
|
||||
<p>where the <code>pmf</code> function gets its name from the standard abbreviation for “<a href="https://en.wikipedia.org/wiki/Probability_mass_function">probability mass function</a>.” The answer is 0.029 or just under 3%. This makes it seem very unlikely that our assumption of 50–50 games would lead to only 43 Series wins for the leading team.</p>
|
||||
<p>But that isn’t the way these sorts of calculations are normally done. If we want to find out if a seemingly out-of-whack result is “statistically significant,” we should look at the probability of results that are at least as far away from our expectations as the actual result was. In our case, that means looking not only at the probability of 43 Series wins out of 65 chances, but also 42 wins, 41 wins, and so on. We then add up all of these “at least as weird” probabilities.</p>
|
||||
<p>The usual terminology for this sort of summation is “cumulative distribution function,” and the <code>binom</code> module has a function for it:</p>
|
||||
<pre><code>python:
|
||||
binom.cdf(43, 65, .75)
|
||||
</code></pre>
|
||||
<p>The result is 0.0695, or about 7%. Another way of looking at this is that if our assumption of 50–50 games were correct, there’s a 93% chance that the leading team would win the Series more than 43 times in 65 chances.</p>
|
||||
<p>In <a href="https://onlinecourses.science.psu.edu/statprogram/node/138">hypothesis testing</a>, the value 0.0695 is called the <em>p-value</em>,
|
||||
and it’s common in many fields to consider a result statistically significant if its p-value is less than 0.05. Using that criterion, we would not take the difference between our “null hypothesis” of 50–50 games and the World Series history as statistically significant.<sup id="fnref:two"><a href="#fn:two" rel="footnote">1</a></sup></p>
|
||||
<p>But it’s something for Dodgers fans to cling to.</p>
|
||||
<div class="footnotes">
|
||||
<hr/>
|
||||
<ol>
|
||||
<li id="fn:two">
|
||||
<p>Yes, I’ve been a little breezy here with my definition of null and alternate hypotheses and one-sided vs. two-sided rejection areas, but it’s just baseball. <a href="#fnref:two" rev="footnote">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/10/binomial-baseball/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Judas</title>
|
||||
<link>http://leancrew.com/all-this/2017/10/judas/</link>
|
||||
<pubDate>Sat, 28 Oct 2017 14:33:47 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/10/judas/</guid>
|
||||
<description>
|
||||
<![CDATA[I can’t say I wasn’t warned. The concert’s promotional artwork (especially those fonts) and the previous two albums, <a href="https://geo.itunes.apple.com/us/album/shadows-in-the-night/id945762927?uo=4&at=10l4Fv"><em>Shadows in the Night</em></a> and <a href="https://geo.itunes.apple.com/us/album/fallen-angels/id1099677284?uo=4&at=10l4Fv"><em>Fallen Angels</em></a>, were strong clues that I was going to be seeing a Bob Dylan infected by the Great American Songbook.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>I can’t say I wasn’t warned. The concert’s promotional artwork (especially those fonts) and the previous two albums, <a href="https://geo.itunes.apple.com/us/album/shadows-in-the-night/id945762927?uo=4&at=10l4Fv"><em>Shadows in the Night</em></a> and <a href="https://geo.itunes.apple.com/us/album/fallen-angels/id1099677284?uo=4&at=10l4Fv"><em>Fallen Angels</em></a>, were strong clues that I was going to be seeing a Bob Dylan infected by the Great American Songbook.</p>
|
||||
<p><img alt="Dylan concert promo" class="ss" src="http://leancrew.com/all-this/images2017/20171028-Dylan%20concert%20promo.png" title="Dylan concert promo"/></p>
|
||||
<p>So yes, we got “Melancholy Mood” and “Autumn Leaves” and “The September of My Years” with Bob crooning into a big microphone and holding its stand at a jaunty angle. But that’s not what I disliked about the concert. In fact, those songs were done pretty well, and Dylan’s voice sounded better than any previous concert I’ve been to. It was what he did to his own songs that was frustrating.</p>
|
||||
<p>No one in his right mind goes to a Dylan concert expecting to hear his classics played or sung the way they were done originally. But the treatment they’re given is always interesting and usually fun. Not this time. Apart from “It Ain’t Me, Babe” and “Highway 61 Revisited,” the Dylan songs suffered from arrangements that were, I guess, meant to match the style of the non-Dylan songs. Under these conditions, you can’t expect the songs to rock, but you can expect them to swing. And the band just didn’t swing.</p>
|
||||
<p>There was a sameness to Charlie Sexton’s guitar work throughout the night. Despite changing instruments several times, his sound was constrained and repetitive. But when a band doesn’t swing, it’s mainly because of the rhythm section. These are talented musicians who can play in any style, so if they didn’t find a groove it must have been because Bob didn’t want them to.</p>
|
||||
<p>As a result, he lost the audience. Many left early and those who hung in there stayed glued to their seats until the encores. Can you imagine a Dylan audience not standing to “Tangled Up in Blue”?</p>
|
||||
<p>The optimist in me notes that Dylan never sticks with anything for long, and after two albums of standards he may be ready for the next thing. I hope he comes back to the notion that that it’s his songs, and the blues and country/folk they came out of, that are truly the Great American Songbook and gives them the treatment they deserve.</p>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/10/judas/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Icons</title>
|
||||
<link>http://leancrew.com/all-this/2017/10/icons/</link>
|
||||
<pubDate>Fri, 27 Oct 2017 01:39:06 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/10/icons/</guid>
|
||||
<description>
|
||||
<![CDATA[I have some code sitting around here somewhere for extracting the images (app icons, book covers, album art, etc.) from the various iTunes and App Stores. But I think it’ll be easier to use <a href="http://brettterpstra.com/2017/10/25/itunesicon-2-dot-3/">Brett Terpstra’s iTunesIcon Automator app</a>, which just got an update.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>I have some code sitting around here somewhere for extracting the images (app icons, book covers, album art, etc.) from the various iTunes and App Stores. But I think it’ll be easier to use <a href="http://brettterpstra.com/2017/10/25/itunesicon-2-dot-3/">Brett Terpstra’s iTunesIcon Automator app</a>, which just got an update.</p>
|
||||
<p>Apart from its appeal to my laziness, Brett’s little app has a cool icon that I took an instant liking to.</p>
|
||||
<p><img alt="iTunesIcon icon" class="ss" src="http://leancrew.com/all-this/images2017/20171026-iTunesIcon%20icon.png" title="iTunesIcon icon"/></p>
|
||||
<p>If you work around industrial and construction equipment, Brett’s icon should bring a smile of familiarity. It’s highly reminiscent of the hands in safety signs and decals that are in dire straits.</p>
|
||||
<p><img alt="Safety symbols" class="ss" src="http://leancrew.com/all-this/images2017/20171026-Safety%20symbols.png" title="Safety symbols"/></p>
|
||||
<p class="caption">Images from <a href="http://www.safetysign.com/iso-warning-labels">SafetySign.com</a></p>
|
||||
<p>I’m sure your fingers are safe when using iTunesIcon.</p>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/10/icons/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Feed reading</title>
|
||||
<link>http://leancrew.com/all-this/2017/10/feed-reading/</link>
|
||||
<pubDate>Mon, 23 Oct 2017 00:10:42 +0000</pubDate>
|
||||
<dc:creator>
|
||||
<![CDATA[Dr. Drang]]>
|
||||
</dc:creator>
|
||||
<guid>http://leancrew.com/all-this/2017/10/feed-reading/</guid>
|
||||
<description>
|
||||
<![CDATA[Gabe Weatherhead has <a href="http://www.macdrifter.com/2017/10/feedbin-as-a-multi-device-reader.html">a nice article today</a> on how he uses Feedbin to handle his reading on the web. If you follow Gabe—and if not, why don’t you?—it will not surprise you to learn that he’s formidably organized. I don’t see myself following in his footsteps, but it’s always useful to learn how smart people do things. His article also reminded me that I’ve been meaning to write about <em>my</em> feed reading setup.]]>
|
||||
</description>
|
||||
<content:encoded>
|
||||
<![CDATA[<p>Gabe Weatherhead has <a href="http://www.macdrifter.com/2017/10/feedbin-as-a-multi-device-reader.html">a nice article today</a> on how he uses Feedbin to handle his reading on the web. If you follow Gabe—and if not, why don’t you?—it will not surprise you to learn that he’s formidably organized. I don’t see myself following in his footsteps, but it’s always useful to learn how smart people do things. His article also reminded me that I’ve been meaning to write about <em>my</em> feed reading setup.</p>
|
||||
<p>A couple of years ago, I let my subscription to <a href="https://feedwrangler.net/welcome.html">Feed Wrangler</a> lapse and started using a homemade, web-based RSS reading system. The heart of the system is still the script described in <a href="http://leancrew.com/all-this/2015/11/simpler-syndication/">this post</a>, but with a some changes as I thought of better ways of doing things.</p>
|
||||
<p>The biggest change came in the past few months. Initially, I created a single static web page with all of today’s articles from the feeds I subscribe to. A <a href="https://en.wikipedia.org/wiki/Cron">cron</a> task updated the page a few times an hour throughout the day. In this system, “today” was defined as “from 10:00 pm last night until now,” and the page would grow in size from morning to night.</p>
|
||||
<p>The advantage of this temporal arrangement from a programming point of view was that I didn’t have to write any code to keep track of whether I’d already read an article or not, and there were no external dependencies. If it was published “today,” it got on the page.</p>
|
||||
<p>The disadvantage was from the reading point of view. As I visited the page throughout the day, it became more and more filled with article I’d already read. This wasn’t as terrible as you might think. The articles were arranged in reverse chronological order by publication time, so the ones I’d read were typically at the bottom of the page. I say “typically” because some feeds—<a href="https://xkcd.com/">XKCD</a> comes to mind—are very bad at providing accurate publication times and their articles would sometimes end up at the bottom despite being recently published.</p>
|
||||
<p>Eliminating the reading disadvantage meant keeping track of what I’d read and showing only what I hadn’t—eliminating the programming advantage. I decided to keep track of read articles in an <a href="https://www.sqlite.org/">SQLite</a> database and to add items to that database through a button placed at the bottom of each article on my RSS page.</p>
|
||||
<p><img alt="RSS buttons" class="ss" src="http://leancrew.com/all-this/images2017/20171022-RSS%20buttons.png" title="RSS buttons" width="60%"/></p>
|
||||
<p>This meant</p>
|
||||
<ol>
|
||||
<li>Building a database that would uniquely identify every article. This was pretty simple. Each record has just two fields: the name of the website and the unique article ID (which is often just the article’s URL but is sometimes a long alphanumeric string generated by the site’s blogging software).</li>
|
||||
<li>Altering the existing script that builds the RSS page to filter out feed items that are in the database. Because Python has an <a href="https://docs.python.org/2/library/sqlite3.html">SQLite module</a> as part of its standard library and the syntax of SQL commands is straightforward, this wasn’t as tricky as I thought it would be. In fact, the new code is easier to read than the time-based filtering code I removed.</li>
|
||||
<li>Writing a server script (basically just a CGI script) to add an article to the database when given the blog name and article ID via the <code>POST</code> method. It’s been a while since I last wrote a CGI script, but it was like riding a bicycle.</li>
|
||||
<li>Adding some JavaScript with <a href="https://en.wikipedia.org/wiki/XMLHttpRequest"><code>XMLHttpRequest</code></a> to the RSS page to call the server script when a button is pressed. This took the most time, mainly because everyone in the world (except me) knows how to do AJAX now, and finding references written at an appropriately low level was harder than I expected. I found <a href="https://stackoverflow.com/questions/9713058/send-post-data-using-xmlhttprequest">this Stack Overflow discussion</a> helpful.</li>
|
||||
</ol>
|
||||
<p>So now I usually tap the <span class="menu">Mark as read</span> button when I get to the end of an article. If it’s a <a href="https://stratechery.com/">long article</a> that I want to read later, I don’t mark it as read, and it’ll be there the next time I bring up the RSS page.</p>
|
||||
<p>Fearing I’d forget how to use <code>XMLHttpRequest</code>, I quickly included another form at the end of each article for adding that article to my <a href="https://pinboard.in/api">Pinboard</a> account. I didn’t bother adding labels to the text field, because I’m the only one who uses this and I know the field is for tags. I did, however, include some DOM stuff to make it obvious when I’d marked an article as read or added it to Pinboard.</p>
|
||||
<p><img alt="RSS buttons marked" class="ss" src="http://leancrew.com/all-this/images2017/20171022-RSS%20buttons%20marked.png" title="RSS buttons marked" width="60%"/></p>
|
||||
<p>What I like about this system is how portable and (I hope) future-proof it is. I’ve been reluctant to sign on with Feedbin or Feedly or BazQux or any of the other Google Reader replacements because I worry they’ll <a href="http://observer.com/2017/01/medium-fail-post-literary-genre/">write a Medium post</a> and disappear with my subscription list and whatever organization scheme I’ve created. My system can run on any web server with Python, SQLite, and a <code>cgi-bin</code> directory. I think that’ll mean “any server, anywhere” for a very long time.</p>
|
||||
<br />
|
||||
<p>[If the formatting looks odd in your feed reader, <a href="http://leancrew.com/all-this/2017/10/feed-reading/">visit the original article</a>]</p>]]>
|
||||
</content:encoded>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
|
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue